feat(02-05): Payment model with FIFO allocation and void

- Add PaymentMethod (CASH, BANK_TRANSFER) and PaymentStatus (COMPLETED, VOIDED) enums
- Add Payment model with idempotency key, journal entry link, void fields
- Add PaymentAllocation model for FIFO invoice allocation tracking
- Add Payment/PaymentAllocation relations to Subscriber, Invoice, User
- Update TENANT_SCOPED_MODELS with "payment" and "paymentAllocation"
- Add payment/paymentAllocation query extensions in withTenantContext()
- Implement recordPayment() with FIFO allocation, overpayment credit balance
- Implement voidPayment() with reversing journal entries
- Implement getSubscriberPaymentHistory() with pagination
- Run migration: 20260304154606_add_payment_model
This commit is contained in:
kevin-asprec
2026-03-04 23:47:58 +08:00
parent df4a467a38
commit 6b91e67bdc
4 changed files with 750 additions and 2 deletions

View File

@@ -30,7 +30,7 @@ import { prisma } from "@/lib/prisma";
* Extend this list as new models are added in later phases:
* e.g., "subscriber", "invoice", "servicePlan", "payment"
*/
export const TENANT_SCOPED_MODELS = ["user", "account", "accountingPeriod", "subscriber", "servicePlan", "tenantSettings", "journalEntry", "journalEntryLine", "invoice", "invoiceLine"] as const;
export const TENANT_SCOPED_MODELS = ["user", "account", "accountingPeriod", "subscriber", "servicePlan", "tenantSettings", "journalEntry", "journalEntryLine", "invoice", "invoiceLine", "payment", "paymentAllocation"] as const;
export type TenantScopedModel = (typeof TENANT_SCOPED_MODELS)[number];
@@ -848,6 +848,94 @@ export function withTenantContext(tenantId: string) {
return query(args);
},
},
payment: {
async findMany({ args, query }) {
args.where = { ...args.where, tenantId };
return query(args);
},
async findFirst({ args, query }) {
args.where = { ...args.where, tenantId };
return query(args);
},
async findFirstOrThrow({ args, query }) {
args.where = { ...args.where, tenantId };
return query(args);
},
async findUnique({ args, query }) {
if (args.where && "id" in args.where && !("tenantId" in (args.where as object))) {
return prisma.payment.findFirst({
...args,
where: { ...args.where, tenantId },
});
}
return query(args);
},
async create({ args, query }) {
args.data = { ...args.data, tenantId } as typeof args.data;
return query(args);
},
async update({ args, query }) {
args.where = { ...args.where, tenantId };
return query(args);
},
async updateMany({ args, query }) {
args.where = { ...args.where, tenantId };
return query(args);
},
async count({ args, query }) {
args.where = { ...args.where, tenantId };
return query(args);
},
async aggregate({ args, query }) {
args.where = { ...args.where, tenantId };
return query(args);
},
},
paymentAllocation: {
async findMany({ args, query }) {
args.where = { ...args.where, tenantId };
return query(args);
},
async findFirst({ args, query }) {
args.where = { ...args.where, tenantId };
return query(args);
},
async create({ args, query }) {
args.data = { ...args.data, tenantId } as typeof args.data;
return query(args);
},
async createMany({ args, query }) {
if (Array.isArray(args.data)) {
args.data = args.data.map((item) => ({ ...item, tenantId })) as typeof args.data;
} else {
args.data = { ...args.data, tenantId } as typeof args.data;
}
return query(args);
},
async deleteMany({ args, query }) {
args.where = { ...args.where, tenantId };
return query(args);
},
async count({ args, query }) {
args.where = { ...args.where, tenantId };
return query(args);
},
},
},
});
}