diff --git a/.env.example b/.env.example index 9fc802e..52efed8 100644 --- a/.env.example +++ b/.env.example @@ -11,3 +11,19 @@ POSTGRES_OPTIONS="postgresql://postgres:9UMrd6hKBho4RhoyP47t@data1.cyberhybridhu SANDBOX_MODE="true" DISABLE_EXTERNAL_APIS="true" LOCAL_DEV_MODE="true" + +# Stripe Configuration +# STRIPE_SECRET_KEY: Secret key for Stripe API (server-side use only) +# Get this from your Stripe Dashboard: https://dashboard.stripe.com/apikeys +# Example: sk_test_51AbCdEfGhIjKlMnOpQrStUvWxYz1234567890 +STRIPE_SECRET_KEY="" + +# STRIPE_WEBHOOK_SECRET: Secret used to verify Stripe webhook signatures +# Get this from your Stripe Dashboard: https://dashboard.stripe.com/webhooks +# Example: whsec_1234567890abcdef1234567890abcdef1234567890abcdef +STRIPE_WEBHOOK_SECRET="" + +# STRIPE_PUBLISHABLE_KEY: Publishable key for Stripe.js (client-side use) +# Get this from your Stripe Dashboard: https://dashboard.stripe.com/apikeys +# Example: pk_test_51AbCdEfGhIjKlMnOpQrStUvWxYz1234567890 +STRIPE_PUBLISHABLE_KEY="" diff --git a/TODO.md b/TODO.md index 088a1be..9bf886b 100644 --- a/TODO.md +++ b/TODO.md @@ -5,11 +5,6 @@ development plans for the application. ## High Priority -- **Implement Lease Creation for Landlord Dashboard**: Add a "Create New Lease" - button to the landlord dashboard that allows landlords to create leases with - basic price information (rent, security deposit). Leases should have two - states: 'new' (initial state) and 'signed' (after tenant accepts invite). The - tenant field should be optional until a tenant accepts an invitation. ## Backend & API @@ -28,7 +23,7 @@ development plans for the application. - Tenant invitation and management (`islands/LandlordInviteTenant.tsx`) - Messaging with tenants (`islands/LandlordMessageThreads.tsx`) - Financial tracking and reporting - - Lease creation and management (current task) + - **Complete Tenant Dashboard**: Finalize all features under the `routes/tenant/` directory. This includes: - Viewing lease documents diff --git a/islands/AddPaymentMethodModal.tsx b/islands/AddPaymentMethodModal.tsx new file mode 100644 index 0000000..586c687 --- /dev/null +++ b/islands/AddPaymentMethodModal.tsx @@ -0,0 +1,231 @@ +import { useCallback, useRef, useState } from "preact/hooks"; +import { + encryptDataWithStoredPassword, + getEncryptionPassword, +} from "../helpers.ts"; + +interface AddPaymentMethodModalProps { + userId?: string; +} + +export default function AddPaymentMethodModal( + { userId }: AddPaymentMethodModalProps, +) { + const formRef = useRef(null); + const encryptedRoutingNumberRef = useRef(null); + const encryptedAccountNumberRef = useRef(null); + const bankNameRef = useRef(null); + const accountTypeRef = useRef(null); + const routingNumberRef = useRef(null); + const accountNumberRef = useRef(null); + const isDefaultRef = useRef(null); + const [loading, setLoading] = useState(false); + + // Only encrypt on submit, not on every keystroke - prevents V8 overload + const handleEncryptData = useCallback(async () => { + // Get current values from the form inputs + const routingNumber = routingNumberRef.current?.value || ""; + const accountNumber = accountNumberRef.current?.value || ""; + + // Check if encryption is available and encrypt sensitive fields + if (userId && (routingNumber || accountNumber)) { + const encryptionPassword = getEncryptionPassword(userId); + + if (encryptionPassword) { + try { + // Encrypt routing number + if (routingNumber && encryptedRoutingNumberRef.current) { + const encryptedRoutingNumber = await encryptDataWithStoredPassword( + userId, + routingNumber, + ); + if (encryptedRoutingNumber) { + encryptedRoutingNumberRef.current.value = encryptedRoutingNumber; + } + } + + // Encrypt account number + if (accountNumber && encryptedAccountNumberRef.current) { + const encryptedAccountNumber = await encryptDataWithStoredPassword( + userId, + accountNumber, + ); + if (encryptedAccountNumber) { + encryptedAccountNumberRef.current.value = encryptedAccountNumber; + } + } + } catch (error) { + console.error("Failed to encrypt payment method data:", error); + } + } + } + }, [userId]); + + const handleAddClick = async () => { + // Validate form + const bankName = bankNameRef.current?.value?.trim() || ""; + const accountType = accountTypeRef.current?.value || ""; + const routingNumber = routingNumberRef.current?.value?.trim() || ""; + const accountNumber = accountNumberRef.current?.value?.trim() || ""; + + if (!bankName || !accountType || !routingNumber || !accountNumber) { + alert("Please fill in all required fields."); + return; + } + + if (routingNumber.length !== 9) { + alert("Routing number must be 9 digits."); + return; + } + + if (accountNumber.length < 4) { + alert("Account number must be at least 4 digits."); + return; + } + + setLoading(true); + // Encrypt data before submission + await handleEncryptData(); + + // Clear plaintext fields if encryption was successful + if (userId && (routingNumber || accountNumber)) { + const encryptionPassword = getEncryptionPassword(userId); + + if (encryptionPassword) { + if (routingNumberRef.current) routingNumberRef.current.value = ""; + if (accountNumberRef.current) accountNumberRef.current.value = ""; + } + } + + // Submit the form + setTimeout(() => { + if (formRef.current) { + formRef.current.submit(); + } + }, 100); + }; + + return ( +
+

