route added
This commit is contained in:
@@ -3,7 +3,14 @@ import { Request, Response } from "express";
|
|||||||
import { storage } from "../storage";
|
import { storage } from "../storage";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { ZodError } from "zod";
|
import { ZodError } from "zod";
|
||||||
import { insertPaymentSchema, updatePaymentSchema } from "@repo/db/types";
|
import {
|
||||||
|
insertPaymentSchema,
|
||||||
|
NewTransactionPayload,
|
||||||
|
newTransactionPayloadSchema,
|
||||||
|
updatePaymentSchema,
|
||||||
|
} from "@repo/db/types";
|
||||||
|
import Decimal from "decimal.js";
|
||||||
|
import { prisma } from "@repo/db/client";
|
||||||
|
|
||||||
const paymentFilterSchema = z.object({
|
const paymentFilterSchema = z.object({
|
||||||
from: z.string().datetime(),
|
from: z.string().datetime(),
|
||||||
@@ -146,8 +153,7 @@ router.get("/:id", async (req: Request, res: Response): Promise<any> => {
|
|||||||
const id = parseIntOrError(req.params.id, "Payment ID");
|
const id = parseIntOrError(req.params.id, "Payment ID");
|
||||||
|
|
||||||
const payment = await storage.getPaymentById(userId, id);
|
const payment = await storage.getPaymentById(userId, id);
|
||||||
if (!payment)
|
if (!payment) return res.status(404).json({ message: "Payment not found" });
|
||||||
return res.status(404).json({ message: "Payment not found" });
|
|
||||||
|
|
||||||
res.status(200).json(payment);
|
res.status(200).json(payment);
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
@@ -157,7 +163,6 @@ router.get("/:id", async (req: Request, res: Response): Promise<any> => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
// POST /api/payments/:claimId
|
// POST /api/payments/:claimId
|
||||||
router.post("/:claimId", async (req: Request, res: Response): Promise<any> => {
|
router.post("/:claimId", async (req: Request, res: Response): Promise<any> => {
|
||||||
try {
|
try {
|
||||||
@@ -194,9 +199,11 @@ router.put("/:id", async (req: Request, res: Response): Promise<any> => {
|
|||||||
const userId = req.user?.id;
|
const userId = req.user?.id;
|
||||||
if (!userId) return res.status(401).json({ message: "Unauthorized" });
|
if (!userId) return res.status(401).json({ message: "Unauthorized" });
|
||||||
|
|
||||||
const id = parseIntOrError(req.params.id, "Payment ID");
|
const paymentId = parseIntOrError(req.params.id, "Payment ID");
|
||||||
|
|
||||||
const validated = updatePaymentSchema.safeParse(req.body);
|
const validated = newTransactionPayloadSchema.safeParse(
|
||||||
|
req.body.data as NewTransactionPayload
|
||||||
|
);
|
||||||
if (!validated.success) {
|
if (!validated.success) {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
message: "Validation failed",
|
message: "Validation failed",
|
||||||
@@ -204,9 +211,97 @@ router.put("/:id", async (req: Request, res: Response): Promise<any> => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const updated = await storage.updatePayment(id, validated.data, userId);
|
const { status, serviceLineTransactions } = validated.data;
|
||||||
|
|
||||||
res.status(200).json(updated);
|
// Wrap everything in a transaction
|
||||||
|
const result = await prisma.$transaction(async (tx) => {
|
||||||
|
// 1. Create all new service line transactions
|
||||||
|
for (const txn of serviceLineTransactions) {
|
||||||
|
await tx.serviceLineTransaction.create({
|
||||||
|
data: {
|
||||||
|
paymentId,
|
||||||
|
serviceLineId: txn.serviceLineId,
|
||||||
|
transactionId: txn.transactionId,
|
||||||
|
paidAmount: new Decimal(txn.paidAmount || 0),
|
||||||
|
adjustedAmount: new Decimal(txn.adjustedAmount || 0),
|
||||||
|
method: txn.method,
|
||||||
|
receivedDate: txn.receivedDate,
|
||||||
|
payerName: txn.payerName,
|
||||||
|
notes: txn.notes,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2. Recalculate that specific service line's totals
|
||||||
|
const aggLine = await tx.serviceLineTransaction.aggregate({
|
||||||
|
_sum: {
|
||||||
|
paidAmount: true,
|
||||||
|
adjustedAmount: true,
|
||||||
|
},
|
||||||
|
where: { serviceLineId: txn.serviceLineId },
|
||||||
|
});
|
||||||
|
|
||||||
|
const serviceLine = await tx.serviceLine.findUniqueOrThrow({
|
||||||
|
where: { id: txn.serviceLineId },
|
||||||
|
select: { totalBilled: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
const totalPaid = aggLine._sum.paidAmount || new Decimal(0);
|
||||||
|
const totalAdjusted = aggLine._sum.adjustedAmount || new Decimal(0);
|
||||||
|
const totalDue = serviceLine.totalBilled
|
||||||
|
.minus(totalPaid)
|
||||||
|
.minus(totalAdjusted);
|
||||||
|
|
||||||
|
await tx.serviceLine.update({
|
||||||
|
where: { id: txn.serviceLineId },
|
||||||
|
data: {
|
||||||
|
totalPaid,
|
||||||
|
totalAdjusted,
|
||||||
|
totalDue,
|
||||||
|
status:
|
||||||
|
totalDue.lte(0) && totalPaid.gt(0)
|
||||||
|
? "PAID"
|
||||||
|
: totalPaid.gt(0)
|
||||||
|
? "PARTIALLY_PAID"
|
||||||
|
: "UNPAID",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Recalculate payment totals
|
||||||
|
const aggPayment = await tx.serviceLineTransaction.aggregate({
|
||||||
|
_sum: {
|
||||||
|
paidAmount: true,
|
||||||
|
adjustedAmount: true,
|
||||||
|
},
|
||||||
|
where: { paymentId },
|
||||||
|
});
|
||||||
|
|
||||||
|
const payment = await tx.payment.findUniqueOrThrow({
|
||||||
|
where: { id: paymentId },
|
||||||
|
select: { totalBilled: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
const totalPaid = aggPayment._sum.paidAmount || new Decimal(0);
|
||||||
|
const totalAdjusted = aggPayment._sum.adjustedAmount || new Decimal(0);
|
||||||
|
const totalDue = payment.totalBilled
|
||||||
|
.minus(totalPaid)
|
||||||
|
.minus(totalAdjusted);
|
||||||
|
|
||||||
|
const updatedPayment = await tx.payment.update({
|
||||||
|
where: { id: paymentId },
|
||||||
|
data: {
|
||||||
|
totalPaid,
|
||||||
|
totalAdjusted,
|
||||||
|
totalDue,
|
||||||
|
status,
|
||||||
|
updatedById: userId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return updatedPayment;
|
||||||
|
});
|
||||||
|
|
||||||
|
res.status(200).json(result);
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
const message =
|
const message =
|
||||||
err instanceof Error ? err.message : "Failed to update payment";
|
err instanceof Error ? err.message : "Failed to update payment";
|
||||||
|
|||||||
@@ -120,7 +120,7 @@ export default function PaymentsRecentTable({
|
|||||||
mutationFn: async (data: NewTransactionPayload) => {
|
mutationFn: async (data: NewTransactionPayload) => {
|
||||||
const response = await apiRequest(
|
const response = await apiRequest(
|
||||||
"PUT",
|
"PUT",
|
||||||
`/api/claims/${data.paymentId}`,
|
`/api/payments/${data.paymentId}`,
|
||||||
{
|
{
|
||||||
data: data,
|
data: data,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -206,6 +206,7 @@ model Payment {
|
|||||||
updatedById Int?
|
updatedById Int?
|
||||||
totalBilled Decimal @db.Decimal(10, 2)
|
totalBilled Decimal @db.Decimal(10, 2)
|
||||||
totalPaid Decimal @default(0.00) @db.Decimal(10, 2)
|
totalPaid Decimal @default(0.00) @db.Decimal(10, 2)
|
||||||
|
totalAdjusted Decimal @default(0.00) @db.Decimal(10, 2)
|
||||||
totalDue Decimal @db.Decimal(10, 2)
|
totalDue Decimal @db.Decimal(10, 2)
|
||||||
status PaymentStatus @default(PENDING)
|
status PaymentStatus @default(PENDING)
|
||||||
notes String?
|
notes String?
|
||||||
|
|||||||
@@ -60,14 +60,6 @@ export const updatePaymentSchema = (
|
|||||||
.partial();
|
.partial();
|
||||||
export type UpdatePayment = z.infer<typeof updatePaymentSchema>;
|
export type UpdatePayment = z.infer<typeof updatePaymentSchema>;
|
||||||
|
|
||||||
// Input for updating a payment with new transactions + updated service payments
|
|
||||||
export type UpdatePaymentInput = {
|
|
||||||
newTransactions: ServiceLineTransactionInput[];
|
|
||||||
totalPaid: Decimal;
|
|
||||||
totalDue: Decimal;
|
|
||||||
status: PaymentStatus;
|
|
||||||
};
|
|
||||||
|
|
||||||
// ========== EXTENDED TYPES ==========
|
// ========== EXTENDED TYPES ==========
|
||||||
|
|
||||||
// Payment with full nested data
|
// Payment with full nested data
|
||||||
@@ -91,18 +83,21 @@ export type PaymentWithExtras = Prisma.PaymentGetPayload<{
|
|||||||
paymentMethod: string;
|
paymentMethod: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const newTransactionPayloadSchema = z.object({
|
||||||
|
paymentId: z.number(),
|
||||||
|
status: PaymentStatusSchema,
|
||||||
|
serviceLineTransactions: z.array(
|
||||||
|
z.object({
|
||||||
|
serviceLineId: z.number(),
|
||||||
|
transactionId: z.string().optional(),
|
||||||
|
paidAmount: z.number(),
|
||||||
|
adjustedAmount: z.number().optional(),
|
||||||
|
method: PaymentMethodSchema,
|
||||||
|
receivedDate: z.coerce.date(),
|
||||||
|
payerName: z.string().optional(),
|
||||||
|
notes: z.string().optional(),
|
||||||
|
})
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
export type NewTransactionPayload = {
|
export type NewTransactionPayload = z.infer<typeof newTransactionPayloadSchema>;
|
||||||
paymentId: number;
|
|
||||||
status: PaymentStatus;
|
|
||||||
serviceLineTransactions: {
|
|
||||||
serviceLineId: number;
|
|
||||||
transactionId?: string;
|
|
||||||
paidAmount: number;
|
|
||||||
adjustedAmount?: number;
|
|
||||||
method: PaymentMethod;
|
|
||||||
receivedDate: Date;
|
|
||||||
payerName?: string;
|
|
||||||
notes?: string;
|
|
||||||
}[];
|
|
||||||
};
|
|
||||||
|
|||||||
Reference in New Issue
Block a user