From 23e4eb6b7cde989f8a2d4e083ae4680e2d45e43d Mon Sep 17 00:00:00 2001 From: Potenz Date: Wed, 30 Jul 2025 23:20:15 +0530 Subject: [PATCH] init files --- apps/Backend/src/routes/payments.ts | 144 ++++++++++++++++++++++ apps/Backend/src/storage/index.ts | 140 ++++++++++++++++++++- apps/Frontend/src/pages/payments-page.tsx | 43 ++++++- packages/db/package.json | 3 +- packages/db/prisma/schema.prisma | 89 +++++++++++-- packages/db/usedSchemas/index.ts | 5 +- 6 files changed, 410 insertions(+), 14 deletions(-) create mode 100644 apps/Backend/src/routes/payments.ts diff --git a/apps/Backend/src/routes/payments.ts b/apps/Backend/src/routes/payments.ts new file mode 100644 index 0000000..e81319a --- /dev/null +++ b/apps/Backend/src/routes/payments.ts @@ -0,0 +1,144 @@ +import { Router } from "express"; +import { Request, Response } from "express"; +import { storage } from "../storage"; +import { z } from "zod"; +import { ClaimUncheckedCreateInputObjectSchema } from "@repo/db/usedSchemas"; + +const router = Router(); + +// Define Zod schemas +const ClaimSchema = ( + ClaimUncheckedCreateInputObjectSchema as unknown as z.ZodObject +).omit({ + id: true, + createdAt: true, + updatedAt: true, +}); + +type InsertClaim = z.infer; + +const updateClaimSchema = ( + ClaimUncheckedCreateInputObjectSchema as unknown as z.ZodObject +) + .omit({ + id: true, + createdAt: true, + updatedAt: true, + }) + .partial(); + +type UpdateClaim = z.infer; + + +// GET /api/payments/recent +router.get('/recent', async (req, res) => { + try { + const userId = req.user.id; + const limit = parseInt(req.query.limit as string) || 10; + const offset = parseInt(req.query.offset as string) || 0; + + const [payments, totalCount] = await Promise.all([ + storage.getRecentPaymentsByUser(userId, limit, offset), + storage.getTotalPaymentCountByUser(userId), + ]); + + res.json({ payments, totalCount }); + } catch (err) { + 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); + + const payment = await storage.getPaymentByClaimId(userId, claimId); + if (!payment) return res.status(404).json({ message: 'Payment not found' }); + + res.json(payment); + } catch (err) { + console.error('Failed to fetch payment by claim:', err); + res.status(500).json({ message: 'Failed to fetch payment' }); + } +}); + +// GET /api/payments/patient/:patientId +router.get('/patient/:patientId', async (req, res) => { + try { + const userId = req.user.id; + const patientId = parseInt(req.params.patientId); + + 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' }); + } +}); + +// GET /api/payments/filter +router.get('/filter', async (req, res) => { + 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 payments = await storage.getPaymentsByDateRange(userId, fromDate, toDate); + res.json(payments); + } catch (err) { + console.error('Failed to filter payments:', err); + res.status(500).json({ message: 'Failed to filter payments' }); + } +}); + +// 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() }); + + try { + const userId = req.user.id; + const claimId = parseInt(req.params.claimId); + const { totalBilled } = req.body; + + const payment = await storage.createPayment({ userId, claimId, totalBilled }); + res.status(201).json(payment); + } catch (err) { + console.error('Failed to create payment:', err); + res.status(500).json({ message: 'Failed to create payment' }); + } +}); + +// PUT /api/payments/:id +router.put('/:id', async (req, res) => { + try { + const userId = req.user.id; + const id = parseInt(req.params.id); + const updates = req.body; + + 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' }); + } +}); + +// DELETE /api/payments/:id +router.delete('/:id', async (req, res) => { + try { + const userId = req.user.id; + const id = parseInt(req.params.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' }); + } +}); + +export default router; diff --git a/apps/Backend/src/storage/index.ts b/apps/Backend/src/storage/index.ts index 0d64007..12129d1 100644 --- a/apps/Backend/src/storage/index.ts +++ b/apps/Backend/src/storage/index.ts @@ -9,8 +9,13 @@ import { PdfFileUncheckedCreateInputObjectSchema, PdfGroupUncheckedCreateInputObjectSchema, PdfCategorySchema, + PaymentUncheckedCreateInputObjectSchema, + PaymentTransactionCreateInputObjectSchema, + ServiceLinePaymentCreateInputObjectSchema, } from "@repo/db/usedSchemas"; import { z } from "zod"; +import { Prisma } from "@repo/db/generated/prisma"; + //creating types out of schema auto generated. type Appointment = z.infer; @@ -160,6 +165,41 @@ export interface ClaimPdfMetadata { uploadedAt: Date; } +// Base Payment type +type Payment = z.infer; +type PaymentTransaction = z.infer; +type ServiceLinePayment = z.infer + + +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; + }; +}>; + export interface IStorage { // User methods getUser(id: number): Promise; @@ -290,7 +330,7 @@ export interface IStorage { updates: Partial> ): Promise; - // Group management + // PDF Group management createPdfGroup( patientId: number, title: string, @@ -309,6 +349,17 @@ export interface IStorage { updates: Partial> ): Promise; deletePdfGroup(id: number): Promise; + + // 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; + getTotalPaymentCountByUser(userId: number): Promise; } export const storage: IStorage = { @@ -831,4 +882,91 @@ export const storage: IStorage = { return false; } }, + + // 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 }); + }, + + async deletePayment(id: number): Promise { + 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 { + return db.payment.findFirst({ + where: { claimId }, + include: { + claim: true, + transactions: true, + servicePayments: true, + }, + }); + }, + + + async getPaymentsByPatientId(patientId: number, userId: number): Promise { + return db.payment.findMany({ + where: { + patientId, + userId, + }, + include: { + claim: true, + transactions: true, + servicePayments: true, + }, + }); + }, + + async getRecentPaymentsByUser(userId: number, limit: number, offset: number): Promise { + return db.payment.findMany({ + where: { userId }, + orderBy: { createdAt: "desc" }, + skip: offset, + take: limit, + include: { + claim: true, + transactions: true, + servicePayments: true, + }, + }); + }, + + async getPaymentsByDateRange(userId: number, from: Date, to: Date): Promise { + return db.payment.findMany({ + where: { + userId, + createdAt: { + gte: from, + lte: to, + }, + }, + orderBy: { createdAt: "desc" }, + include: { + claim: true, + transactions: true, + servicePayments: true, + }, + }); + }, + + async getTotalPaymentCountByUser(userId: number): Promise { + return db.payment.count({ where: { userId } }); + }, }; diff --git a/apps/Frontend/src/pages/payments-page.tsx b/apps/Frontend/src/pages/payments-page.tsx index 10b532a..5707e5a 100644 --- a/apps/Frontend/src/pages/payments-page.tsx +++ b/apps/Frontend/src/pages/payments-page.tsx @@ -6,8 +6,6 @@ import { Card, CardHeader, CardTitle, CardContent, CardFooter } from "@/componen import { Button } from "@/components/ui/button"; import { useToast } from "@/hooks/use-toast"; import { useAuth } from "@/hooks/use-auth"; -// import { Patient, Appointment } from "@repo/db/shared/schemas"; -import { Patient, Appointment } from "@repo/db/shared/schemas"; import { CreditCard, Clock, @@ -35,6 +33,47 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; +import { PatientUncheckedCreateInputObjectSchema, AppointmentUncheckedCreateInputObjectSchema } from "@repo/db/usedSchemas"; +import { z } from "zod"; + +const PatientSchema = ( + PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject +).omit({ + appointments: true, +}); +type Patient = z.infer; + +const insertPatientSchema = ( + PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject +).omit({ + id: true, + createdAt: true, + userId: true, +}); +type InsertPatient = z.infer; + + +//creating types out of schema auto generated. +type Appointment = z.infer; + +const insertAppointmentSchema = ( + AppointmentUncheckedCreateInputObjectSchema as unknown as z.ZodObject +).omit({ + id: true, + createdAt: true, +}); +type InsertAppointment = z.infer; + +const updateAppointmentSchema = ( + AppointmentUncheckedCreateInputObjectSchema as unknown as z.ZodObject +) + .omit({ + id: true, + createdAt: true, + userId: true, + }) + .partial(); +type UpdateAppointment = z.infer; export default function PaymentsPage() { const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); diff --git a/packages/db/package.json b/packages/db/package.json index 1d04817..773642f 100644 --- a/packages/db/package.json +++ b/packages/db/package.json @@ -13,7 +13,8 @@ "exports": { "./client": "./src/index.ts", "./shared/schemas" : "./shared/schemas/index.ts", - "./usedSchemas" : "./usedSchemas/index.ts" + "./usedSchemas" : "./usedSchemas/index.ts", + "./generated/prisma" : "./generated/prisma/index.d.ts" }, "dependencies": { "@prisma/client": "^6.10.0", diff --git a/packages/db/prisma/schema.prisma b/packages/db/prisma/schema.prisma index b14c339..9793f20 100644 --- a/packages/db/prisma/schema.prisma +++ b/packages/db/prisma/schema.prisma @@ -114,6 +114,7 @@ model Claim { staff Staff? @relation("ClaimStaff", fields: [staffId], references: [id]) serviceLines ServiceLine[] + payment Payment? } enum ClaimStatus { @@ -124,15 +125,16 @@ enum ClaimStatus { } model ServiceLine { - id Int @id @default(autoincrement()) - claimId Int - procedureCode String - procedureDate DateTime @db.Date - oralCavityArea String? - toothNumber String? - toothSurface String? - billedAmount Float - claim Claim @relation(fields: [claimId], references: [id], onDelete: Cascade) + id Int @id @default(autoincrement()) + claimId Int + procedureCode String + procedureDate DateTime @db.Date + oralCavityArea String? + toothNumber String? + toothSurface String? + billedAmount Float + claim Claim @relation(fields: [claimId], references: [id], onDelete: Cascade) + servicePayments ServiceLinePayment[] } model InsuranceCredential { @@ -177,3 +179,72 @@ enum PdfCategory { ELIGIBILITY OTHER } + +model Payment { + id Int @id @default(autoincrement()) + patientId Int + userId Int + claimId Int @unique + totalBilled Decimal @db.Decimal(10, 2) + totalPaid Decimal @default(0.00) @db.Decimal(10, 2) + totalDue Decimal @db.Decimal(10, 2) + status PaymentStatus @default(PENDING) + receivedDate DateTime? + paymentMethod PaymentMethod? + notes String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + claim Claim @relation(fields: [claimId], references: [id], onDelete: Cascade) + transactions PaymentTransaction[] + servicePayments ServiceLinePayment[] + + @@index([claimId]) +} + +model PaymentTransaction { + id Int @id @default(autoincrement()) + paymentId Int + amount Decimal @db.Decimal(10, 2) + method PaymentMethod + transactionId String? + receivedDate DateTime + payerName String? + notes String? + createdAt DateTime @default(now()) + + payment Payment @relation(fields: [paymentId], references: [id], onDelete: Cascade) + + @@index([paymentId]) +} + +model ServiceLinePayment { + id Int @id @default(autoincrement()) + paymentId Int + serviceLineId Int + paidAmount Decimal @db.Decimal(10, 2) + adjustedAmount Decimal @default(0.00) @db.Decimal(10, 2) + notes String? + + payment Payment @relation(fields: [paymentId], references: [id], onDelete: Cascade) + serviceLine ServiceLine @relation(fields: [serviceLineId], references: [id], onDelete: Cascade) + + @@index([paymentId]) + @@index([serviceLineId]) +} + +enum PaymentStatus { + PENDING + PARTIALLY_PAID + PAID + OVERPAID + DENIED +} + +enum PaymentMethod { + EFT + CHECK + CASH + CARD + OTHER +} diff --git a/packages/db/usedSchemas/index.ts b/packages/db/usedSchemas/index.ts index 87c9315..c7f7b0a 100644 --- a/packages/db/usedSchemas/index.ts +++ b/packages/db/usedSchemas/index.ts @@ -8,4 +8,7 @@ export * from '../shared/schemas/objects/InsuranceCredentialUncheckedCreateInput export * from '../shared/schemas/objects/PdfFileUncheckedCreateInput.schema' export * from '../shared/schemas/objects/PdfGroupUncheckedCreateInput.schema' export * from '../shared/schemas/enums/PdfCategory.schema' -export * from '../shared/schemas/enums/ClaimStatus.schema' \ No newline at end of file +export * from '../shared/schemas/enums/ClaimStatus.schema' +export * from '../shared/schemas/objects/PaymentUncheckedCreateInput.schema' +export * from '../shared/schemas/objects/PaymentTransactionCreateInput.schema' +export * from '../shared/schemas/objects/ServiceLinePaymentCreateInput.schema' \ No newline at end of file