fix(types fixed of status, method)

This commit is contained in:
2025-10-10 21:44:51 +05:30
parent da71a0ffc7
commit 6e981a71dc
8 changed files with 159 additions and 18 deletions

View File

@@ -8,6 +8,9 @@ import {
NewTransactionPayload, NewTransactionPayload,
newTransactionPayloadSchema, newTransactionPayloadSchema,
paymentMethodOptions, paymentMethodOptions,
PaymentStatus,
paymentStatusOptions,
claimStatusOptions,
} from "@repo/db/types"; } from "@repo/db/types";
import { prisma } from "@repo/db/client"; import { prisma } from "@repo/db/client";
import { PaymentStatusSchema } from "@repo/db/types"; import { PaymentStatusSchema } from "@repo/db/types";
@@ -283,7 +286,7 @@ router.put(
serviceLineId: line.id, serviceLineId: line.id,
paidAmount: line.totalDue.toNumber(), paidAmount: line.totalDue.toNumber(),
adjustedAmount: 0, adjustedAmount: 0,
method: paymentMethodOptions[1], method: paymentMethodOptions.CHECK,
receivedDate: new Date(), receivedDate: new Date(),
notes: "Full claim payment", notes: "Full claim payment",
})); }));
@@ -338,7 +341,7 @@ router.put(
serviceLineId: line.id, serviceLineId: line.id,
paidAmount: line.totalPaid.negated().toNumber(), // negative to undo paidAmount: line.totalPaid.negated().toNumber(), // negative to undo
adjustedAmount: line.totalAdjusted.negated().toNumber(), adjustedAmount: line.totalAdjusted.negated().toNumber(),
method: paymentMethodOptions[4], method: paymentMethodOptions.OTHER,
receivedDate: new Date(), receivedDate: new Date(),
notes: "Reverted full claim", notes: "Reverted full claim",
})); }));
@@ -374,18 +377,51 @@ router.patch(
const paymentId = parseIntOrError(req.params.id, "Payment ID"); 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({ // Load existing payment
const existingPayment = await storage.getPayment(paymentId);
if (!existingPayment) {
return res.status(404).json({ message: "Payment not found" });
}
// 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 }, where: { id: paymentId },
data: { status, updatedById: userId }, data: { status, updatedById: userId },
}); }),
prisma.claim.update({
where: { id: existingPayment.claimId },
data: { status: claimStatusOptions.VOID },
}),
]);
res.json(updatedPayment); 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) { } catch (err: unknown) {
const message = const message =
err instanceof Error ? err.message : "Failed to update payment status"; err instanceof Error ? err.message : "Failed to update payment status";
res.status(500).json({ message }); return res.status(500).json({ message });
} }
} }
); );

View File

