From 5810328711d7ab1ca9e2ae197d546b73213573e9 Mon Sep 17 00:00:00 2001 From: Potenz Date: Thu, 31 Jul 2025 23:11:59 +0530 Subject: [PATCH] payment checkpoint 2 --- apps/Backend/src/routes/index.ts | 23 +- apps/Backend/src/routes/payments.ts | 264 ++++++--- apps/Backend/src/storage/index.ts | 197 ++++--- .../payments/payment-edit-modal.tsx | 141 +++++ .../payments/payment-view-modal.tsx | 35 ++ .../payments/payments-recent-table.tsx | 295 +++++++++++ package-lock.json | 501 ++++++++++++++++-- package.json | 2 + packages/db/prisma/schema.prisma | 34 +- 9 files changed, 1292 insertions(+), 200 deletions(-) create mode 100644 apps/Frontend/src/components/payments/payment-edit-modal.tsx create mode 100644 apps/Frontend/src/components/payments/payment-view-modal.tsx create mode 100644 apps/Frontend/src/components/payments/payments-recent-table.tsx diff --git a/apps/Backend/src/routes/index.ts b/apps/Backend/src/routes/index.ts index 96a8059..2b7937d 100644 --- a/apps/Backend/src/routes/index.ts +++ b/apps/Backend/src/routes/index.ts @@ -1,25 +1,26 @@ import { Router } from 'express'; -import patientRoutes from './patients'; -import appointmentRoutes from './appointments' -import userRoutes from './users' -import staffRoutes from './staffs' +import patientsRoutes from './patients'; +import appointmentsRoutes from './appointments' +import usersRoutes from './users' +import staffsRoutes from './staffs' import pdfExtractionRoutes from './pdfExtraction'; import claimsRoutes from './claims'; import insuranceCredsRoutes from './insuranceCreds'; -import documentRoutes from './documents'; +import documentsRoutes from './documents'; import insuranceEligibilityRoutes from './insuranceEligibility' +import paymentsRoutes from './payments' const router = Router(); -router.use('/patients', patientRoutes); -router.use('/appointments', appointmentRoutes); -router.use('/users', userRoutes); -router.use('/staffs', staffRoutes); +router.use('/patients', patientsRoutes); +router.use('/appointments', appointmentsRoutes); +router.use('/users', usersRoutes); +router.use('/staffs', staffsRoutes); router.use('/pdfExtraction', pdfExtractionRoutes); router.use('/claims', claimsRoutes); router.use('/insuranceCreds', insuranceCredsRoutes); -router.use('/documents', documentRoutes); +router.use('/documents', documentsRoutes); router.use('/insuranceEligibility', insuranceEligibilityRoutes); - +router.use('/payments', paymentsRoutes); export default router; \ No newline at end of file diff --git a/apps/Backend/src/routes/payments.ts b/apps/Backend/src/routes/payments.ts index e81319a..139c189 100644 --- a/apps/Backend/src/routes/payments.ts +++ b/apps/Backend/src/routes/payments.ts @@ -2,11 +2,52 @@ import { Router } from "express"; import { Request, Response } from "express"; import { storage } from "../storage"; import { z } from "zod"; -import { ClaimUncheckedCreateInputObjectSchema } from "@repo/db/usedSchemas"; +import { + ClaimUncheckedCreateInputObjectSchema, + PaymentUncheckedCreateInputObjectSchema, + PaymentTransactionCreateInputObjectSchema, + ServiceLinePaymentCreateInputObjectSchema, +} from "@repo/db/usedSchemas"; +import { Prisma } from "@repo/db/generated/prisma"; +import { ZodError } from "zod"; -const router = Router(); +// Base Payment type +type Payment = z.infer; +type PaymentTransaction = z.infer< + typeof PaymentTransactionCreateInputObjectSchema +>; +type ServiceLinePayment = z.infer< + typeof ServiceLinePaymentCreateInputObjectSchema +>; -// Define Zod schemas +const insertPaymentSchema = ( + PaymentUncheckedCreateInputObjectSchema as unknown as z.ZodObject +).omit({ + id: true, + createdAt: true, + updatedAt: true, +}); +type InsertPayment = z.infer; + +const updatePaymentSchema = ( + PaymentUncheckedCreateInputObjectSchema as unknown as z.ZodObject +) + .omit({ + id: true, + createdAt: true, + }) + .partial(); +type UpdatePayment = z.infer; + +type PaymentWithExtras = Prisma.PaymentGetPayload<{ + include: { + transactions: true; + servicePayments: true; + claim: true; + }; +}>; + +// Claim schema const ClaimSchema = ( ClaimUncheckedCreateInputObjectSchema as unknown as z.ZodObject ).omit({ @@ -29,11 +70,42 @@ const updateClaimSchema = ( type UpdateClaim = z.infer; +const paymentFilterSchema = z.object({ + from: z.string().datetime(), + to: z.string().datetime(), +}); + +function parseIntOrError(input: string | undefined, name: string) { + if (!input) throw new Error(`${name} is required`); + const value = parseInt(input, 10); + if (isNaN(value)) throw new Error(`${name} must be a valid number`); + return value; +} + +export function handleRouteError( + res: Response, + error: unknown, + defaultMsg: string +) { + if (error instanceof ZodError) { + return res.status(400).json({ + message: "Validation error", + errors: error.format(), + }); + } + + const msg = error instanceof Error ? error.message : defaultMsg; + return res.status(500).json({ message: msg }); +} + +const router = Router(); // GET /api/payments/recent -router.get('/recent', async (req, res) => { +router.get("/recent", async (req: Request, res: Response): Promise => { try { - const userId = req.user.id; + const userId = req.user?.id; + if (!userId) return res.status(401).json({ message: "Unauthorized" }); + const limit = parseInt(req.query.limit as string) || 10; const offset = parseInt(req.query.offset as string) || 0; @@ -42,102 +114,164 @@ router.get('/recent', async (req, res) => { storage.getTotalPaymentCountByUser(userId), ]); - res.json({ payments, totalCount }); + res.status(200).json({ payments, totalCount }); } catch (err) { - console.error('Failed to fetch payments:', err); - res.status(500).json({ message: 'Failed to fetch recent payments' }); + console.error("Failed to fetch payments:", err); + res.status(500).json({ message: "Failed to fetch recent payments" }); } }); // GET /api/payments/claim/:claimId -router.get('/claim/:claimId', async (req: Request, res: Response): Promise => { - try { - const userId = req.user.id; - const claimId = parseInt(req.params.claimId); +router.get( + "/claim/:claimId", + async (req: Request, res: Response): Promise => { + try { + const userId = req.user?.id; + if (!userId) return res.status(401).json({ message: "Unauthorized" }); - const payment = await storage.getPaymentByClaimId(userId, claimId); - if (!payment) return res.status(404).json({ message: 'Payment not found' }); + const parsedClaimId = parseIntOrError(req.params.claimId, "Claim ID"); - res.json(payment); - } catch (err) { - console.error('Failed to fetch payment by claim:', err); - res.status(500).json({ message: 'Failed to fetch payment' }); + const payments = await storage.getPaymentsByClaimId( + userId, + parsedClaimId + ); + if (!payments) + return res.status(404).json({ message: "No payments found for claim" }); + + res.status(200).json(payments); + } catch (error) { + console.error("Error fetching payments:", error); + res.status(500).json({ message: "Failed to retrieve payments" }); + } } -}); +); // GET /api/payments/patient/:patientId -router.get('/patient/:patientId', async (req, res) => { - try { - const userId = req.user.id; - const patientId = parseInt(req.params.patientId); +router.get( + "/patient/:patientId", + async (req: Request, res: Response): Promise => { + try { + const userId = req.user?.id; + if (!userId) return res.status(401).json({ message: "Unauthorized" }); - const payments = await storage.getPaymentsByPatientId(userId, patientId); - res.json(payments); - } catch (err) { - console.error('Failed to fetch patient payments:', err); - res.status(500).json({ message: 'Failed to fetch patient payments' }); + const parsedPatientId = parseIntOrError( + req.params.patientId, + "Patient ID" + ); + + const payments = await storage.getPaymentsByPatientId( + userId, + parsedPatientId + ); + + if (!payments) + return res.status(404).json({ message: "No payments found for claim" }); + + res.status(200).json(payments); + } catch (err) { + console.error("Failed to fetch patient payments:", err); + res.status(500).json({ message: "Failed to fetch patient payments" }); + } } -}); +); // GET /api/payments/filter -router.get('/filter', async (req, res) => { +router.get("/filter", async (req: Request, res: Response): Promise => { try { - const userId = req.user.id; - const { from, to } = req.query; - const fromDate = new Date(from as string); - const toDate = new Date(to as string); + const userId = req.user?.id; + if (!userId) return res.status(401).json({ message: "Unauthorized" }); - const payments = await storage.getPaymentsByDateRange(userId, fromDate, toDate); - res.json(payments); + const validated = paymentFilterSchema.safeParse(req.query); + if (!validated.success) { + return res.status(400).json({ + message: "Invalid date format", + errors: validated.error.errors, + }); + } + + const { from, to } = validated.data; + const payments = await storage.getPaymentsByDateRange( + userId, + new Date(from), + new Date(to) + ); + res.status(200).json(payments); } catch (err) { - console.error('Failed to filter payments:', err); - res.status(500).json({ message: 'Failed to filter payments' }); + console.error("Failed to filter payments:", err); + res.status(500).json({ message: "Server error" }); } }); // POST /api/payments/:claimId -router.post('/:claimId', body('totalBilled').isDecimal(),(req: Request, res: Response): Promise => { - const errors = validationResult(req); - if (!errors.isEmpty()) return res.status(400).json({ errors: errors.array() }); - +router.post("/:claimId", async (req: Request, res: Response): Promise => { try { - const userId = req.user.id; - const claimId = parseInt(req.params.claimId); - const { totalBilled } = req.body; + const userId = req.user?.id; + if (!userId) return res.status(401).json({ message: "Unauthorized" }); - const payment = await storage.createPayment({ userId, claimId, totalBilled }); + const claimId = parseIntOrError(req.params.claimId, "Claim ID"); + + const validated = insertPaymentSchema.safeParse({ + ...req.body, + claimId, + userId, + }); + + if (!validated.success) { + return res.status(400).json({ + message: "Validation failed", + errors: validated.error.flatten(), + }); + } + + const payment = await storage.createPayment(validated.data); res.status(201).json(payment); - } catch (err) { - console.error('Failed to create payment:', err); - res.status(500).json({ message: 'Failed to create payment' }); + } catch (err: unknown) { + const message = + err instanceof Error ? err.message : "Failed to create payment"; + res.status(500).json({ message }); } }); // PUT /api/payments/:id -router.put('/:id', async (req, res) => { +router.put("/:id", async (req: Request, res: Response): Promise => { try { - const userId = req.user.id; - const id = parseInt(req.params.id); - const updates = req.body; + const userId = req.user?.id; + if (!userId) return res.status(401).json({ message: "Unauthorized" }); - const updated = await storage.updatePayment(userId, id, updates); - res.json(updated); - } catch (err) { - console.error('Failed to update payment:', err); - res.status(500).json({ message: 'Failed to update payment' }); + const id = parseIntOrError(req.params.id, "Payment ID"); + + const validated = updatePaymentSchema.safeParse(req.body); + if (!validated.success) { + return res.status(400).json({ + message: "Validation failed", + errors: validated.error.flatten(), + }); + } + + const updated = await storage.updatePayment(id, validated.data, userId); + + res.status(200).json(updated); + } catch (err: unknown) { + const message = + err instanceof Error ? err.message : "Failed to update payment"; + res.status(500).json({ message }); } }); // DELETE /api/payments/:id -router.delete('/:id', async (req, res) => { +router.delete("/:id", async (req: Request, res: Response): Promise => { try { - const userId = req.user.id; - const id = parseInt(req.params.id); + const userId = req.user?.id; + if (!userId) return res.status(401).json({ message: "Unauthorized" }); + + const id = parseIntOrError(req.params.id, "Payment ID"); await storage.deletePayment(userId, id); - res.json({ message: 'Payment deleted' }); - } catch (err) { - console.error('Failed to delete payment:', err); - res.status(500).json({ message: 'Failed to delete payment' }); + + res.status(200).json({ message: "Payment deleted successfully" }); + } catch (err: unknown) { + const message = + err instanceof Error ? err.message : "Failed to delete payment"; + res.status(500).json({ message }); } }); diff --git a/apps/Backend/src/storage/index.ts b/apps/Backend/src/storage/index.ts index 12129d1..b19b8dd 100644 --- a/apps/Backend/src/storage/index.ts +++ b/apps/Backend/src/storage/index.ts @@ -16,7 +16,6 @@ import { import { z } from "zod"; import { Prisma } from "@repo/db/generated/prisma"; - //creating types out of schema auto generated. type Appointment = z.infer; @@ -167,9 +166,12 @@ export interface ClaimPdfMetadata { // Base Payment type type Payment = z.infer; -type PaymentTransaction = z.infer; -type ServiceLinePayment = z.infer - +type PaymentTransaction = z.infer< + typeof PaymentTransactionCreateInputObjectSchema +>; +type ServiceLinePayment = z.infer< + typeof ServiceLinePaymentCreateInputObjectSchema +>; const insertPaymentSchema = ( PaymentUncheckedCreateInputObjectSchema as unknown as z.ZodObject @@ -180,7 +182,6 @@ const insertPaymentSchema = ( }); type InsertPayment = z.infer; - const updatePaymentSchema = ( PaymentUncheckedCreateInputObjectSchema as unknown as z.ZodObject ) @@ -191,7 +192,6 @@ const updatePaymentSchema = ( .partial(); type UpdatePayment = z.infer; - type PaymentWithExtras = Prisma.PaymentGetPayload<{ include: { transactions: true; @@ -350,15 +350,33 @@ export interface IStorage { ): Promise; deletePdfGroup(id: number): Promise; - // Payment methods: + // Payment methods: createPayment(data: InsertPayment): Promise; - updatePayment(id: number, updates: UpdatePayment): Promise; - deletePayment(id: number): Promise; - getPaymentById(id: number): Promise; - getPaymentByClaimId(claimId: number): Promise; - getPaymentsByPatientId(patientId: number, userId: number): Promise; - getRecentPaymentsByUser(userId: number, limit: number, offset: number): Promise; - getPaymentsByDateRange(userId: number, from: Date, to: Date): Promise; + updatePayment( + id: number, + updates: UpdatePayment, + userId: number + ): Promise; + deletePayment(id: number, userId: number): Promise; + getPaymentById(id: number, userId: number): Promise; + getPaymentsByClaimId( + claimId: number, + userId: number + ): Promise; + getPaymentsByPatientId( + patientId: number, + userId: number + ): Promise; + getRecentPaymentsByUser( + userId: number, + limit: number, + offset: number + ): Promise; + getPaymentsByDateRange( + userId: number, + from: Date, + to: Date + ): Promise; getTotalPaymentCountByUser(userId: number): Promise; } @@ -557,18 +575,20 @@ export const storage: IStorage = { } }, - async getPatientAppointmentByDateTime( + async getPatientAppointmentByDateTime( patientId: number, date: Date, startTime: string ): Promise { - return await db.appointment.findFirst({ - where: { - patientId, - date, - startTime, - }, - }) ?? undefined; + return ( + (await db.appointment.findFirst({ + where: { + patientId, + date, + startTime, + }, + })) ?? undefined + ); }, async getStaffAppointmentByDateTime( @@ -577,14 +597,16 @@ export const storage: IStorage = { startTime: string, excludeId?: number ): Promise { - return await db.appointment.findFirst({ - where: { - staffId, - date, - startTime, - NOT: excludeId ? { id: excludeId } : undefined, - }, - }) ?? undefined; + return ( + (await db.appointment.findFirst({ + where: { + staffId, + date, + startTime, + NOT: excludeId ? { id: excludeId } : undefined, + }, + })) ?? undefined + ); }, async getPatientConflictAppointment( @@ -593,14 +615,16 @@ export const storage: IStorage = { startTime: string, excludeId: number ): Promise { - return await db.appointment.findFirst({ - where: { - patientId, - date, - startTime, - NOT: { id: excludeId }, - }, - }) ?? undefined; + return ( + (await db.appointment.findFirst({ + where: { + patientId, + date, + startTime, + NOT: { id: excludeId }, + }, + })) ?? undefined + ); }, async getStaffConflictAppointment( @@ -609,17 +633,18 @@ export const storage: IStorage = { startTime: string, excludeId: number ): Promise { - return await db.appointment.findFirst({ - where: { - staffId, - date, - startTime, - NOT: { id: excludeId }, - }, - }) ?? undefined; + return ( + (await db.appointment.findFirst({ + where: { + staffId, + date, + startTime, + NOT: { id: excludeId }, + }, + })) ?? undefined + ); }, - // Staff methods async getStaff(id: number): Promise { const staff = await db.staff.findUnique({ where: { id } }); @@ -884,33 +909,41 @@ export const storage: IStorage = { }, // Payment Methods - async createPayment(payment: InsertPayment): Promise { - return db.payment.create({ data: payment as Payment }); -}, - - async updatePayment(id: number, updates: UpdatePayment): Promise { - return db.payment.update({ where: { id }, data: updates }); + return db.payment.create({ data: payment as Payment }); }, - async deletePayment(id: number): Promise { + async updatePayment( + id: number, + updates: UpdatePayment, + userId: number + ): Promise { + const existing = await db.payment.findFirst({ where: { id, userId } }); + if (!existing) { + throw new Error("Not authorized or payment not found"); + } + + return db.payment.update({ + where: { id }, + data: updates, + }); + }, + + async deletePayment(id: number, userId: number): Promise { + const existing = await db.payment.findFirst({ where: { id, userId } }); + if (!existing) { + throw new Error("Not authorized or payment not found"); + } + await db.payment.delete({ where: { id } }); }, - async getPaymentById(id: number): Promise { - return db.payment.findUnique({ - where: { id }, - include: { - claim: true, - transactions: true, - servicePayments: true, - }, - }); - }, - - async getPaymentByClaimId(claimId: number): Promise { + async getPaymentById( + id: number, + userId: number + ): Promise { return db.payment.findFirst({ - where: { claimId }, + where: { id, userId }, include: { claim: true, transactions: true, @@ -919,8 +952,24 @@ export const storage: IStorage = { }); }, + async getPaymentsByClaimId( + claimId: number, + userId: number + ): Promise { + return db.payment.findFirst({ + where: { claimId, userId }, + include: { + claim: true, + transactions: true, + servicePayments: true, + }, + }); + }, - async getPaymentsByPatientId(patientId: number, userId: number): Promise { + async getPaymentsByPatientId( + patientId: number, + userId: number + ): Promise { return db.payment.findMany({ where: { patientId, @@ -934,7 +983,11 @@ export const storage: IStorage = { }); }, - async getRecentPaymentsByUser(userId: number, limit: number, offset: number): Promise { + async getRecentPaymentsByUser( + userId: number, + limit: number, + offset: number + ): Promise { return db.payment.findMany({ where: { userId }, orderBy: { createdAt: "desc" }, @@ -948,7 +1001,11 @@ export const storage: IStorage = { }); }, - async getPaymentsByDateRange(userId: number, from: Date, to: Date): Promise { + async getPaymentsByDateRange( + userId: number, + from: Date, + to: Date + ): Promise { return db.payment.findMany({ where: { userId, diff --git a/apps/Frontend/src/components/payments/payment-edit-modal.tsx b/apps/Frontend/src/components/payments/payment-edit-modal.tsx new file mode 100644 index 0000000..8d727be --- /dev/null +++ b/apps/Frontend/src/components/payments/payment-edit-modal.tsx @@ -0,0 +1,141 @@ +import { useState } from "react"; +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; +import { Label } from "@/components/ui/label"; +import { Textarea } from "@/components/ui/textarea"; +import { apiRequest, queryClient } from "@/lib/queryClient"; +import { useToast } from "@/hooks/use-toast"; +import { z } from "zod"; +import { format } from "date-fns"; +import { PaymentUncheckedCreateInputObjectSchema } from "@repo/db/usedSchemas"; + +type Payment = z.infer; + +interface PaymentEditModalProps { + isOpen: boolean; + onClose: () => void; + payment: Payment; + onSave: () => void; +} + +export default function PaymentEditModal({ + isOpen, + onClose, + payment, + onSave, +}: PaymentEditModalProps) { + const { toast } = useToast(); + const [form, setForm] = useState({ + payerName: payment.payerName, + amountPaid: payment.amountPaid.toString(), + paymentDate: format(new Date(payment.paymentDate), "yyyy-MM-dd"), + paymentMethod: payment.paymentMethod, + note: payment.note || "", + }); + + const [loading, setLoading] = useState(false); + + const handleChange = (e: React.ChangeEvent) => { + const { name, value } = e.target; + setForm({ ...form, [name]: value }); + }; + + const handleSubmit = async () => { + setLoading(true); + try { + const res = await apiRequest("PUT", `/api/payments/${payment.id}`, { + ...form, + amountPaid: parseFloat(form.amountPaid), + paymentDate: new Date(form.paymentDate), + }); + + if (!res.ok) throw new Error("Failed to update payment"); + + toast({ title: "Success", description: "Payment updated successfully" }); + queryClient.invalidateQueries(); + onClose(); + onSave(); + } catch (error) { + toast({ + title: "Error", + description: "Failed to update payment", + variant: "destructive", + }); + } finally { + setLoading(false); + } + }; + + return ( + + + + Edit Payment + + +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +