401 lines
11 KiB
TypeScript
401 lines
11 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();
|
|
}
|
|
|
|
export async function getTransactionByStripeId(stripeId: string) {
|
|
if (isE2EEnabled()) {
|
|
const transactions = getE2EPaymentTransactionsByOwner(""); // Get all transactions and filter
|
|
return transactions.find((tx) => tx.stripe_payment_id === stripeId) || null;
|
|
}
|
|
const result = await db.select()
|
|
.from(payment_transactions)
|
|
.where(eq(payment_transactions.stripe_payment_id, stripeId));
|
|
return result[0] || null;
|
|
}
|