// services/paymentService.ts // Handle payment methods, transactions, and rent schedules import { and, eq } from "drizzle-orm"; import { db } from "../db/db.ts"; import { leases, payment_methods, payment_transactions, rent_schedules, } from "../db/schema.ts"; export interface PaymentMethod { id: string; user_id: string; bank_name: string | null; method_type: string | null; // 'bank_account', 'credit_card' account_type: string | null; // 'checking', 'savings' routing_number_encrypted: string | null; account_number_encrypted: string | null; is_default: boolean | null; created_on: number; updated_on: number; } export interface PaymentTransaction { id: string; lease_id: string; amount: number; payment_date: number; payment_method: string | null; // 'card', 'ach', 'check', 'cash' transaction_status: string | null; // 'pending', 'completed', 'failed' stripe_payment_id: string | null; created_on: number; } export interface RentSchedule { id: string; property_id: string; rent_amount: number; due_day: number | null; // Day of month rent is due (1-31) late_fee_amount: number | null; grace_period_days: number | null; payment_methods_allowed: string | null; // JSON array of allowed methods created_on: number; updated_on: number; } // Payment Methods CRUD export async function createPaymentMethod( user_id: string, bank_name: string, method_type: string, account_type: string, routing_number_encrypted: string, account_number_encrypted: string, is_default: boolean = false, ) { const now = Date.now(); // If this is set as default, unset other defaults first if (is_default) { await db.update(payment_methods) .set({ is_default: false }) .where(eq(payment_methods.user_id, user_id)); } return await db.insert(payment_methods) .values({ id: crypto.randomUUID(), user_id, bank_name, method_type, account_type, routing_number_encrypted, account_number_encrypted, is_default, created_on: now, updated_on: now, }) .returning(); } export async function getPaymentMethodsByUser(user_id: string) { return await db.select() .from(payment_methods) .where(eq(payment_methods.user_id, user_id)); } export async function getDefaultPaymentMethod(user_id: string) { const result = await db.select() .from(payment_methods) .where(and( eq(payment_methods.user_id, user_id), eq(payment_methods.is_default, true), )); return result[0] || null; } export async function updatePaymentMethod( id: string, updates: Partial>, ) { const now = Date.now(); return await db.update(payment_methods) .set({ ...updates, updated_on: now }) .where(eq(payment_methods.id, id)) .returning(); } export async function deletePaymentMethod(id: string) { return await db.delete(payment_methods) .where(eq(payment_methods.id, id)) .returning(); } export async function getPaymentTransactionsByUser( user_id: string, options: { limit?: number } = {}, ) { const query = db.select() .from(payment_transactions) .innerJoin(leases, eq(payment_transactions.lease_id, leases.id)) .where(eq(leases.tenant_id, user_id)) .orderBy(payment_transactions.payment_date); if (options.limit) { return await query.limit(options.limit); } return await query; } // Payment Transactions CRUD export async function createPaymentTransaction( lease_id: string, amount: number, payment_date: number, payment_method: string, transaction_status: string = "pending", stripe_payment_id?: string, ) { const now = Date.now(); return await db.insert(payment_transactions) .values({ id: crypto.randomUUID(), lease_id, amount, payment_date, payment_method, transaction_status, stripe_payment_id: stripe_payment_id || null, created_on: now, }) .returning(); } export async function getTransactionsByLease(lease_id: string) { return await db.select() .from(payment_transactions) .where(eq(payment_transactions.lease_id, lease_id)); } export async function getPaymentTransactionsByOwner( owner_id: string, options: { limit?: number } = {}, ) { const query = db.select({ id: payment_transactions.id, lease_id: payment_transactions.lease_id, amount: payment_transactions.amount, payment_date: payment_transactions.payment_date, payment_method: payment_transactions.payment_method, transaction_status: payment_transactions.transaction_status, stripe_payment_id: payment_transactions.stripe_payment_id, created_on: payment_transactions.created_on, }) .from(payment_transactions) .innerJoin(leases, eq(payment_transactions.lease_id, leases.id)) .where(eq(leases.owner_id, owner_id)) .orderBy(payment_transactions.payment_date); if (options.limit) { return await query.limit(options.limit); } return await query; } export async function updateTransactionStatus(id: string, status: string) { return await db.update(payment_transactions) .set({ transaction_status: status }) .where(eq(payment_transactions.id, id)) .returning(); } // Rent Schedules CRUD export async function createRentSchedule( property_id: string, rent_amount: number, due_day: number, late_fee_amount: number = 0, grace_period_days: number = 0, payment_methods_allowed: string[] = ["card", "ach"], ) { const now = Date.now(); return await db.insert(rent_schedules) .values({ id: crypto.randomUUID(), property_id, rent_amount, due_day, late_fee_amount, grace_period_days, payment_methods_allowed: JSON.stringify(payment_methods_allowed), created_on: now, updated_on: now, }) .returning(); } export async function getRentScheduleByProperty(property_id: string) { const result = await db.select() .from(rent_schedules) .where(eq(rent_schedules.property_id, property_id)); return result[0] || null; } export async function updateRentSchedule( id: string, updates: Partial>, ) { const now = Date.now(); return await db.update(rent_schedules) .set({ ...updates, updated_on: now }) .where(eq(rent_schedules.id, id)) .returning(); } export async function deleteRentSchedule(id: string) { return await db.delete(rent_schedules) .where(eq(rent_schedules.id, id)) .returning(); } // Payment Scheduling export async function schedulePayment( lease_id: string, _user_id: string, amount: number, payment_date: number, // Unix timestamp payment_method: string, // e.g., 'card', 'ach' stripe_payment_id: string | null = null, ) { const now = Date.now(); return await db.insert(payment_transactions) .values({ id: crypto.randomUUID(), lease_id, amount, payment_date, payment_method, transaction_status: "scheduled", stripe_payment_id, created_on: now, }) .returning(); } // Auto-Payment Setup // Note: This function updates the lease record to link a default payment method for auto-debit. export async function setAutoPayment( lease_id: string, user_id: string, payment_method_id: string, ) { // Ensure the user owns the payment method and is the tenant on the lease const lease = await db.select().from(leases) .where(and(eq(leases.id, lease_id), eq(leases.tenant_id, user_id))); if (!lease.length) { throw new Error("Lease not found or user is not the tenant."); } const paymentMethod = await db.select().from(payment_methods) .where( and( eq(payment_methods.id, payment_method_id), eq(payment_methods.user_id, user_id), ), ); if (!paymentMethod.length) { throw new Error("Payment method not found or unauthorized."); } const now = Date.now(); return await db.update(leases) .set({ default_payment_method_id: payment_method_id, updated_on: now }) .where(eq(leases.id, lease_id)) .returning(); } export async function cancelAutoPayment( lease_id: string, user_id: string, ) { // Ensure the user is the tenant on the lease const lease = await db.select().from(leases) .where(and(eq(leases.id, lease_id), eq(leases.tenant_id, user_id))); if (!lease.length) { throw new Error("Lease not found or user is not the tenant."); } const now = Date.now(); return await db.update(leases) .set({ default_payment_method_id: null, updated_on: now }) .where(eq(leases.id, lease_id)) .returning(); } export async function getTransactionByStripeId(stripeId: string) { const result = await db.select() .from(payment_transactions) .where(eq(payment_transactions.stripe_payment_id, stripeId)); return result[0] || null; }