integratedhub/services/paymentService.ts
2026-01-20 09:02:26 -06:00

390 lines
10 KiB
TypeScript

// 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";
import {
addE2EPaymentMethod,
addE2EPaymentTransaction,
deleteE2EPaymentMethod,
getE2EDefaultPaymentMethod,
getE2EPaymentMethodsByUser,
getE2EPaymentTransactionsByLease,
getE2EPaymentTransactionsByOwner,
getE2EPaymentTransactionsByUser,
isE2EEnabled,
updateE2EPaymentMethod,
} from "./e2eFixtures.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,
) {
if (isE2EEnabled()) {
return [addE2EPaymentMethod(user_id, {
bank_name,
method_type,
account_type,
routing_number_encrypted,
account_number_encrypted,
is_default,
})];
}
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) {
if (isE2EEnabled()) {
return getE2EPaymentMethodsByUser(user_id);
}
return await db.select()
.from(payment_methods)
.where(eq(payment_methods.user_id, user_id));
}
export async function getDefaultPaymentMethod(user_id: string) {
if (isE2EEnabled()) {
return getE2EDefaultPaymentMethod(user_id);
}
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<Omit<PaymentMethod, "id" | "user_id" | "created_on">>,
) {
if (isE2EEnabled()) {
const updated = updateE2EPaymentMethod(id, updates);
return updated ? [updated] : [];
}
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) {
if (isE2EEnabled()) {
const deleted = deleteE2EPaymentMethod(id);
return deleted ? [deleted] : [];
}
return await db.delete(payment_methods)
.where(eq(payment_methods.id, id))
.returning();
}
export async function getPaymentTransactionsByUser(
user_id: string,
options: { limit?: number } = {},
) {
if (isE2EEnabled()) {
const transactions = getE2EPaymentTransactionsByUser(user_id);
return options.limit ? transactions.slice(0, options.limit) : transactions;
}
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,
) {
if (isE2EEnabled()) {
return [addE2EPaymentTransaction(
lease_id,
amount,
payment_date,
payment_method,
transaction_status,
stripe_payment_id,
)];
}
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) {
if (isE2EEnabled()) {
return getE2EPaymentTransactionsByLease(lease_id);
}
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 } = {},
) {
if (isE2EEnabled()) {
const transactions = getE2EPaymentTransactionsByOwner(owner_id);
return options.limit ? transactions.slice(0, options.limit) : transactions;
}
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<Omit<RentSchedule, "id" | "property_id" | "created_on">>,
) {
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,
) {
if (isE2EEnabled()) {
return [addE2EPaymentTransaction(
lease_id,
amount,
payment_date,
payment_method,
"scheduled",
stripe_payment_id || undefined,
)];
}
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();
}