From fc15f5136860d3ba05f48350426ac0435b60fee6 Mon Sep 17 00:00:00 2001 From: rayeberechi Date: Sun, 29 Mar 2026 15:22:32 +0100 Subject: [PATCH 1/2] feat(compliance): merchant account deletion / anonymization (#314) Schema: - Add deletion_requested_at, anonymized_at to Merchant - Add MerchantDeletionRequest model (tracks request + execution) - Add AuditActionType: merchant_deletion_requested, merchant_anonymized - Add AuditEntityType: merchant_account - Migration: 20260329143000_merchant_deletion_anonymization Service (merchantDeletion.service.ts): - requestDeletion(): records request, marks merchant, writes audit log - executeDeletion(): anonymizes PII fields, wipes KYC docs, clears webhook URLs, hard-deletes OTPs/bank/subs/customers; retains payments/settlements/refunds/invoices under legal hold - getDeletionRequest(): status polling Routes: - POST /api/v1/merchants/me/deletion-request - GET /api/v1/merchants/me/deletion-request - POST /api/v1/merchants/admin/:id/deletion-request - POST /api/v1/merchants/admin/:id/anonymize Tests: 9 unit tests, all passing - Fix IKMSProvider/KMSFactory type errors (pre-existing) - Fix payment.service uuid ESM issue (crypto.randomUUID) - Add adminUser to AuthRequest type --- .../migration.sql | 30 +++ fluxapay_backend/prisma/schema.prisma | 22 +++ fluxapay_backend/src/app.ts | 2 + .../merchantDeletion.controller.ts | 71 +++++++ .../src/routes/merchantDeletion.route.ts | 22 +++ .../merchantDeletion.service.test.ts | 174 ++++++++++++++++++ .../src/services/kms/IKMSProvider.ts | 5 +- .../src/services/kms/KMSFactory.ts | 2 +- .../src/services/merchantDeletion.service.ts | 161 ++++++++++++++++ .../src/services/payment.service.ts | 4 +- fluxapay_backend/src/types/express.d.ts | 5 + 11 files changed, 493 insertions(+), 5 deletions(-) create mode 100644 fluxapay_backend/prisma/migrations/20260329143000_merchant_deletion_anonymization/migration.sql create mode 100644 fluxapay_backend/src/controllers/merchantDeletion.controller.ts create mode 100644 fluxapay_backend/src/routes/merchantDeletion.route.ts create mode 100644 fluxapay_backend/src/services/__tests__/merchantDeletion.service.test.ts create mode 100644 fluxapay_backend/src/services/merchantDeletion.service.ts diff --git a/fluxapay_backend/prisma/migrations/20260329143000_merchant_deletion_anonymization/migration.sql b/fluxapay_backend/prisma/migrations/20260329143000_merchant_deletion_anonymization/migration.sql new file mode 100644 index 00000000..ae1ba532 --- /dev/null +++ b/fluxapay_backend/prisma/migrations/20260329143000_merchant_deletion_anonymization/migration.sql @@ -0,0 +1,30 @@ +-- Add compliance columns to Merchant +ALTER TABLE "Merchant" + ADD COLUMN "deletion_requested_at" TIMESTAMP(3), + ADD COLUMN "anonymized_at" TIMESTAMP(3); + +-- Extend AuditActionType enum +ALTER TYPE "AuditActionType" ADD VALUE IF NOT EXISTS 'merchant_deletion_requested'; +ALTER TYPE "AuditActionType" ADD VALUE IF NOT EXISTS 'merchant_anonymized'; + +-- Extend AuditEntityType enum +ALTER TYPE "AuditEntityType" ADD VALUE IF NOT EXISTS 'merchant_account'; + +-- CreateTable: MerchantDeletionRequest +CREATE TABLE "MerchantDeletionRequest" ( + "id" TEXT NOT NULL, + "merchantId" TEXT NOT NULL, + "reason" TEXT, + "requested_by" TEXT NOT NULL, + "executed_at" TIMESTAMP(3), + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "MerchantDeletionRequest_pkey" PRIMARY KEY ("id") +); + +CREATE UNIQUE INDEX "MerchantDeletionRequest_merchantId_key" + ON "MerchantDeletionRequest"("merchantId"); + +CREATE INDEX "MerchantDeletionRequest_merchantId_idx" + ON "MerchantDeletionRequest"("merchantId"); diff --git a/fluxapay_backend/prisma/schema.prisma b/fluxapay_backend/prisma/schema.prisma index 98e9f916..47928d3c 100644 --- a/fluxapay_backend/prisma/schema.prisma +++ b/fluxapay_backend/prisma/schema.prisma @@ -37,6 +37,10 @@ model Merchant { email_notifications_enabled Boolean @default(true) notify_on_payment Boolean @default(false) notify_on_settlement Boolean @default(true) + /** Compliance: set when merchant requests account deletion */ + deletion_requested_at DateTime? + /** Compliance: set after PII anonymization is complete */ + anonymized_at DateTime? created_at DateTime @default(now()) updated_at DateTime @updatedAt otps OTP[] @@ -496,6 +500,8 @@ enum AuditActionType { settlement_batch_initiate settlement_batch_complete settlement_batch_fail + merchant_deletion_requested + merchant_anonymized } enum AuditEntityType { @@ -503,4 +509,20 @@ enum AuditEntityType { system_config sweep_operation settlement_batch + merchant_account +} + +/// Tracks a merchant's right-to-erasure request. +/// Financial records are retained under legal hold; only PII is anonymized. +model MerchantDeletionRequest { + id String @id @default(cuid()) + merchantId String @unique + reason String? + requested_by String // "merchant" | "admin:" + /// Set when PII anonymization is executed + executed_at DateTime? + created_at DateTime @default(now()) + updated_at DateTime @updatedAt + + @@index([merchantId]) } diff --git a/fluxapay_backend/src/app.ts b/fluxapay_backend/src/app.ts index 41fd7573..84d61e03 100644 --- a/fluxapay_backend/src/app.ts +++ b/fluxapay_backend/src/app.ts @@ -24,6 +24,7 @@ import keysRoutes from "./routes/keys.route"; import settlementBatchRoutes from "./routes/settlementBatch.route"; import dashboardRoutes from "./routes/dashboard.route"; import auditRoutes from "./routes/audit.route"; +import merchantDeletionRoutes from "./routes/merchantDeletion.route"; const app = express(); const prisma = new PrismaClient(); @@ -86,6 +87,7 @@ app.use("/api/v1/admin/reconciliation", reconciliationRoutes); app.use("/api/v1/admin/settlement", settlementBatchRoutes); app.use("/api/v1/admin/sweep", sweepRoutes); app.use("/api/v1/admin", auditRoutes); +app.use("/api/v1/merchants", merchantDeletionRoutes); // Basic health check app.get("/health", (req, res) => { diff --git a/fluxapay_backend/src/controllers/merchantDeletion.controller.ts b/fluxapay_backend/src/controllers/merchantDeletion.controller.ts new file mode 100644 index 00000000..3e1d1ed5 --- /dev/null +++ b/fluxapay_backend/src/controllers/merchantDeletion.controller.ts @@ -0,0 +1,71 @@ +import { Response } from "express"; +import { AuthRequest } from "../types/express"; +import { validateUserId } from "../helpers/request.helper"; +import { + requestDeletion, + executeDeletion, + getDeletionRequest, +} from "../services/merchantDeletion.service"; + +/** + * POST /api/v1/merchants/me/deletion-request + * Merchant self-service: submit a right-to-erasure request. + */ +export async function selfRequestDeletion(req: AuthRequest, res: Response) { + try { + const merchantId = await validateUserId(req); + const { reason } = req.body ?? {}; + const result = await requestDeletion(merchantId, "merchant", reason); + res.status(202).json({ + message: "Deletion request recorded. An admin will review and execute anonymization.", + ...result, + }); + } catch (err: any) { + res.status(err.status || 500).json({ message: err.message || "Server error" }); + } +} + +/** + * GET /api/v1/merchants/me/deletion-request + * Merchant: check status of their deletion request. + */ +export async function selfGetDeletionRequest(req: AuthRequest, res: Response) { + try { + const merchantId = await validateUserId(req); + const request = await getDeletionRequest(merchantId); + res.json(request); + } catch (err: any) { + res.status(err.status || 500).json({ message: err.message || "Server error" }); + } +} + +/** + * POST /api/v1/admin/merchants/:merchantId/deletion-request + * Admin: submit a deletion request on behalf of a merchant. + */ +export async function adminRequestDeletion(req: AuthRequest, res: Response) { + try { + const { merchantId } = req.params as Record; + const adminId = req.user?.id ?? "admin"; + const { reason } = req.body ?? {}; + const result = await requestDeletion(merchantId, `admin:${adminId}`, reason); + res.status(202).json({ message: "Deletion request recorded.", ...result }); + } catch (err: any) { + res.status(err.status || 500).json({ message: err.message || "Server error" }); + } +} + +/** + * POST /api/v1/admin/merchants/:merchantId/anonymize + * Admin: execute PII anonymization (irreversible). + */ +export async function adminExecuteDeletion(req: AuthRequest, res: Response) { + try { + const { merchantId } = req.params as Record; + const adminId = req.user?.id ?? "admin"; + await executeDeletion(merchantId, adminId); + res.json({ message: "Merchant account anonymized. Financial records retained." }); + } catch (err: any) { + res.status(err.status || 500).json({ message: err.message || "Server error" }); + } +} diff --git a/fluxapay_backend/src/routes/merchantDeletion.route.ts b/fluxapay_backend/src/routes/merchantDeletion.route.ts new file mode 100644 index 00000000..f782c0c8 --- /dev/null +++ b/fluxapay_backend/src/routes/merchantDeletion.route.ts @@ -0,0 +1,22 @@ +import { Router } from "express"; +import { authenticateApiKey } from "../middleware/apiKeyAuth.middleware"; +import { authenticateToken } from "../middleware/auth.middleware"; +import { adminAuth } from "../middleware/adminAuth.middleware"; +import { + selfRequestDeletion, + selfGetDeletionRequest, + adminRequestDeletion, + adminExecuteDeletion, +} from "../controllers/merchantDeletion.controller"; + +const router = Router(); + +// ── Merchant self-service ───────────────────────────────────────────────────── +router.post("/me/deletion-request", authenticateApiKey, selfRequestDeletion); +router.get("/me/deletion-request", authenticateApiKey, selfGetDeletionRequest); + +// ── Admin ───────────────────────────────────────────────────────────────────── +router.post("/admin/:merchantId/deletion-request", authenticateToken, adminAuth, adminRequestDeletion); +router.post("/admin/:merchantId/anonymize", authenticateToken, adminAuth, adminExecuteDeletion); + +export default router; diff --git a/fluxapay_backend/src/services/__tests__/merchantDeletion.service.test.ts b/fluxapay_backend/src/services/__tests__/merchantDeletion.service.test.ts new file mode 100644 index 00000000..eaa710b0 --- /dev/null +++ b/fluxapay_backend/src/services/__tests__/merchantDeletion.service.test.ts @@ -0,0 +1,174 @@ +import { AuditActionType, AuditEntityType } from "../../generated/client/client"; + +// ── Prisma mock ─────────────────────────────────────────────────────────────── +const merchant = { findUnique: jest.fn(), update: jest.fn() }; +const merchantDeletionRequest = { + upsert: jest.fn(), + findUnique: jest.fn(), + update: jest.fn(), +}; +const merchantKYC = { updateMany: jest.fn() }; +const kYCDocument = { deleteMany: jest.fn() }; +const webhookLog = { updateMany: jest.fn() }; +const oTP = { deleteMany: jest.fn() }; +const bankAccount = { deleteMany: jest.fn() }; +const merchantSubscription = { deleteMany: jest.fn() }; +const customer = { deleteMany: jest.fn() }; +const auditLog = { create: jest.fn() }; + +// $transaction executes the callback with the same mock client +const txClient = { + merchant, + merchantDeletionRequest, + merchantKYC, + kYCDocument, + webhookLog, + oTP, + bankAccount, + merchantSubscription, + customer, + auditLog, +}; + +jest.mock("../../generated/client/client", () => ({ + PrismaClient: jest.fn(() => ({ + ...txClient, + $transaction: jest.fn((fn: (tx: typeof txClient) => Promise) => fn(txClient)), + })), + AuditActionType: { + merchant_deletion_requested: "merchant_deletion_requested", + merchant_anonymized: "merchant_anonymized", + }, + AuditEntityType: { + merchant_account: "merchant_account", + }, +})); + +import { + requestDeletion, + executeDeletion, + getDeletionRequest, +} from "../merchantDeletion.service"; + +const MERCHANT_ID = "merchant-1"; +const ADMIN_ID = "admin-1"; + +const activeMerchant = { + id: MERCHANT_ID, + anonymized_at: null, + deletion_requested_at: null, +}; + +beforeEach(() => jest.clearAllMocks()); + +describe("requestDeletion", () => { + it("creates a deletion request and audit log", async () => { + merchant.findUnique.mockResolvedValue(activeMerchant); + merchantDeletionRequest.upsert.mockResolvedValue({ id: "req-1", merchantId: MERCHANT_ID }); + merchant.update.mockResolvedValue({}); + auditLog.create.mockResolvedValue({}); + + const result = await requestDeletion(MERCHANT_ID, "merchant", "closing business"); + + expect(merchantDeletionRequest.upsert).toHaveBeenCalledWith( + expect.objectContaining({ + where: { merchantId: MERCHANT_ID }, + create: expect.objectContaining({ requested_by: "merchant" }), + }), + ); + expect(merchant.update).toHaveBeenCalledWith( + expect.objectContaining({ data: expect.objectContaining({ deletion_requested_at: expect.any(Date) }) }), + ); + expect(auditLog.create).toHaveBeenCalledWith( + expect.objectContaining({ + data: expect.objectContaining({ + action_type: AuditActionType.merchant_deletion_requested, + entity_type: AuditEntityType.merchant_account, + entity_id: MERCHANT_ID, + }), + }), + ); + expect(result.requestId).toBe("req-1"); + }); + + it("throws 404 when merchant not found", async () => { + merchant.findUnique.mockResolvedValue(null); + await expect(requestDeletion(MERCHANT_ID, "merchant")).rejects.toMatchObject({ status: 404 }); + }); + + it("throws 409 when already anonymized", async () => { + merchant.findUnique.mockResolvedValue({ ...activeMerchant, anonymized_at: new Date() }); + await expect(requestDeletion(MERCHANT_ID, "merchant")).rejects.toMatchObject({ status: 409 }); + }); +}); + +describe("executeDeletion", () => { + beforeEach(() => { + merchant.findUnique.mockResolvedValue(activeMerchant); + merchantDeletionRequest.findUnique.mockResolvedValue({ id: "req-1", merchantId: MERCHANT_ID }); + merchant.update.mockResolvedValue({}); + merchantKYC.updateMany.mockResolvedValue({}); + kYCDocument.deleteMany.mockResolvedValue({}); + webhookLog.updateMany.mockResolvedValue({}); + oTP.deleteMany.mockResolvedValue({}); + bankAccount.deleteMany.mockResolvedValue({}); + merchantSubscription.deleteMany.mockResolvedValue({}); + customer.deleteMany.mockResolvedValue({}); + merchantDeletionRequest.update.mockResolvedValue({}); + auditLog.create.mockResolvedValue({}); + }); + + it("anonymizes PII and writes audit log", async () => { + await executeDeletion(MERCHANT_ID, ADMIN_ID); + + expect(merchant.update).toHaveBeenCalledWith( + expect.objectContaining({ + data: expect.objectContaining({ + email: expect.stringContaining("anonymized.invalid"), + password: "REDACTED", + anonymized_at: expect.any(Date), + }), + }), + ); + expect(kYCDocument.deleteMany).toHaveBeenCalled(); + expect(oTP.deleteMany).toHaveBeenCalled(); + expect(bankAccount.deleteMany).toHaveBeenCalled(); + expect(auditLog.create).toHaveBeenCalledWith( + expect.objectContaining({ + data: expect.objectContaining({ + action_type: AuditActionType.merchant_anonymized, + entity_id: MERCHANT_ID, + }), + }), + ); + }); + + it("throws 404 when merchant not found", async () => { + merchant.findUnique.mockResolvedValue(null); + await expect(executeDeletion(MERCHANT_ID, ADMIN_ID)).rejects.toMatchObject({ status: 404 }); + }); + + it("throws 409 when already anonymized", async () => { + merchant.findUnique.mockResolvedValue({ ...activeMerchant, anonymized_at: new Date() }); + await expect(executeDeletion(MERCHANT_ID, ADMIN_ID)).rejects.toMatchObject({ status: 409 }); + }); + + it("throws 400 when no deletion request exists", async () => { + merchantDeletionRequest.findUnique.mockResolvedValue(null); + await expect(executeDeletion(MERCHANT_ID, ADMIN_ID)).rejects.toMatchObject({ status: 400 }); + }); +}); + +describe("getDeletionRequest", () => { + it("returns the request when found", async () => { + const req = { id: "req-1", merchantId: MERCHANT_ID }; + merchantDeletionRequest.findUnique.mockResolvedValue(req); + const result = await getDeletionRequest(MERCHANT_ID); + expect(result.id).toBe("req-1"); + }); + + it("throws 404 when not found", async () => { + merchantDeletionRequest.findUnique.mockResolvedValue(null); + await expect(getDeletionRequest(MERCHANT_ID)).rejects.toMatchObject({ status: 404 }); + }); +}); diff --git a/fluxapay_backend/src/services/kms/IKMSProvider.ts b/fluxapay_backend/src/services/kms/IKMSProvider.ts index bfdd1246..5f15d80a 100644 --- a/fluxapay_backend/src/services/kms/IKMSProvider.ts +++ b/fluxapay_backend/src/services/kms/IKMSProvider.ts @@ -16,9 +16,10 @@ export interface IKMSProvider { storeMasterSeed(seed: string): Promise; /** - * Rotates the master seed encryption key (if supported) + * Rotates the master seed encryption key (if supported). + * Returns the new encrypted seed value. */ - rotateEncryptionKey?(): Promise; + rotateEncryptionKey?(newKeyId?: string): Promise; /** * Health check for KMS availability diff --git a/fluxapay_backend/src/services/kms/KMSFactory.ts b/fluxapay_backend/src/services/kms/KMSFactory.ts index ee154c73..133d0fa7 100644 --- a/fluxapay_backend/src/services/kms/KMSFactory.ts +++ b/fluxapay_backend/src/services/kms/KMSFactory.ts @@ -30,7 +30,7 @@ export class KMSFactory { break; } - return this.instance; + return this.instance!; } /** diff --git a/fluxapay_backend/src/services/merchantDeletion.service.ts b/fluxapay_backend/src/services/merchantDeletion.service.ts new file mode 100644 index 00000000..180cee76 --- /dev/null +++ b/fluxapay_backend/src/services/merchantDeletion.service.ts @@ -0,0 +1,161 @@ +/** + * Merchant account deletion / anonymization service. + * + * Retention policy (legal hold): + * - Payment, Settlement, Refund, Invoice, AuditLog records are KEPT + * (financial / regulatory obligation — typically 7 years). + * - PII fields on Merchant are overwritten with anonymized placeholders. + * - Webhook logs endpoint URL is cleared (may contain PII). + * - KYC documents are deleted; KYC record is anonymized. + * - OTPs, BankAccount, Customers, Subscriptions are hard-deleted. + * - An AuditLog entry is written for both the request and the execution. + */ +import { PrismaClient, AuditActionType, AuditEntityType } from "../generated/client/client"; + +const prisma = new PrismaClient(); + +const ANON_EMAIL = (id: string) => `deleted-${id}@anonymized.invalid`; +const ANON_PHONE = (id: string) => `+000000${id.slice(-6)}`; +const ANON_NAME = "Anonymized Account"; + +/** + * Record a deletion request (step 1 — merchant self-service or admin). + * Does NOT anonymize yet; an admin must approve via executeDeletion(). + */ +export async function requestDeletion( + merchantId: string, + requestedBy: string, + reason?: string, +): Promise<{ requestId: string }> { + const merchant = await prisma.merchant.findUnique({ where: { id: merchantId } }); + if (!merchant) throw { status: 404, message: "Merchant not found" }; + if (merchant.anonymized_at) throw { status: 409, message: "Account already anonymized" }; + + // Upsert so re-requests are idempotent + const req = await prisma.merchantDeletionRequest.upsert({ + where: { merchantId }, + create: { merchantId, reason, requested_by: requestedBy }, + update: { reason, requested_by: requestedBy, executed_at: null }, + }); + + // Mark merchant as pending deletion + await prisma.merchant.update({ + where: { id: merchantId }, + data: { deletion_requested_at: new Date() }, + }); + + // Audit log + await prisma.auditLog.create({ + data: { + admin_id: requestedBy, + action_type: AuditActionType.merchant_deletion_requested, + entity_type: AuditEntityType.merchant_account, + entity_id: merchantId, + details: { + reason: reason ?? null, + requested_by: requestedBy, + requested_at: new Date().toISOString(), + }, + }, + }); + + return { requestId: req.id }; +} + +/** + * Execute anonymization (admin-only step 2). + * + * Financial records (payments, settlements, refunds, invoices) are retained. + * PII is overwritten. Hard-deletes non-financial data. + */ +export async function executeDeletion( + merchantId: string, + adminId: string, +): Promise { + const merchant = await prisma.merchant.findUnique({ where: { id: merchantId } }); + if (!merchant) throw { status: 404, message: "Merchant not found" }; + if (merchant.anonymized_at) throw { status: 409, message: "Account already anonymized" }; + + const deletionReq = await prisma.merchantDeletionRequest.findUnique({ + where: { merchantId }, + }); + if (!deletionReq) throw { status: 400, message: "No deletion request found for this merchant" }; + + await prisma.$transaction(async (tx) => { + // 1. Anonymize Merchant PII + await tx.merchant.update({ + where: { id: merchantId }, + data: { + business_name: ANON_NAME, + email: ANON_EMAIL(merchantId), + phone_number: ANON_PHONE(merchantId), + password: "REDACTED", + webhook_url: null, + webhook_secret: "REDACTED", + api_key_hashed: null, + api_key_last_four: null, + checkout_logo_url: null, + checkout_accent_color: null, + anonymized_at: new Date(), + }, + }); + + // 2. Anonymize KYC record (keep for audit trail, wipe PII) + await tx.merchantKYC.updateMany({ + where: { merchantId }, + data: { + legal_business_name: ANON_NAME, + director_full_name: ANON_NAME, + director_email: ANON_EMAIL(merchantId), + director_phone: ANON_PHONE(merchantId), + government_id_number: "REDACTED", + business_registration_number: null, + business_address: "REDACTED", + }, + }); + + // 3. Delete KYC documents (files already on Cloudinary — caller must purge separately) + await tx.kYCDocument.deleteMany({ where: { kyc: { merchantId } } }); + + // 4. Clear webhook log endpoint URLs (may contain PII in query params) + await tx.webhookLog.updateMany({ + where: { merchantId }, + data: { endpoint_url: "REDACTED" }, + }); + + // 5. Hard-delete non-financial / session data + await tx.oTP.deleteMany({ where: { merchantId } }); + await tx.bankAccount.deleteMany({ where: { merchantId } }); + await tx.merchantSubscription.deleteMany({ where: { merchantId } }); + await tx.customer.deleteMany({ where: { merchantId } }); + + // 6. Mark deletion request as executed + await tx.merchantDeletionRequest.update({ + where: { merchantId }, + data: { executed_at: new Date() }, + }); + + // 7. Audit log + await tx.auditLog.create({ + data: { + admin_id: adminId, + action_type: AuditActionType.merchant_anonymized, + entity_type: AuditEntityType.merchant_account, + entity_id: merchantId, + details: { + executed_by: adminId, + executed_at: new Date().toISOString(), + retained: ["payments", "settlements", "refunds", "invoices", "audit_logs"], + deleted: ["otps", "bank_account", "subscriptions", "customers", "kyc_documents"], + anonymized: ["merchant_profile", "kyc_record", "webhook_log_urls"], + }, + }, + }); + }); +} + +export async function getDeletionRequest(merchantId: string) { + const req = await prisma.merchantDeletionRequest.findUnique({ where: { merchantId } }); + if (!req) throw { status: 404, message: "No deletion request found" }; + return req; +} diff --git a/fluxapay_backend/src/services/payment.service.ts b/fluxapay_backend/src/services/payment.service.ts index 2dc80456..490889e0 100644 --- a/fluxapay_backend/src/services/payment.service.ts +++ b/fluxapay_backend/src/services/payment.service.ts @@ -1,5 +1,5 @@ import { PrismaClient } from "../generated/client/client"; -import { v4 as uuidv4 } from "uuid"; +import crypto from "crypto"; import { HDWalletService } from "./HDWalletService"; import { StellarService } from "./StellarService"; import { sorobanService } from "./SorobanService"; @@ -62,7 +62,7 @@ export class PaymentService { cancel_url?: string; customerId?: string; }) { - const paymentId = uuidv4(); + const paymentId = crypto.randomUUID(); const expiration = new Date(Date.now() + 15 * 60 * 1000); // 15 min expiry const sanitizedMetadata = validateAndSanitizeMetadata(metadata); diff --git a/fluxapay_backend/src/types/express.d.ts b/fluxapay_backend/src/types/express.d.ts index e98a06fd..583f63c9 100644 --- a/fluxapay_backend/src/types/express.d.ts +++ b/fluxapay_backend/src/types/express.d.ts @@ -5,6 +5,11 @@ export interface AuthRequest extends Request { id?: string; email?: string; }; + adminUser?: { + id: string; + email: string; + role: string; + }; merchantId?: string; requestId?: string; } From d5616fcddb367edbb1fe782428cc837941471ce9 Mon Sep 17 00:00:00 2001 From: rayeberechi Date: Sun, 29 Mar 2026 15:33:31 +0100 Subject: [PATCH 2/2] fix: run prisma generate before tsc to expose MerchantDeletionRequest model and new Merchant fields --- fluxapay_backend/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fluxapay_backend/package.json b/fluxapay_backend/package.json index 03cbca2d..a046c102 100644 --- a/fluxapay_backend/package.json +++ b/fluxapay_backend/package.json @@ -4,7 +4,7 @@ "description": "Fluxapay is a payment gateway on the Stellar blockchain that enables merchants to accept crypto payments and get settled in their local fiat currency.", "main": "index.js", "scripts": { - "build": "tsc", + "build": "prisma generate && tsc", "start": "node dist/index.js", "dev": "nodemon src/index.ts", "test": "jest",