@@ -1,5 +1,6 @@
import { import {
Claim, Claim,
ClaimStatus,
ClaimWithServiceLines, ClaimWithServiceLines,
InsertClaim, InsertClaim,
UpdateClaim, UpdateClaim,
@@ -13,13 +14,13 @@ export interface IStorage {
limit: number, limit: number,
offset: number offset: number
): Promise<ClaimWithServiceLines[]>; ): Promise<ClaimWithServiceLines[]>;
getTotalClaimCountByPatient(patientId: number): Promise<number>; getTotalClaimCountByPatient(patientId: number): Promise<number>;
getClaimsByAppointmentId(appointmentId: number): Promise<Claim[]>; getClaimsByAppointmentId(appointmentId: number): Promise<Claim[]>;
getRecentClaims(limit: number, offset: number): Promise<Claim[]>; getRecentClaims(limit: number, offset: number): Promise<Claim[]>;
getTotalClaimCount(): Promise<number>; getTotalClaimCount(): Promise<number>;
createClaim(claim: InsertClaim): Promise<Claim>; createClaim(claim: InsertClaim): Promise<Claim>;
updateClaim(id: number, updates: UpdateClaim): Promise<Claim>; updateClaim(id: number, updates: UpdateClaim): Promise<Claim>;
updateClaimStatus(id: number, status: ClaimStatus): Promise<Claim>;
deleteClaim(id: number): Promise<void>; deleteClaim(id: number): Promise<void>;
} }
@@ -88,6 +89,18 @@ export const claimsStorage: IStorage = {
} }
}, },
async updateClaimStatus(id: number, status: ClaimStatus): Promise<Claim> {
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<void> { async deleteClaim(id: number): Promise<void> {
try { try {
await db.claim.delete({ where: { id } }); await db.claim.delete({ where: { id } });

View File

@@ -11,6 +11,11 @@ export interface IStorage {
getPayment(id: number): Promise<Payment | undefined>; getPayment(id: number): Promise<Payment | undefined>;
createPayment(data: InsertPayment): Promise<Payment>; createPayment(data: InsertPayment): Promise<Payment>;
updatePayment(id: number, updates: UpdatePayment): Promise<Payment>; updatePayment(id: number, updates: UpdatePayment): Promise<Payment>;
updatePaymentStatus(
id: number,
updates: UpdatePayment,
updatedById?: number
): Promise<Payment>;
deletePayment(id: number, userId: number): Promise<void>; deletePayment(id: number, userId: number): Promise<void>;
getPaymentById(id: number): Promise<PaymentWithExtras | null>; getPaymentById(id: number): Promise<PaymentWithExtras | null>;
getRecentPaymentsByPatientId( getRecentPaymentsByPatientId(
@@ -51,6 +56,25 @@ export const paymentsStorage: IStorage = {
}); });
}, },
async updatePaymentStatus(
id: number,
updates: UpdatePayment,
updatedById?: number
): Promise<Payment> {
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<void> { async deletePayment(id: number, userId: number): Promise<void> {
const existing = await db.payment.findFirst({ where: { id, userId } }); const existing = await db.payment.findFirst({ where: { id, userId } });
if (!existing) { if (!existing) {

View File

@@ -14,11 +14,12 @@ import {
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { import {
PaymentStatus, PaymentStatus,
paymentStatusOptions,
PaymentMethod, PaymentMethod,
paymentMethodOptions, paymentMethodOptions,
PaymentWithExtras, PaymentWithExtras,
NewTransactionPayload, NewTransactionPayload,
paymentStatusArray,
paymentMethodArray,
} from "@repo/db/types"; } from "@repo/db/types";
import { import {
Select, Select,
@@ -90,7 +91,7 @@ export default function PaymentEditModal({
transactionId: "", transactionId: "",
paidAmount: 0, paidAmount: 0,
adjustedAmount: 0, adjustedAmount: 0,
method: paymentMethodOptions[1] as PaymentMethod, method: paymentMethodOptions.CHECK as PaymentMethod,
receivedDate: formatLocalDate(new Date()), receivedDate: formatLocalDate(new Date()),
payerName: "", payerName: "",
notes: "", notes: "",
@@ -298,7 +299,7 @@ export default function PaymentEditModal({
transactionId: "", transactionId: "",
paidAmount: Number(line.totalDue) > 0 ? Number(line.totalDue) : 0, paidAmount: Number(line.totalDue) > 0 ? Number(line.totalDue) : 0,
adjustedAmount: 0, adjustedAmount: 0,
method: paymentMethodOptions[1] as PaymentMethod, method: paymentMethodOptions.CHECK as PaymentMethod,
receivedDate: formatLocalDate(new Date()), receivedDate: formatLocalDate(new Date()),
payerName: "", payerName: "",
notes: "", notes: "",
@@ -431,7 +432,7 @@ export default function PaymentEditModal({
serviceLineId: line.id, serviceLineId: line.id,
paidAmount: dueAmount, paidAmount: dueAmount,
adjustedAmount: 0, adjustedAmount: 0,
method: paymentMethodOptions[1] as PaymentMethod, // Maybe make dynamic later method: paymentMethodOptions.CHECK as PaymentMethod, // Maybe make dynamic later
receivedDate: new Date(), receivedDate: new Date(),
}, },
], ],
@@ -565,7 +566,7 @@ export default function PaymentEditModal({
<SelectValue /> <SelectValue />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{paymentStatusOptions.map((status) => ( {paymentStatusArray.map((status) => (
<SelectItem key={status} value={status}> <SelectItem key={status} value={status}>
{status} {status}
</SelectItem> </SelectItem>
@@ -727,7 +728,7 @@ export default function PaymentEditModal({
<SelectValue placeholder="Select a payment method" /> <SelectValue placeholder="Select a payment method" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{paymentMethodOptions.map((methodOption) => ( {paymentMethodArray.map((methodOption) => (
<SelectItem <SelectItem
key={methodOption} key={methodOption}
value={methodOption} value={methodOption}

View File

@@ -132,6 +132,7 @@ enum ClaimStatus {
APPROVED APPROVED
CANCELLED CANCELLED
REVIEW REVIEW
VOID
} }
model ServiceLine { model ServiceLine {

View File

@@ -5,6 +5,7 @@ import {
import { z } from "zod"; import { z } from "zod";
import { Decimal } from "decimal.js"; import { Decimal } from "decimal.js";
import { Staff } from "@repo/db/types"; import { Staff } from "@repo/db/types";
import { makeEnumOptions } from "../utils";
export const insertClaimSchema = ( export const insertClaimSchema = (
ClaimUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any> ClaimUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
@@ -36,6 +37,14 @@ export const ExtendedClaimSchema = (
export type Claim = z.infer<typeof ClaimUncheckedCreateInputObjectSchema>; export type Claim = z.infer<typeof ClaimUncheckedCreateInputObjectSchema>;
export type ClaimStatus = z.infer<typeof ClaimStatusSchema>; export type ClaimStatus = z.infer<typeof ClaimStatusSchema>;
export const claimStatusOptions =
makeEnumOptions<
typeof ClaimStatusSchema extends z.ZodTypeAny
? z.infer<typeof ClaimStatusSchema>
: string
>(ClaimStatusSchema);
export type ClaimStatusOptions =
(typeof claimStatusOptions)[keyof typeof claimStatusOptions];
export type ClaimFileMeta = { export type ClaimFileMeta = {
id?: number; id?: number;

View File

@@ -6,6 +6,7 @@ import {
} from "@repo/db/usedSchemas"; } from "@repo/db/usedSchemas";
import { Prisma } from "@repo/db/generated/prisma"; import { Prisma } from "@repo/db/generated/prisma";
import { z } from "zod"; import { z } from "zod";
import { makeEnumOptions } from "../utils";
// ========== BASIC TYPES ========== // ========== BASIC TYPES ==========
@@ -31,8 +32,29 @@ export type PaymentStatus = z.infer<typeof PaymentStatusSchema>;
export type PaymentMethod = z.infer<typeof PaymentMethodSchema>; export type PaymentMethod = z.infer<typeof PaymentMethodSchema>;
// ✅ Runtime arrays (used in code logic / map / select options) // ✅ Runtime arrays (used in code logic / map / select options)
export const paymentStatusOptions = PaymentStatusSchema.options; export const paymentStatusOptions =
export const paymentMethodOptions = PaymentMethodSchema.options; makeEnumOptions<
typeof PaymentStatusSchema extends z.ZodTypeAny
? z.infer<typeof PaymentStatusSchema>
: 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<typeof PaymentMethodSchema>
: string
>(PaymentMethodSchema);
export type PaymentMethodOptions =
(typeof paymentMethodOptions)[keyof typeof paymentMethodOptions];
export const paymentMethodArray = Object.values(
paymentMethodOptions
) as PaymentMethodOptions[];
// ========== INPUT TYPES ========== // ========== INPUT TYPES ==========

View File

@@ -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<T extends string | number>(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<T extends string | number>(schema: any) {
const values = extractEnumValues<T>(schema);
const map = {} as Record<string, T>;
values.forEach((v) => {
map[String(v)] = v;
});
return map as { [K in T & (string | number)]: K };
}