import { permissions } from "@mrp/shared"; import { crmContactEntryTypes, crmContactRoles, crmLifecycleStages, crmRecordStatuses } from "@mrp/shared/dist/crm/types.js"; import { Router } from "express"; import { z } from "zod"; import { fail, ok } from "../../lib/http.js"; import { requirePermissions } from "../../lib/rbac.js"; import { createCustomerContactEntry, createCustomerContact, createCustomer, createVendorContactEntry, createVendorContact, createVendor, getCustomerById, getVendorById, listCustomers, listCustomerHierarchyOptions, listVendors, updateCustomer, updateVendor, } from "./service.js"; const crmRecordSchema = z.object({ name: z.string().trim().min(1), email: z.string().trim().email(), phone: z.string().trim().min(1), addressLine1: z.string().trim().min(1), addressLine2: z.string(), city: z.string().trim().min(1), state: z.string().trim().min(1), postalCode: z.string().trim().min(1), country: z.string().trim().min(1), status: z.enum(crmRecordStatuses), lifecycleStage: z.enum(crmLifecycleStages).optional(), notes: z.string(), isReseller: z.boolean().optional(), resellerDiscountPercent: z.number().min(0).max(100).nullable().optional(), parentCustomerId: z.string().nullable().optional(), paymentTerms: z.string().nullable().optional(), currencyCode: z.string().max(8).nullable().optional(), taxExempt: z.boolean().optional(), creditHold: z.boolean().optional(), preferredAccount: z.boolean().optional(), strategicAccount: z.boolean().optional(), requiresApproval: z.boolean().optional(), blockedAccount: z.boolean().optional(), }); const crmListQuerySchema = z.object({ q: z.string().optional(), state: z.string().optional(), status: z.enum(crmRecordStatuses).optional(), lifecycleStage: z.enum(crmLifecycleStages).optional(), flag: z.enum(["PREFERRED", "STRATEGIC", "REQUIRES_APPROVAL", "BLOCKED"]).optional(), }); const crmContactEntrySchema = z.object({ type: z.enum(crmContactEntryTypes), summary: z.string().trim().min(1).max(160), body: z.string().trim().min(1).max(4000), contactAt: z.string().datetime(), }); const crmContactSchema = z.object({ fullName: z.string().trim().min(1).max(160), role: z.enum(crmContactRoles), email: z.string().trim().email(), phone: z.string().trim().min(1).max(64), isPrimary: z.boolean(), }); function getRouteParam(value: unknown) { return typeof value === "string" ? value : null; } export const crmRouter = Router(); crmRouter.get("/customers", requirePermissions([permissions.crmRead]), async (_request, response) => { const parsed = crmListQuerySchema.safeParse(_request.query); if (!parsed.success) { return fail(response, 400, "INVALID_INPUT", "CRM filters are invalid."); } return ok( response, await listCustomers({ query: parsed.data.q, status: parsed.data.status, state: parsed.data.state, lifecycleStage: parsed.data.lifecycleStage, flag: parsed.data.flag, }) ); }); crmRouter.get("/customers/hierarchy-options", requirePermissions([permissions.crmRead]), async (request, response) => { const excludeCustomerId = getRouteParam(request.query.excludeCustomerId); return ok(response, await listCustomerHierarchyOptions(excludeCustomerId ?? undefined)); }); crmRouter.get("/customers/:customerId", requirePermissions([permissions.crmRead]), async (request, response) => { const customerId = getRouteParam(request.params.customerId); if (!customerId) { return fail(response, 400, "INVALID_INPUT", "Customer id is invalid."); } const customer = await getCustomerById(customerId); if (!customer) { return fail(response, 404, "CRM_CUSTOMER_NOT_FOUND", "Customer record was not found."); } return ok(response, customer); }); crmRouter.post("/customers", requirePermissions([permissions.crmWrite]), async (request, response) => { const parsed = crmRecordSchema.safeParse(request.body); if (!parsed.success) { return fail(response, 400, "INVALID_INPUT", "Customer payload is invalid."); } const customer = await createCustomer(parsed.data, request.authUser?.id); if (!customer) { return fail(response, 400, "INVALID_INPUT", "Customer reseller relationship is invalid."); } return ok(response, customer, 201); }); crmRouter.put("/customers/:customerId", requirePermissions([permissions.crmWrite]), async (request, response) => { const customerId = getRouteParam(request.params.customerId); if (!customerId) { return fail(response, 400, "INVALID_INPUT", "Customer id is invalid."); } const parsed = crmRecordSchema.safeParse(request.body); if (!parsed.success) { return fail(response, 400, "INVALID_INPUT", "Customer payload is invalid."); } const existingCustomer = await getCustomerById(customerId); if (!existingCustomer) { return fail(response, 404, "CRM_CUSTOMER_NOT_FOUND", "Customer record was not found."); } const customer = await updateCustomer(customerId, parsed.data, request.authUser?.id); if (!customer) { return fail(response, 400, "INVALID_INPUT", "Customer reseller relationship is invalid."); } return ok(response, customer); }); crmRouter.post("/customers/:customerId/contact-history", requirePermissions([permissions.crmWrite]), async (request, response) => { const customerId = getRouteParam(request.params.customerId); if (!customerId) { return fail(response, 400, "INVALID_INPUT", "Customer id is invalid."); } const parsed = crmContactEntrySchema.safeParse(request.body); if (!parsed.success) { return fail(response, 400, "INVALID_INPUT", "Contact history entry is invalid."); } const entry = await createCustomerContactEntry(customerId, parsed.data, request.authUser?.id); if (!entry) { return fail(response, 404, "CRM_CUSTOMER_NOT_FOUND", "Customer record was not found."); } return ok(response, entry, 201); }); crmRouter.post("/customers/:customerId/contacts", requirePermissions([permissions.crmWrite]), async (request, response) => { const customerId = getRouteParam(request.params.customerId); if (!customerId) { return fail(response, 400, "INVALID_INPUT", "Customer id is invalid."); } const parsed = crmContactSchema.safeParse(request.body); if (!parsed.success) { return fail(response, 400, "INVALID_INPUT", "CRM contact is invalid."); } const contact = await createCustomerContact(customerId, parsed.data, request.authUser?.id); if (!contact) { return fail(response, 404, "CRM_CUSTOMER_NOT_FOUND", "Customer record was not found."); } return ok(response, contact, 201); }); crmRouter.get("/vendors", requirePermissions([permissions.crmRead]), async (_request, response) => { const parsed = crmListQuerySchema.safeParse(_request.query); if (!parsed.success) { return fail(response, 400, "INVALID_INPUT", "CRM filters are invalid."); } return ok( response, await listVendors({ query: parsed.data.q, status: parsed.data.status, state: parsed.data.state, lifecycleStage: parsed.data.lifecycleStage, flag: parsed.data.flag, }) ); }); crmRouter.get("/vendors/:vendorId", requirePermissions([permissions.crmRead]), async (request, response) => { const vendorId = getRouteParam(request.params.vendorId); if (!vendorId) { return fail(response, 400, "INVALID_INPUT", "Vendor id is invalid."); } const vendor = await getVendorById(vendorId); if (!vendor) { return fail(response, 404, "CRM_VENDOR_NOT_FOUND", "Vendor record was not found."); } return ok(response, vendor); }); crmRouter.post("/vendors", requirePermissions([permissions.crmWrite]), async (request, response) => { const parsed = crmRecordSchema.safeParse(request.body); if (!parsed.success) { return fail(response, 400, "INVALID_INPUT", "Vendor payload is invalid."); } return ok(response, await createVendor(parsed.data, request.authUser?.id), 201); }); crmRouter.put("/vendors/:vendorId", requirePermissions([permissions.crmWrite]), async (request, response) => { const vendorId = getRouteParam(request.params.vendorId); if (!vendorId) { return fail(response, 400, "INVALID_INPUT", "Vendor id is invalid."); } const parsed = crmRecordSchema.safeParse(request.body); if (!parsed.success) { return fail(response, 400, "INVALID_INPUT", "Vendor payload is invalid."); } const vendor = await updateVendor(vendorId, parsed.data, request.authUser?.id); if (!vendor) { return fail(response, 404, "CRM_VENDOR_NOT_FOUND", "Vendor record was not found."); } return ok(response, vendor); }); crmRouter.post("/vendors/:vendorId/contact-history", requirePermissions([permissions.crmWrite]), async (request, response) => { const vendorId = getRouteParam(request.params.vendorId); if (!vendorId) { return fail(response, 400, "INVALID_INPUT", "Vendor id is invalid."); } const parsed = crmContactEntrySchema.safeParse(request.body); if (!parsed.success) { return fail(response, 400, "INVALID_INPUT", "Contact history entry is invalid."); } const entry = await createVendorContactEntry(vendorId, parsed.data, request.authUser?.id); if (!entry) { return fail(response, 404, "CRM_VENDOR_NOT_FOUND", "Vendor record was not found."); } return ok(response, entry, 201); }); crmRouter.post("/vendors/:vendorId/contacts", requirePermissions([permissions.crmWrite]), async (request, response) => { const vendorId = getRouteParam(request.params.vendorId); if (!vendorId) { return fail(response, 400, "INVALID_INPUT", "Vendor id is invalid."); } const parsed = crmContactSchema.safeParse(request.body); if (!parsed.success) { return fail(response, 400, "INVALID_INPUT", "CRM contact is invalid."); } const contact = await createVendorContact(vendorId, parsed.data, request.authUser?.id); if (!contact) { return fail(response, 404, "CRM_VENDOR_NOT_FOUND", "Vendor record was not found."); } return ok(response, contact, 201); });