Add Payment Method

+
+ + + + +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+ +
+ + Cancel + + +
+
+ ); +} diff --git a/islands/PaymentMethodsList.tsx b/islands/PaymentMethodsList.tsx new file mode 100644 index 0000000..b08f47a --- /dev/null +++ b/islands/PaymentMethodsList.tsx @@ -0,0 +1,302 @@ +// islands/PaymentMethodsList.tsx +import { PaymentMethod } from "../services/paymentService.ts"; +import { useEffect, useState } from "preact/hooks"; +import { PopAddPaymentMethodDialog } from "./dialogs/PopAddPaymentMethodDialog.tsx"; +import { + decryptDataWithStoredPassword, + getEncryptionPassword, +} from "../helpers.ts"; + +interface Props { + paymentMethods: PaymentMethod[]; + userId?: string; + openModal?: boolean; +} + +interface ProcessedPaymentMethod extends PaymentMethod { + maskedAccountNumber?: string; + maskedRoutingNumber?: string; +} + +export default function PaymentMethodsList( + { paymentMethods, userId, openModal = false }: Props, +) { + const [_popAddPaymentMethod, popupAddPaymentMethodDialog] = + PopAddPaymentMethodDialog({ + userId, + defaultOpen: openModal, + }); + const [processedMethods, setProcessedMethods] = useState< + ProcessedPaymentMethod[] + >([]); + const [isProcessing, setIsProcessing] = useState(false); + const [_isOpeningModal, _setIsOpeningModal] = useState(openModal); + + // Process payment methods on component mount and when paymentMethods or userId change + useEffect(() => { + setTimeout(() => { + const lockRoute = localStorage.getItem("lock-route"); + const url = new URL(globalThis.location.href); + const currentRoute = url.pathname + url.search; + + if (lockRoute && lockRoute === currentRoute) { + localStorage.removeItem("lock-route"); + } + }, 100); + + if (!userId) { + setProcessedMethods(paymentMethods as ProcessedPaymentMethod[]); + return; + } + + const processMethods = async () => { + setIsProcessing(true); + const encryptionPassword = getEncryptionPassword(userId); + + if (!encryptionPassword) { + // No encryption key available, use methods as-is + setProcessedMethods(paymentMethods as ProcessedPaymentMethod[]); + setIsProcessing(false); + return; + } + + const updatedMethods: ProcessedPaymentMethod[] = []; + + for (const method of paymentMethods) { + const processedMethod: ProcessedPaymentMethod = { ...method }; + + // Decrypt and mask account number + if (method.account_number_encrypted) { + try { + const decryptedData = await decryptDataWithStoredPassword( + userId, + method.account_number_encrypted, + ); + if (decryptedData) { + const accountNumber = decryptedData; + // Show only last 4 digits + processedMethod.maskedAccountNumber = "****" + + accountNumber.slice(-4); + } + } catch (error) { + console.error("Failed to decrypt account number:", error); + } + } + + // Decrypt and mask routing number + if (method.routing_number_encrypted) { + try { + const decryptedData = await decryptDataWithStoredPassword( + userId, + method.routing_number_encrypted, + ); + if (decryptedData) { + const routingNumber = decryptedData; + // Show only last 4 digits + processedMethod.maskedRoutingNumber = "****" + + routingNumber.slice(-4); + } + } catch (error) { + console.error("Failed to decrypt routing number:", error); + } + } + + updatedMethods.push(processedMethod); + } + + setProcessedMethods(updatedMethods); + setIsProcessing(false); + }; + + processMethods(); + }, [paymentMethods, userId]); + + const handleSetDefault = async (methodId: string) => { + try { + const response = await fetch("/api/update/payment_method", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ id: methodId, is_default: true }), + }); + + const data = await response.json(); + + if (data.success) { + // Refresh the page to show updated default + globalThis.location.reload(); + } else { + console.error("Failed to set default payment method:", data.error); + alert("Failed to set default payment method"); + } + } catch (error) { + console.error("Error setting default payment method:", error); + alert("Failed to set default payment method"); + } + }; + + if (isProcessing) { + return ( +
+
+
+ ); + } + + return ( + <> + {popupAddPaymentMethodDialog} + +
+ {processedMethods.length === 0 + ? ( +
+ + + +

+ No payment methods +

+

+ Add a payment method to get started with rent payments. +

+ +
+ ) + : ( + <> +
    + {processedMethods.map((method) => ( +
  • +
    +
    +
    +

    + {method.bank_name || "Bank Account"} +

    + {method.is_default && ( + + Default + + )} +
    +
    +
    + Type:{" "} + {method.account_type === "checking" + ? "Checking" + : method.account_type === "savings" + ? "Savings" + : method.account_type || "Unknown"} +
    +
    + Account:{" "} + {method.maskedAccountNumber || "****"} +
    +
    + Routing:{" "} + {method.maskedRoutingNumber || "****"} +
    +
    +
    + +
    + {!method.is_default && ( + + )} + +
    + + + +
    +
    +
    +
  • + ))} +
+ +
+ +
+ + )} +
+ + ); +} diff --git a/islands/TenantSettings.tsx b/islands/TenantSettings.tsx index 6c8b442..74cdcd8 100644 --- a/islands/TenantSettings.tsx +++ b/islands/TenantSettings.tsx @@ -185,6 +185,14 @@ export default function TenantSettings({ user }: TenantSettingsProps) { checked={paymentPrefs.payment_reminders} onChange={() => handlePaymentChange("payment_reminders")} /> + {/* Auto-pay setup is handled on the Payment Methods page, not here */}
diff --git a/islands/dialogs/AddPaymentMethodDialog.tsx b/islands/dialogs/AddPaymentMethodDialog.tsx new file mode 100644 index 0000000..f54a8b7 --- /dev/null +++ b/islands/dialogs/AddPaymentMethodDialog.tsx @@ -0,0 +1,325 @@ +import { useEffect, useRef, useState } from "preact/hooks"; +import { + encryptDataWithStoredPassword, + getEncryptionPassword, +} from "../../helpers.ts"; + +interface AddPaymentMethodDialogProps { + onClose: () => void; + userId?: string; +} + +type FieldErrorState = { + bank_name: boolean; + account_type: boolean; + routing_number: boolean; + account_number: boolean; +}; + +export default function AddPaymentMethodDialog( + { onClose, userId }: AddPaymentMethodDialogProps, +) { + const formRef = useRef(null); + const firstInputRef = useRef(null); + const [loading, setLoading] = useState(false); + const [fieldErrors, setFieldErrors] = useState({ + bank_name: false, + account_type: false, + routing_number: false, + account_number: false, + }); + + useEffect(() => { + // Focus first input when dialog opens + setTimeout(() => { + firstInputRef.current?.focus(); + }, 100); + }, []); + + const clearFieldError = (field: keyof FieldErrorState) => { + setFieldErrors((prev) => ({ ...prev, [field]: false })); + }; + + const getInputErrorClass = (hasError: boolean) => { + return hasError ? "border-red-500 focus:border-red-500" : ""; + }; + + const addNewPaymentMethod = async () => { + if (!formRef.current) return; + + // Collect form data + const formData = new FormData(formRef.current); + const bankName = formData.get("bank_name")?.toString().trim() || ""; + const accountType = formData.get("account_type")?.toString() || ""; + const routingNumber = formData.get("routing_number")?.toString().trim() || + ""; + const accountNumber = formData.get("account_number")?.toString().trim() || + ""; + const isDefault = formData.get("is_default") === "true"; + const sender = typeof location !== "undefined" ? location.pathname : "/"; + + const validation = { + bank_name: !bankName, + account_type: !accountType, + routing_number: !routingNumber || routingNumber.length !== 9, + account_number: !accountNumber || accountNumber.length < 4, + }; + setFieldErrors(validation); + + if ( + validation.bank_name || validation.account_type || + validation.routing_number || validation.account_number + ) { + return; + } + + setLoading(true); + localStorage.setItem("lock-route", "/tenant/payment-methods?modal=off"); + + // Build submission data + const submitData = new FormData(); + submitData.set("sender", sender); + submitData.set("bank_name", bankName); + submitData.set("account_type", accountType); + submitData.set("is_default", isDefault ? "true" : "false"); + + // Encrypt sensitive fields if password is available + const encryptionPassword = userId ? getEncryptionPassword(userId) : null; + + if (encryptionPassword && (routingNumber || accountNumber)) { + try { + // Encrypt routing number + const encryptedRoutingNumber = await encryptDataWithStoredPassword( + userId!, + routingNumber, + ); + + // Encrypt account number + const encryptedAccountNumber = await encryptDataWithStoredPassword( + userId!, + accountNumber, + ); + + if (encryptedRoutingNumber && encryptedAccountNumber) { + submitData.set("routing_number_encrypted", encryptedRoutingNumber); + submitData.set("account_number_encrypted", encryptedAccountNumber); + // Send empty plaintext when encrypted + submitData.set("routing_number", ""); + submitData.set("account_number", ""); + } else { + throw new Error("Encryption returned empty result"); + } + } catch (error) { + console.error("Encryption failed:", error); + alert("Failed to encrypt payment method data. Please try again."); + setLoading(false); + return; + } + } else { + // No encryption available, send plaintext (not recommended for production) + console.warn("No encryption password available, sending plaintext"); + submitData.set("routing_number", routingNumber); + submitData.set("account_number", accountNumber); + } + + // Submit the form + try { + const response = await fetch("/api/create/payment_method", { + method: "POST", + body: submitData, + }); + + const data = await response.json(); + + if (data.success) { + // Redirect to payment methods page + globalThis.location.href = "/tenant/payment-methods"; + } else { + console.error("Failed to add payment method:", data.error); + alert(data.error || "Failed to add payment method. Please try again."); + setLoading(false); + } + } catch (error) { + console.error("Error adding payment method:", error); + alert("Failed to add payment method. Please try again."); + setLoading(false); + } + }; + + return ( +
+
+
+

+ Add Payment Method +

+ +
+
+ + clearFieldError("bank_name")} + /> +
+ Bank name is required. +
+
+ +
+ + +
+ Account type is required. +
+
+ +
+ + clearFieldError("routing_number")} + /> +
+ Routing number must be 9 digits. +
+
+ +
+ + clearFieldError("account_number")} + /> +
+ Account number is required (min 4 digits). +
+
+ +
+ + +
+
+ +
+ + +
+
+
+
+ ); +} diff --git a/islands/dialogs/PopAddPaymentMethodDialog.tsx b/islands/dialogs/PopAddPaymentMethodDialog.tsx new file mode 100644 index 0000000..b61048a --- /dev/null +++ b/islands/dialogs/PopAddPaymentMethodDialog.tsx @@ -0,0 +1,40 @@ +import { useState } from "preact/hooks"; +import AddPaymentMethodDialog from "./AddPaymentMethodDialog.tsx"; + +interface PopAddPaymentMethodDialogOptions { + userId?: string; + defaultOpen?: boolean; +} + +interface PopupController { + resolve?: (value: boolean) => void; +} + +export function PopAddPaymentMethodDialog( + options: PopAddPaymentMethodDialogOptions = {}, +) { + const [popup, setPopup] = useState(() => { + return options.defaultOpen ? { resolve: () => {} } : null; + }); + + // Show the popup and return a Promise for the answer + const popAddPaymentMethod = (): Promise => { + return new Promise((resolve) => { + setPopup({ resolve }); + }); + }; + + // Render the popup if needed + const popupAddPaymentMethodDialog = popup + ? ( + { + globalThis.location.href = "/tenant/payment-methods?modal=off"; + }} + userId={options.userId} + /> + ) + : null; + + return [popAddPaymentMethod, popupAddPaymentMethodDialog] as const; +} diff --git a/routes/api/payments/cancel_auto_pay.ts b/routes/api/payments/cancel_auto_pay.ts new file mode 100644 index 0000000..3d26fc9 --- /dev/null +++ b/routes/api/payments/cancel_auto_pay.ts @@ -0,0 +1,79 @@ +// routes/api/payments/cancel_auto_pay.ts +import { define } from "../../../utils.ts"; +import { cancelAutoPayment } from "../../../services/paymentService.ts"; +import { SessionCookie } from "../../auth/callback.ts"; + +export const handler = define.handlers({ + async POST(ctx) { + try { + const stateData: SessionCookie = ctx.state.data; + const user_id = stateData.user_id; + + if (!user_id) { + return new Response( + JSON.stringify({ + error: "User not authenticated", + }), + { + status: 401, + headers: { "Content-Type": "application/json" }, + }, + ); + } + + const contentType = ctx.req.headers.get("content-type") || ""; + let data: Record = {}; + + // Handle both JSON and form data + if (contentType.includes("application/json")) { + const jsonData = await ctx.req.json(); + data = jsonData; + } else { + const formData = await ctx.req.formData(); + data = { + lease_id: formData.get("lease_id")?.toString() || "", + }; + } + + const lease_id = data.lease_id as string; + + if (!lease_id) { + return new Response( + JSON.stringify({ + error: "Lease ID is required", + }), + { + status: 400, + headers: { "Content-Type": "application/json" }, + }, + ); + } + + const result = await cancelAutoPayment(lease_id, user_id); + + return new Response( + JSON.stringify({ + success: true, + lease: result[0], + }), + { + status: 200, + headers: { "Content-Type": "application/json" }, + }, + ); + } catch (e) { + console.error("Error canceling auto payment:", e); + return new Response( + JSON.stringify({ + error: e instanceof Error + ? e.message + : "Failed to cancel auto payment", + }), + { + status: 500, + headers: { "Content-Type": "application/json" }, + }, + ); + } + }, +}); diff --git a/routes/api/payments/set_auto_pay.ts b/routes/api/payments/set_auto_pay.ts new file mode 100644 index 0000000..bb59d3e --- /dev/null +++ b/routes/api/payments/set_auto_pay.ts @@ -0,0 +1,92 @@ +// routes/api/payments/set_auto_pay.ts +import { define } from "../../../utils.ts"; +import { setAutoPayment } from "../../../services/paymentService.ts"; +import { SessionCookie } from "../../auth/callback.ts"; + +export const handler = define.handlers({ + async POST(ctx) { + try { + const stateData: SessionCookie = ctx.state.data; + const user_id = stateData.user_id; + + if (!user_id) { + return new Response( + JSON.stringify({ + error: "User not authenticated", + }), + { + status: 401, + headers: { "Content-Type": "application/json" }, + }, + ); + } + + const contentType = ctx.req.headers.get("content-type") || ""; + let data: Record = {}; + + // Handle both JSON and form data + if (contentType.includes("application/json")) { + const jsonData = await ctx.req.json(); + data = jsonData; + } else { + const formData = await ctx.req.formData(); + data = { + lease_id: formData.get("lease_id")?.toString() || "", + payment_method_id: formData.get("payment_method_id")?.toString() || + "", + }; + } + + const lease_id = data.lease_id as string; + const payment_method_id = data.payment_method_id as string; + + if (!lease_id) { + return new Response( + JSON.stringify({ + error: "Lease ID is required", + }), + { + status: 400, + headers: { "Content-Type": "application/json" }, + }, + ); + } + + if (!payment_method_id) { + return new Response( + JSON.stringify({ + error: "Payment method ID is required", + }), + { + status: 400, + headers: { "Content-Type": "application/json" }, + }, + ); + } + + const result = await setAutoPayment(lease_id, user_id, payment_method_id); + + return new Response( + JSON.stringify({ + success: true, + lease: result[0], + }), + { + status: 200, + headers: { "Content-Type": "application/json" }, + }, + ); + } catch (e) { + console.error("Error setting auto payment:", e); + return new Response( + JSON.stringify({ + error: e instanceof Error ? e.message : "Failed to set auto payment", + }), + { + status: 500, + headers: { "Content-Type": "application/json" }, + }, + ); + } + }, +}); diff --git a/routes/tenant/payment-methods/index.tsx b/routes/tenant/payment-methods/index.tsx new file mode 100644 index 0000000..1994d25 --- /dev/null +++ b/routes/tenant/payment-methods/index.tsx @@ -0,0 +1,97 @@ +// routes/tenant/payment-methods/index.tsx +import { define } from "../../../utils.ts"; +import { page } from "fresh"; +import { PaymentMethod } from "../../../services/paymentService.ts"; +import PaymentMethodsList from "../../../islands/PaymentMethodsList.tsx"; + +interface PaymentMethodsPageData { + paymentMethods?: PaymentMethod[]; + error?: string; +} + +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 response = await fetch("/api/retrieve/payment_methods", { + headers: { + "Cookie": ctx.req.headers.get("Cookie") || "", + }, + }); + + const data = await response.json(); + + if (!data.success) { + console.error("Failed to fetch payment methods:", data.error); + return page({ + error: "Failed to load payment methods", + }); + } + + return page({ + paymentMethods: data.payment_methods || [], + }); + } catch (error) { + console.error("Error loading payment methods:", error); + return page({ + error: "Failed to load payment methods", + }); + } + }, +}); + +export default define.page(function PaymentMethodsPage({ + data, +}: { + data: PaymentMethodsPageData; +}) { + const { paymentMethods, error } = data as PaymentMethodsPageData; + + if (error) { + return ( +
+
+

Error

+

{error}

+ +
+
+ ); + } + + return ( +
+
+
+
+

+ Payment Methods +

+

+ Manage your payment methods for rent payments. +

+
+
+
+ +
+ +
+
+ ); +}); diff --git a/routes/tenant/payments/index.tsx b/routes/tenant/payments/index.tsx index 1a87503..e61fb05 100644 --- a/routes/tenant/payments/index.tsx +++ b/routes/tenant/payments/index.tsx @@ -103,7 +103,7 @@ export default define.page(function TenantPayments({ data }) { Payment Methods Manage Payment Methods diff --git a/routes/tenant/rent/pay/index.tsx b/routes/tenant/rent/pay/index.tsx index 01a9afc..a2e4aed 100644 --- a/routes/tenant/rent/pay/index.tsx +++ b/routes/tenant/rent/pay/index.tsx @@ -1,13 +1,10 @@ // 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"; @@ -70,24 +67,46 @@ export const handler = define.handlers({ 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 _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( + // Call the payment intent creation endpoint + const intentResponse = await fetch("/api/payments/create_intent", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ lease_id, amount, - getE2ENow(), - "ach", - "completed", + payment_method: "ach", + }), + }); + + const intentData = await intentResponse.json(); + + if (!intentData.success) { + console.error("Payment intent creation failed:", intentData.error); + return page({ + error: intentData.error || + "Failed to create payment intent. Please try again.", + }); + } + + // Redirect to confirmation page with payment details + const redirectUrl = new URL("/tenant/rent/confirmation", ctx.req.url); + redirectUrl.searchParams.set("success", "true"); + redirectUrl.searchParams.set("amount", String(amount)); + + if (intentData.mode === "stripe") { + redirectUrl.searchParams.set( + "payment_intent_id", + intentData.payment_intent_id, ); + redirectUrl.searchParams.set("client_secret", intentData.client_secret); } return new Response("", { status: 302, - headers: { Location: "/tenant/rent/confirmation?success=true" }, + headers: { Location: redirectUrl.toString() }, }); } catch (error) { console.error("Error processing payment:", error);