diff --git a/apps/Backend/src/routes/payments.ts b/apps/Backend/src/routes/payments.ts index 4683aa1..1a7bc08 100644 --- a/apps/Backend/src/routes/payments.ts +++ b/apps/Backend/src/routes/payments.ts @@ -8,6 +8,9 @@ import { NewTransactionPayload, newTransactionPayloadSchema, paymentMethodOptions, + PaymentStatus, + paymentStatusOptions, + claimStatusOptions, } from "@repo/db/types"; import { prisma } from "@repo/db/client"; import { PaymentStatusSchema } from "@repo/db/types"; @@ -283,7 +286,7 @@ router.put( serviceLineId: line.id, paidAmount: line.totalDue.toNumber(), adjustedAmount: 0, - method: paymentMethodOptions[1], + method: paymentMethodOptions.CHECK, receivedDate: new Date(), notes: "Full claim payment", })); @@ -338,7 +341,7 @@ router.put( serviceLineId: line.id, paidAmount: line.totalPaid.negated().toNumber(), // negative to undo adjustedAmount: line.totalAdjusted.negated().toNumber(), - method: paymentMethodOptions[4], + method: paymentMethodOptions.OTHER, receivedDate: new Date(), notes: "Reverted full claim", })); @@ -374,18 +377,51 @@ router.patch( const paymentId = parseIntOrError(req.params.id, "Payment ID"); - const status = PaymentStatusSchema.parse(req.body.data.status); + // Parse & coerce to PaymentStatus enum + const rawStatus = PaymentStatusSchema.parse(req.body.data.status); + if ( + !Object.values(paymentStatusOptions).includes( + rawStatus as PaymentStatus + ) + ) { + return res.status(400).json({ message: "Invalid payment status" }); + } + const status = rawStatus as PaymentStatus; - const updatedPayment = await prisma.payment.update({ - where: { id: paymentId }, - data: { status, updatedById: userId }, - }); + // Load existing payment + const existingPayment = await storage.getPayment(paymentId); + if (!existingPayment) { + return res.status(404).json({ message: "Payment not found" }); + } - res.json(updatedPayment); + // If changing to VOID and linked to a claim -> update both atomically + if (status === paymentStatusOptions.VOID && existingPayment.claimId) { + const [updatedPayment, updatedClaim] = await prisma.$transaction([ + prisma.payment.update({ + where: { id: paymentId }, + data: { status, updatedById: userId }, + }), + prisma.claim.update({ + where: { id: existingPayment.claimId }, + data: { status: claimStatusOptions.VOID }, + }), + ]); + + return res.json(updatedPayment); + } + + // Otherwise just update payment (use storage helper) + const updatedPayment = await storage.updatePaymentStatus( + paymentId, + { status } as any, + userId + ); + + return res.json(updatedPayment); } catch (err: unknown) { const message = err instanceof Error ? err.message : "Failed to update payment status"; - res.status(500).json({ message }); + return res.status(500).json({ message }); } } ); diff --git a/apps/Backend/src/storage/claims-storage.ts b/apps/Backend/src/storage/claims-storage.ts index f5a9229..2a0cc29 100644 --- a/apps/Backend/src/storage/claims-storage.ts +++ b/apps/Backend/src/storage/claims-storage.ts @@ -1,5 +1,6 @@ import { Claim, + ClaimStatus, ClaimWithServiceLines, InsertClaim, UpdateClaim, @@ -13,13 +14,13 @@ export interface IStorage { limit: number, offset: number ): Promise; - getTotalClaimCountByPatient(patientId: number): Promise; getClaimsByAppointmentId(appointmentId: number): Promise; getRecentClaims(limit: number, offset: number): Promise; getTotalClaimCount(): Promise; createClaim(claim: InsertClaim): Promise; updateClaim(id: number, updates: UpdateClaim): Promise; + updateClaimStatus(id: number, status: ClaimStatus): Promise; deleteClaim(id: number): Promise; } @@ -88,6 +89,18 @@ export const claimsStorage: IStorage = { } }, + async updateClaimStatus(id: number, status: ClaimStatus): Promise { + const existing = await db.claim.findUnique({ where: { id } }); + if (!existing) { + throw new Error("Claim not found"); + } + + return db.claim.update({ + where: { id }, + data: { status }, + }); + }, + async deleteClaim(id: number): Promise { try { await db.claim.delete({ where: { id } }); diff --git a/apps/Backend/src/storage/payments-storage.ts b/apps/Backend/src/storage/payments-storage.ts index d6e61ff..e0ffd1b 100644 --- a/apps/Backend/src/storage/payments-storage.ts +++ b/apps/Backend/src/storage/payments-storage.ts @@ -11,6 +11,11 @@ export interface IStorage { getPayment(id: number): Promise; createPayment(data: InsertPayment): Promise; updatePayment(id: number, updates: UpdatePayment): Promise; + updatePaymentStatus( + id: number, + updates: UpdatePayment, + updatedById?: number + ): Promise; deletePayment(id: number, userId: number): Promise; getPaymentById(id: number): Promise; getRecentPaymentsByPatientId( @@ -51,6 +56,25 @@ export const paymentsStorage: IStorage = { }); }, + async updatePaymentStatus( + id: number, + updates: UpdatePayment, + updatedById?: number + ): Promise { + const existing = await db.payment.findFirst({ where: { id } }); + if (!existing) { + throw new Error("Payment not found"); + } + + const data: any = { ...updates }; + if (typeof updatedById === "number") data.updatedById = updatedById; + + return db.payment.update({ + where: { id }, + data, + }); + }, + async deletePayment(id: number, userId: number): Promise { const existing = await db.payment.findFirst({ where: { id, userId } }); if (!existing) { diff --git a/apps/Frontend/src/components/payments/payment-edit-modal.tsx b/apps/Frontend/src/components/payments/payment-edit-modal.tsx index da29e1f..e707636 100644 --- a/apps/Frontend/src/components/payments/payment-edit-modal.tsx +++ b/apps/Frontend/src/components/payments/payment-edit-modal.tsx @@ -14,11 +14,12 @@ import { import React, { useEffect, useState } from "react"; import { PaymentStatus, - paymentStatusOptions, PaymentMethod, paymentMethodOptions, PaymentWithExtras, NewTransactionPayload, + paymentStatusArray, + paymentMethodArray, } from "@repo/db/types"; import { Select, @@ -90,7 +91,7 @@ export default function PaymentEditModal({ transactionId: "", paidAmount: 0, adjustedAmount: 0, - method: paymentMethodOptions[1] as PaymentMethod, + method: paymentMethodOptions.CHECK as PaymentMethod, receivedDate: formatLocalDate(new Date()), payerName: "", notes: "", @@ -298,7 +299,7 @@ export default function PaymentEditModal({ transactionId: "", paidAmount: Number(line.totalDue) > 0 ? Number(line.totalDue) : 0, adjustedAmount: 0, - method: paymentMethodOptions[1] as PaymentMethod, + method: paymentMethodOptions.CHECK as PaymentMethod, receivedDate: formatLocalDate(new Date()), payerName: "", notes: "", @@ -431,7 +432,7 @@ export default function PaymentEditModal({ serviceLineId: line.id, paidAmount: dueAmount, adjustedAmount: 0, - method: paymentMethodOptions[1] as PaymentMethod, // Maybe make dynamic later + method: paymentMethodOptions.CHECK as PaymentMethod, // Maybe make dynamic later receivedDate: new Date(), }, ], @@ -565,7 +566,7 @@ export default function PaymentEditModal({ - {paymentStatusOptions.map((status) => ( + {paymentStatusArray.map((status) => ( {status} @@ -727,7 +728,7 @@ export default function PaymentEditModal({ - {paymentMethodOptions.map((methodOption) => ( + {paymentMethodArray.map((methodOption) => ( @@ -36,6 +37,14 @@ export const ExtendedClaimSchema = ( export type Claim = z.infer; export type ClaimStatus = z.infer; +export const claimStatusOptions = + makeEnumOptions< + typeof ClaimStatusSchema extends z.ZodTypeAny + ? z.infer + : string + >(ClaimStatusSchema); +export type ClaimStatusOptions = + (typeof claimStatusOptions)[keyof typeof claimStatusOptions]; export type ClaimFileMeta = { id?: number; diff --git a/packages/db/types/payment-types.ts b/packages/db/types/payment-types.ts index a7db811..af4b217 100644 --- a/packages/db/types/payment-types.ts +++ b/packages/db/types/payment-types.ts @@ -6,6 +6,7 @@ import { } from "@repo/db/usedSchemas"; import { Prisma } from "@repo/db/generated/prisma"; import { z } from "zod"; +import { makeEnumOptions } from "../utils"; // ========== BASIC TYPES ========== @@ -31,8 +32,29 @@ export type PaymentStatus = z.infer; export type PaymentMethod = z.infer; // ✅ Runtime arrays (used in code logic / map / select options) -export const paymentStatusOptions = PaymentStatusSchema.options; -export const paymentMethodOptions = PaymentMethodSchema.options; +export const paymentStatusOptions = + makeEnumOptions< + typeof PaymentStatusSchema extends z.ZodTypeAny + ? z.infer + : string + >(PaymentStatusSchema); +export type PaymentStatusOptions = + (typeof paymentStatusOptions)[keyof typeof paymentStatusOptions]; +export const paymentStatusArray = Object.values( + paymentStatusOptions +) as PaymentStatusOptions[]; + +export const paymentMethodOptions = + makeEnumOptions< + typeof PaymentMethodSchema extends z.ZodTypeAny + ? z.infer + : string + >(PaymentMethodSchema); +export type PaymentMethodOptions = + (typeof paymentMethodOptions)[keyof typeof paymentMethodOptions]; +export const paymentMethodArray = Object.values( + paymentMethodOptions +) as PaymentMethodOptions[]; // ========== INPUT TYPES ========== diff --git a/packages/db/utils/index.ts b/packages/db/utils/index.ts new file mode 100644 index 0000000..cc43c0b --- /dev/null +++ b/packages/db/utils/index.ts @@ -0,0 +1,35 @@ +/** + * Extract enum values from a Zod enum or native enum schema. + * Supports z.enum([...]) and z.nativeEnum(SomeTsEnum). + */ +export function extractEnumValues(schema: any): T[] { + // z.enum([...]) => schema.options exists + if (Array.isArray(schema?.options)) { + return schema.options as T[]; + } + + // z.nativeEnum(SomeEnum) => schema._def?.values may exist or enum is in schema._def?.enum + if (Array.isArray(schema?._def?.values)) { + return schema._def.values as T[]; + } + + if (schema?._def?.enum) { + // enum object -> values + return Object.values(schema._def.enum) as T[]; + } + + throw new Error("Unsupported Zod schema type for enum extraction"); +} + +/** + * Build a runtime map: { VAL: "VAL", ... } with proper typing + * so callers can import paymentStatusOptions.VOID etc. + */ +export function makeEnumOptions(schema: any) { + const values = extractEnumValues(schema); + const map = {} as Record; + values.forEach((v) => { + map[String(v)] = v; + }); + return map as { [K in T & (string | number)]: K }; +}