open-swe-dev-andrinoid[bot] 146dca155c Apply patch [skip ci]
2026-01-22 02:04:08 +00:00

350 lines
13 KiB
TypeScript

// routes/tenant/rent/pay/index.tsx
import { define } from "../../../../utils.ts";
import { SessionCookie } from "../../../auth/callback.ts";
import { getLeasesByTenant } from "../../../../services/leaseService.ts";
import {
getPaymentMethodsByUser,
PaymentMethod,
} from "../../../../services/paymentService.ts";
import { createPaymentTransaction } from "../../../../services/paymentService.ts";
import { getE2ENow, isE2EEnabled } from "../../../../services/e2eFixtures.ts";
import { page } from "fresh";
import { Lease } from "../../../../db/interfaces.ts";
// Flexible interface for rent payment page data
interface RentPaymentPageData {
currentLease?: Lease | null;
paymentMethods?: PaymentMethod[];
error?: string;
success?: boolean;
paymentAmount?: number;
dueDate?: string;
lateFee?: number;
// Allow additional properties for future extensibility
[key: string]: string | number | boolean | object | null | undefined;
}
export const handler = define.handlers({
async GET(ctx) {
const { user_id } = ctx.state.data;
if (!user_id) {
return new Response("", {
status: 302,
headers: { Location: "/auth/login" },
});
}
try {
const leases = await getLeasesByTenant(user_id);
const paymentMethods = await getPaymentMethodsByUser(user_id);
const currentLease = leases?.[0]; // Assuming tenant has one active lease
return page({
currentLease,
paymentMethods,
error: "",
success: false,
paymentAmount: 0,
dueDate: "",
lateFee: 0,
});
} catch (error) {
console.error("Error loading rent payment page:", error);
return page({
error: "Failed to load payment information",
});
}
},
async POST(ctx) {
const { user_id } = ctx.state.data;
if (!user_id) {
return new Response("", {
status: 302,
headers: { Location: "/auth/login" },
});
}
try {
const formData = await ctx.req.formData();
const amount = parseFloat(formData.get("amount") as string);
const payment_method_id = formData.get("payment_method_id") as string;
const lease_id = formData.get("lease_id") as string;
// TODO: Implement actual payment processing with Stripe or similar
// For now, simulate successful payment
if (isE2EEnabled()) {
await createPaymentTransaction(
lease_id,
amount,
getE2ENow(),
"ach",
"completed",
);
}
return new Response("", {
status: 302,
headers: { Location: "/tenant/rent/confirmation?success=true" },
});
} catch (error) {
console.error("Error processing payment:", error);
return page({
error: "Payment processing failed. Please try again.",
});
}
},
});
export default define.page(
function RentPayment({ data }: { data: RentPaymentPageData }) {
const { currentLease, paymentMethods, error } = data;
if (!currentLease) {
return (
<div class="min-h-screen bg-gray-50 flex items-center justify-center">
<div class="text-center">
<h1 class="text-2xl font-bold text-gray-900 mb-4">
No Active Lease
</h1>
<p class="text-gray-600">
You don't have an active lease to make payments for.
</p>
<a
href="/tenant/dashboard"
class="mt-4 inline-block text-blue-600 hover:text-blue-500"
>
Return to Dashboard
</a>
</div>
</div>
);
}
return (
<div class="min-h-screen bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
<div class="max-w-3xl mx-auto">
<div class="text-center mb-8">
<h2 class="text-3xl font-extrabold text-gray-900">Pay Rent</h2>
<p class="mt-2 text-sm text-gray-600">
Make your monthly rent payment securely online
</p>
</div>
<div class="bg-white shadow sm:rounded-lg">
<div class="px-4 py-5 sm:p-6">
{error && (
<div class="mb-6 bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded">
{error}
</div>
)}
{/* Property Information */}
<div class="mb-6 p-4 bg-gray-50 rounded-lg">
<h3 class="text-lg font-medium text-gray-900 mb-2">
Property Information
</h3>
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div>
<dt class="text-sm font-medium text-gray-500">
Property Address
</dt>
<dd class="mt-1 text-sm text-gray-900">
{currentLease.property_address || "Property Address"}
</dd>
</div>
<div>
<dt class="text-sm font-medium text-gray-500">
Monthly Rent
</dt>
<dd class="mt-1 text-sm text-gray-900">
${currentLease.rent?.toLocaleString() || "N/A"}
</dd>
</div>
<div>
<dt class="text-sm font-medium text-gray-500">Due Date</dt>
<dd class="mt-1 text-sm text-gray-900">
1st of each month
</dd>
</div>
<div>
<dt class="text-sm font-medium text-gray-500">Late Fee</dt>
<dd class="mt-1 text-sm text-gray-900">
$50 after 5th of month
</dd>
</div>
</div>
</div>
<form method="POST" class="space-y-6">
<input type="hidden" name="lease_id" value={currentLease.id} />
{/* Payment Amount */}
<div>
<label
for="amount"
class="block text-sm font-medium text-gray-700"
>
Payment Amount
</label>
<div class="mt-1 relative rounded-md shadow-sm">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<span class="text-gray-500 sm:text-sm">$</span>
</div>
<input
type="number"
name="amount"
id="amount"
step="0.01"
min="0"
value={currentLease.rent || 0}
required
class="focus:ring-blue-500 focus:border-blue-500 block w-full pl-7 pr-12 sm:text-sm border-gray-300 rounded-md"
placeholder="0.00"
/>
</div>
<p class="mt-2 text-sm text-gray-500">
Standard monthly rent:
${currentLease.rent?.toLocaleString() || "N/A"}
</p>
</div>
{/* Payment Method */}
<div>
<label
for="payment_method_id"
class="block text-sm font-medium text-gray-700"
>
Payment Method
</label>
<select
id="payment_method_id"
name="payment_method_id"
required
class="mt-1 block w-full pl-3 pr-10 py-2 text-base border-gray-300 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm rounded-md"
>
<option value="">Select payment method</option>
{paymentMethods && paymentMethods.length > 0
? (
paymentMethods.map((method) => (
<option key={method.id} value={method.id}>
{method.method_type === "bank_account"
? "Bank Account"
: "Credit Card"}
{method.is_default && " (Default)"}
</option>
))
)
: (
<option value="" disabled>
No payment methods available
</option>
)}
</select>
{(!paymentMethods || paymentMethods.length === 0) && (
<p class="mt-2 text-sm text-red-600">
You need to add a payment method first.{" "}
<a
href="/tenant/payment-methods/add"
class="text-blue-600 hover:text-blue-500"
>
Add payment method
</a>
</p>
)}
</div>
{/* Payment Schedule */}
<div class="bg-blue-50 border border-blue-200 rounded-md p-4">
<div class="flex">
<div class="flex-shrink-0">
<svg
class="h-5 w-5 text-blue-400"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fill-rule="evenodd"
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z"
clip-rule="evenodd"
/>
</svg>
</div>
<div class="ml-3">
<h3 class="text-sm font-medium text-blue-800">
Automatic Payments Available
</h3>
<div class="mt-2 text-sm text-blue-700">
<p>
Set up automatic monthly payments to never miss a due
date.{" "}
<a
href="/tenant/autopay/setup"
class="font-medium underline"
>
Enable autopay
</a>
</p>
</div>
</div>
</div>
</div>
{/* Security Notice */}
<div class="bg-gray-50 border border-gray-200 rounded-md p-4">
<div class="flex">
<div class="flex-shrink-0">
<svg
class="h-5 w-5 text-gray-400"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fill-rule="evenodd"
d="M5 9V7a5 5 0 0110 0v2a2 2 0 012 2v5a2 2 0 01-2 2H5a2 2 0 01-2-2v-5a2 2 0 012-2zm8-2v2H7V7a3 3 0 016 0z"
clip-rule="evenodd"
/>
</svg>
</div>
<div class="ml-3">
<h3 class="text-sm font-medium text-gray-800">
Secure Payment Processing
</h3>
<div class="mt-2 text-sm text-gray-600">
<p>
Your payment information is encrypted and processed
securely. You will receive a confirmation email once
your payment is processed.
</p>
</div>
</div>
</div>
</div>
{/* Submit Button */}
<div class="flex justify-between pt-6">
<a
href="/tenant/dashboard"
class="inline-flex justify-center py-2 px-4 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
>
Cancel
</a>
<button
type="submit"
disabled={!paymentMethods || paymentMethods.length === 0}
class="inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-green-600 hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500 disabled:bg-gray-400 disabled:cursor-not-allowed"
>
Pay ${currentLease.rent?.toLocaleString() || "0"}
</button>
</div>
</form>
</div>
</div>
</div>
</div>
);
},
);