crm4
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
ALTER TABLE "Customer" ADD COLUMN "isReseller" BOOLEAN NOT NULL DEFAULT false;
|
||||
|
||||
ALTER TABLE "Customer" ADD COLUMN "resellerDiscountPercent" REAL NOT NULL DEFAULT 0;
|
||||
|
||||
ALTER TABLE "Customer" ADD COLUMN "parentCustomerId" TEXT REFERENCES "Customer" ("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
CREATE INDEX "Customer_parentCustomerId_idx" ON "Customer"("parentCustomerId");
|
||||
@@ -0,0 +1,27 @@
|
||||
ALTER TABLE "Customer" ADD COLUMN "paymentTerms" TEXT;
|
||||
ALTER TABLE "Customer" ADD COLUMN "currencyCode" TEXT DEFAULT 'USD';
|
||||
ALTER TABLE "Customer" ADD COLUMN "taxExempt" BOOLEAN NOT NULL DEFAULT false;
|
||||
ALTER TABLE "Customer" ADD COLUMN "creditHold" BOOLEAN NOT NULL DEFAULT false;
|
||||
|
||||
ALTER TABLE "Vendor" ADD COLUMN "paymentTerms" TEXT;
|
||||
ALTER TABLE "Vendor" ADD COLUMN "currencyCode" TEXT DEFAULT 'USD';
|
||||
ALTER TABLE "Vendor" ADD COLUMN "taxExempt" BOOLEAN NOT NULL DEFAULT false;
|
||||
ALTER TABLE "Vendor" ADD COLUMN "creditHold" BOOLEAN NOT NULL DEFAULT false;
|
||||
|
||||
CREATE TABLE "CrmContact" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"fullName" TEXT NOT NULL,
|
||||
"role" TEXT NOT NULL DEFAULT 'OTHER',
|
||||
"email" TEXT NOT NULL,
|
||||
"phone" TEXT NOT NULL,
|
||||
"isPrimary" BOOLEAN NOT NULL DEFAULT false,
|
||||
"customerId" TEXT,
|
||||
"vendorId" TEXT,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" DATETIME NOT NULL,
|
||||
CONSTRAINT "CrmContact_customerId_fkey" FOREIGN KEY ("customerId") REFERENCES "Customer" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
CONSTRAINT "CrmContact_vendorId_fkey" FOREIGN KEY ("vendorId") REFERENCES "Vendor" ("id") ON DELETE CASCADE ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX "CrmContact_customerId_idx" ON "CrmContact"("customerId");
|
||||
CREATE INDEX "CrmContact_vendorId_idx" ON "CrmContact"("vendorId");
|
||||
@@ -113,10 +113,20 @@ model Customer {
|
||||
postalCode String
|
||||
country String
|
||||
status String @default("ACTIVE")
|
||||
isReseller Boolean @default(false)
|
||||
resellerDiscountPercent Float @default(0)
|
||||
parentCustomerId String?
|
||||
paymentTerms String?
|
||||
currencyCode String? @default("USD")
|
||||
taxExempt Boolean @default(false)
|
||||
creditHold Boolean @default(false)
|
||||
notes String
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
contactEntries CrmContactEntry[]
|
||||
contacts CrmContact[]
|
||||
parentCustomer Customer? @relation("CustomerHierarchy", fields: [parentCustomerId], references: [id], onDelete: SetNull)
|
||||
childCustomers Customer[] @relation("CustomerHierarchy")
|
||||
}
|
||||
|
||||
model Vendor {
|
||||
@@ -131,10 +141,15 @@ model Vendor {
|
||||
postalCode String
|
||||
country String
|
||||
status String @default("ACTIVE")
|
||||
paymentTerms String?
|
||||
currencyCode String? @default("USD")
|
||||
taxExempt Boolean @default(false)
|
||||
creditHold Boolean @default(false)
|
||||
notes String
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
contactEntries CrmContactEntry[]
|
||||
contacts CrmContact[]
|
||||
}
|
||||
|
||||
model CrmContactEntry {
|
||||
@@ -152,3 +167,18 @@ model CrmContactEntry {
|
||||
vendor Vendor? @relation(fields: [vendorId], references: [id], onDelete: Cascade)
|
||||
createdBy User? @relation(fields: [createdById], references: [id], onDelete: SetNull)
|
||||
}
|
||||
|
||||
model CrmContact {
|
||||
id String @id @default(cuid())
|
||||
fullName String
|
||||
role String @default("OTHER")
|
||||
email String
|
||||
phone String
|
||||
isPrimary Boolean @default(false)
|
||||
customerId String?
|
||||
vendorId String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
customer Customer? @relation(fields: [customerId], references: [id], onDelete: Cascade)
|
||||
vendor Vendor? @relation(fields: [vendorId], references: [id], onDelete: Cascade)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { permissions } from "@mrp/shared";
|
||||
import { crmContactEntryTypes, crmRecordStatuses } from "@mrp/shared/dist/crm/types.js";
|
||||
import { crmContactEntryTypes, crmContactRoles, crmRecordStatuses } from "@mrp/shared/dist/crm/types.js";
|
||||
import { Router } from "express";
|
||||
import { z } from "zod";
|
||||
|
||||
@@ -7,12 +7,15 @@ 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,
|
||||
@@ -30,6 +33,13 @@ const crmRecordSchema = z.object({
|
||||
country: z.string().trim().min(1),
|
||||
status: z.enum(crmRecordStatuses),
|
||||
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(),
|
||||
});
|
||||
|
||||
const crmListQuerySchema = z.object({
|
||||
@@ -45,7 +55,15 @@ const crmContactEntrySchema = z.object({
|
||||
contactAt: z.string().datetime(),
|
||||
});
|
||||
|
||||
function getRouteParam(value: string | string[] | undefined) {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -67,6 +85,11 @@ crmRouter.get("/customers", requirePermissions([permissions.crmRead]), async (_r
|
||||
);
|
||||
});
|
||||
|
||||
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) {
|
||||
@@ -87,7 +110,12 @@ crmRouter.post("/customers", requirePermissions([permissions.crmWrite]), async (
|
||||
return fail(response, 400, "INVALID_INPUT", "Customer payload is invalid.");
|
||||
}
|
||||
|
||||
return ok(response, await createCustomer(parsed.data), 201);
|
||||
const customer = await createCustomer(parsed.data);
|
||||
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) => {
|
||||
@@ -101,9 +129,14 @@ crmRouter.put("/customers/:customerId", requirePermissions([permissions.crmWrite
|
||||
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);
|
||||
if (!customer) {
|
||||
return fail(response, 404, "CRM_CUSTOMER_NOT_FOUND", "Customer record was not found.");
|
||||
return fail(response, 400, "INVALID_INPUT", "Customer reseller relationship is invalid.");
|
||||
}
|
||||
|
||||
return ok(response, customer);
|
||||
@@ -128,6 +161,25 @@ crmRouter.post("/customers/:customerId/contact-history", requirePermissions([per
|
||||
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);
|
||||
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) {
|
||||
@@ -204,3 +256,22 @@ crmRouter.post("/vendors/:vendorId/contact-history", requirePermissions([permiss
|
||||
|
||||
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);
|
||||
if (!contact) {
|
||||
return fail(response, 404, "CRM_VENDOR_NOT_FOUND", "Vendor record was not found.");
|
||||
}
|
||||
|
||||
return ok(response, contact, 201);
|
||||
});
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import type {
|
||||
CrmContactDto,
|
||||
CrmContactInput,
|
||||
CrmContactRole,
|
||||
CrmContactEntryDto,
|
||||
CrmContactEntryInput,
|
||||
CrmContactEntryType,
|
||||
CrmCustomerChildDto,
|
||||
CrmRecordDetailDto,
|
||||
CrmRecordInput,
|
||||
CrmRecordStatus,
|
||||
@@ -37,6 +41,88 @@ function mapDetail(record: Customer | Vendor): CrmRecordDetailDto {
|
||||
};
|
||||
}
|
||||
|
||||
type CustomerSummaryRecord = Customer & {
|
||||
parentCustomer: Pick<Customer, "id" | "name"> | null;
|
||||
};
|
||||
|
||||
type CustomerDetailedRecord = Customer & {
|
||||
parentCustomer: Pick<Customer, "id" | "name"> | null;
|
||||
childCustomers: Pick<Customer, "id" | "name" | "status">[];
|
||||
contactEntries: ContactEntryWithAuthor[];
|
||||
contacts: ContactRecord[];
|
||||
};
|
||||
|
||||
type VendorDetailedRecord = Vendor & {
|
||||
contactEntries: ContactEntryWithAuthor[];
|
||||
contacts: ContactRecord[];
|
||||
};
|
||||
|
||||
type ContactRecord = {
|
||||
id: string;
|
||||
fullName: string;
|
||||
role: string;
|
||||
email: string;
|
||||
phone: string;
|
||||
isPrimary: boolean;
|
||||
createdAt: Date;
|
||||
};
|
||||
|
||||
function mapCustomerChild(record: Pick<Customer, "id" | "name" | "status">): CrmCustomerChildDto {
|
||||
return {
|
||||
id: record.id,
|
||||
name: record.name,
|
||||
status: record.status as CrmRecordStatus,
|
||||
};
|
||||
}
|
||||
|
||||
function mapCustomerSummary(record: CustomerSummaryRecord): CrmRecordSummaryDto {
|
||||
return {
|
||||
...mapSummary(record),
|
||||
isReseller: record.isReseller,
|
||||
parentCustomerId: record.parentCustomer?.id ?? null,
|
||||
parentCustomerName: record.parentCustomer?.name ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
function mapCustomerDetail(record: CustomerDetailedRecord): CrmRecordDetailDto {
|
||||
return {
|
||||
...mapDetailedRecord(record),
|
||||
isReseller: record.isReseller,
|
||||
resellerDiscountPercent: record.resellerDiscountPercent,
|
||||
parentCustomerId: record.parentCustomer?.id ?? null,
|
||||
parentCustomerName: record.parentCustomer?.name ?? null,
|
||||
childCustomers: record.childCustomers.map(mapCustomerChild),
|
||||
paymentTerms: record.paymentTerms,
|
||||
currencyCode: record.currencyCode,
|
||||
taxExempt: record.taxExempt,
|
||||
creditHold: record.creditHold,
|
||||
contacts: record.contacts.map(mapCrmContact),
|
||||
};
|
||||
}
|
||||
|
||||
function mapCrmContact(record: ContactRecord): CrmContactDto {
|
||||
return {
|
||||
id: record.id,
|
||||
fullName: record.fullName,
|
||||
role: record.role as CrmContactRole,
|
||||
email: record.email,
|
||||
phone: record.phone,
|
||||
isPrimary: record.isPrimary,
|
||||
createdAt: record.createdAt.toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
function mapVendorDetail(record: VendorDetailedRecord): CrmRecordDetailDto {
|
||||
return {
|
||||
...mapDetailedRecord(record),
|
||||
paymentTerms: record.paymentTerms,
|
||||
currencyCode: record.currencyCode,
|
||||
taxExempt: record.taxExempt,
|
||||
creditHold: record.creditHold,
|
||||
contacts: record.contacts.map(mapCrmContact),
|
||||
};
|
||||
}
|
||||
|
||||
type ContactEntryWithAuthor = {
|
||||
id: string;
|
||||
type: string;
|
||||
@@ -120,16 +206,43 @@ function buildWhereClause(filters: CrmListFilters) {
|
||||
export async function listCustomers(filters: CrmListFilters = {}) {
|
||||
const customers = await prisma.customer.findMany({
|
||||
where: buildWhereClause(filters),
|
||||
include: {
|
||||
parentCustomer: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: { name: "asc" },
|
||||
});
|
||||
|
||||
return customers.map(mapSummary);
|
||||
return customers.map(mapCustomerSummary);
|
||||
}
|
||||
|
||||
export async function getCustomerById(customerId: string) {
|
||||
const customer = await prisma.customer.findUnique({
|
||||
where: { id: customerId },
|
||||
include: {
|
||||
parentCustomer: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
childCustomers: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
status: true,
|
||||
},
|
||||
orderBy: {
|
||||
name: "asc",
|
||||
},
|
||||
},
|
||||
contacts: {
|
||||
orderBy: [{ isPrimary: "desc" }, { fullName: "asc" }],
|
||||
},
|
||||
contactEntries: {
|
||||
include: {
|
||||
createdBy: true,
|
||||
@@ -139,15 +252,56 @@ export async function getCustomerById(customerId: string) {
|
||||
},
|
||||
});
|
||||
|
||||
return customer ? mapDetailedRecord(customer) : null;
|
||||
return customer ? mapCustomerDetail(customer) : null;
|
||||
}
|
||||
|
||||
export async function createCustomer(payload: CrmRecordInput) {
|
||||
if (payload.parentCustomerId) {
|
||||
const parentCustomer = await prisma.customer.findUnique({
|
||||
where: { id: payload.parentCustomerId },
|
||||
});
|
||||
|
||||
if (!parentCustomer) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const customer = await prisma.customer.create({
|
||||
data: payload,
|
||||
data: {
|
||||
name: payload.name,
|
||||
email: payload.email,
|
||||
phone: payload.phone,
|
||||
addressLine1: payload.addressLine1,
|
||||
addressLine2: payload.addressLine2,
|
||||
city: payload.city,
|
||||
state: payload.state,
|
||||
postalCode: payload.postalCode,
|
||||
country: payload.country,
|
||||
status: payload.status,
|
||||
notes: payload.notes,
|
||||
isReseller: payload.isReseller ?? false,
|
||||
resellerDiscountPercent: payload.resellerDiscountPercent ?? 0,
|
||||
parentCustomerId: payload.parentCustomerId ?? null,
|
||||
paymentTerms: payload.paymentTerms ?? null,
|
||||
currencyCode: payload.currencyCode ?? "USD",
|
||||
taxExempt: payload.taxExempt ?? false,
|
||||
creditHold: payload.creditHold ?? false,
|
||||
},
|
||||
});
|
||||
|
||||
return mapDetail(customer);
|
||||
return {
|
||||
...mapDetail(customer),
|
||||
isReseller: customer.isReseller,
|
||||
resellerDiscountPercent: customer.resellerDiscountPercent,
|
||||
parentCustomerId: customer.parentCustomerId,
|
||||
parentCustomerName: null,
|
||||
childCustomers: [],
|
||||
paymentTerms: customer.paymentTerms,
|
||||
currencyCode: customer.currencyCode,
|
||||
taxExempt: customer.taxExempt,
|
||||
creditHold: customer.creditHold,
|
||||
contacts: [],
|
||||
};
|
||||
}
|
||||
|
||||
export async function updateCustomer(customerId: string, payload: CrmRecordInput) {
|
||||
@@ -159,12 +313,57 @@ export async function updateCustomer(customerId: string, payload: CrmRecordInput
|
||||
return null;
|
||||
}
|
||||
|
||||
if (payload.parentCustomerId === customerId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (payload.parentCustomerId) {
|
||||
const parentCustomer = await prisma.customer.findUnique({
|
||||
where: { id: payload.parentCustomerId },
|
||||
});
|
||||
|
||||
if (!parentCustomer) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const customer = await prisma.customer.update({
|
||||
where: { id: customerId },
|
||||
data: payload,
|
||||
data: {
|
||||
name: payload.name,
|
||||
email: payload.email,
|
||||
phone: payload.phone,
|
||||
addressLine1: payload.addressLine1,
|
||||
addressLine2: payload.addressLine2,
|
||||
city: payload.city,
|
||||
state: payload.state,
|
||||
postalCode: payload.postalCode,
|
||||
country: payload.country,
|
||||
status: payload.status,
|
||||
notes: payload.notes,
|
||||
isReseller: payload.isReseller ?? false,
|
||||
resellerDiscountPercent: payload.resellerDiscountPercent ?? 0,
|
||||
parentCustomerId: payload.parentCustomerId ?? null,
|
||||
paymentTerms: payload.paymentTerms ?? null,
|
||||
currencyCode: payload.currencyCode ?? "USD",
|
||||
taxExempt: payload.taxExempt ?? false,
|
||||
creditHold: payload.creditHold ?? false,
|
||||
},
|
||||
});
|
||||
|
||||
return mapDetail(customer);
|
||||
return {
|
||||
...mapDetail(customer),
|
||||
isReseller: customer.isReseller,
|
||||
resellerDiscountPercent: customer.resellerDiscountPercent,
|
||||
parentCustomerId: customer.parentCustomerId,
|
||||
parentCustomerName: null,
|
||||
childCustomers: [],
|
||||
paymentTerms: customer.paymentTerms,
|
||||
currencyCode: customer.currencyCode,
|
||||
taxExempt: customer.taxExempt,
|
||||
creditHold: customer.creditHold,
|
||||
contacts: [],
|
||||
};
|
||||
}
|
||||
|
||||
export async function listVendors(filters: CrmListFilters = {}) {
|
||||
@@ -176,10 +375,44 @@ export async function listVendors(filters: CrmListFilters = {}) {
|
||||
return vendors.map(mapSummary);
|
||||
}
|
||||
|
||||
export async function listCustomerHierarchyOptions(excludeCustomerId?: string) {
|
||||
const customers = await prisma.customer.findMany({
|
||||
where: excludeCustomerId
|
||||
? {
|
||||
isReseller: true,
|
||||
id: {
|
||||
not: excludeCustomerId,
|
||||
},
|
||||
}
|
||||
: {
|
||||
isReseller: true,
|
||||
},
|
||||
orderBy: {
|
||||
name: "asc",
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
status: true,
|
||||
isReseller: true,
|
||||
},
|
||||
});
|
||||
|
||||
return customers.map((customer) => ({
|
||||
id: customer.id,
|
||||
name: customer.name,
|
||||
status: customer.status as CrmRecordStatus,
|
||||
isReseller: customer.isReseller,
|
||||
}));
|
||||
}
|
||||
|
||||
export async function getVendorById(vendorId: string) {
|
||||
const vendor = await prisma.vendor.findUnique({
|
||||
where: { id: vendorId },
|
||||
include: {
|
||||
contacts: {
|
||||
orderBy: [{ isPrimary: "desc" }, { fullName: "asc" }],
|
||||
},
|
||||
contactEntries: {
|
||||
include: {
|
||||
createdBy: true,
|
||||
@@ -189,15 +422,38 @@ export async function getVendorById(vendorId: string) {
|
||||
},
|
||||
});
|
||||
|
||||
return vendor ? mapDetailedRecord(vendor) : null;
|
||||
return vendor ? mapVendorDetail(vendor) : null;
|
||||
}
|
||||
|
||||
export async function createVendor(payload: CrmRecordInput) {
|
||||
const vendor = await prisma.vendor.create({
|
||||
data: payload,
|
||||
data: {
|
||||
name: payload.name,
|
||||
email: payload.email,
|
||||
phone: payload.phone,
|
||||
addressLine1: payload.addressLine1,
|
||||
addressLine2: payload.addressLine2,
|
||||
city: payload.city,
|
||||
state: payload.state,
|
||||
postalCode: payload.postalCode,
|
||||
country: payload.country,
|
||||
status: payload.status,
|
||||
notes: payload.notes,
|
||||
paymentTerms: payload.paymentTerms ?? null,
|
||||
currencyCode: payload.currencyCode ?? "USD",
|
||||
taxExempt: payload.taxExempt ?? false,
|
||||
creditHold: payload.creditHold ?? false,
|
||||
},
|
||||
});
|
||||
|
||||
return mapDetail(vendor);
|
||||
return {
|
||||
...mapDetail(vendor),
|
||||
paymentTerms: vendor.paymentTerms,
|
||||
currencyCode: vendor.currencyCode,
|
||||
taxExempt: vendor.taxExempt,
|
||||
creditHold: vendor.creditHold,
|
||||
contacts: [],
|
||||
};
|
||||
}
|
||||
|
||||
export async function updateVendor(vendorId: string, payload: CrmRecordInput) {
|
||||
@@ -211,10 +467,33 @@ export async function updateVendor(vendorId: string, payload: CrmRecordInput) {
|
||||
|
||||
const vendor = await prisma.vendor.update({
|
||||
where: { id: vendorId },
|
||||
data: payload,
|
||||
data: {
|
||||
name: payload.name,
|
||||
email: payload.email,
|
||||
phone: payload.phone,
|
||||
addressLine1: payload.addressLine1,
|
||||
addressLine2: payload.addressLine2,
|
||||
city: payload.city,
|
||||
state: payload.state,
|
||||
postalCode: payload.postalCode,
|
||||
country: payload.country,
|
||||
status: payload.status,
|
||||
notes: payload.notes,
|
||||
paymentTerms: payload.paymentTerms ?? null,
|
||||
currencyCode: payload.currencyCode ?? "USD",
|
||||
taxExempt: payload.taxExempt ?? false,
|
||||
creditHold: payload.creditHold ?? false,
|
||||
},
|
||||
});
|
||||
|
||||
return mapDetail(vendor);
|
||||
return {
|
||||
...mapDetail(vendor),
|
||||
paymentTerms: vendor.paymentTerms,
|
||||
currencyCode: vendor.currencyCode,
|
||||
taxExempt: vendor.taxExempt,
|
||||
creditHold: vendor.creditHold,
|
||||
contacts: [],
|
||||
};
|
||||
}
|
||||
|
||||
export async function createCustomerContactEntry(customerId: string, payload: CrmContactEntryInput, createdById?: string) {
|
||||
@@ -268,3 +547,63 @@ export async function createVendorContactEntry(vendorId: string, payload: CrmCon
|
||||
|
||||
return mapContactEntry(entry);
|
||||
}
|
||||
|
||||
export async function createCustomerContact(customerId: string, payload: CrmContactInput) {
|
||||
const existingCustomer = await prisma.customer.findUnique({
|
||||
where: { id: customerId },
|
||||
});
|
||||
|
||||
if (!existingCustomer) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (payload.isPrimary) {
|
||||
await prisma.crmContact.updateMany({
|
||||
where: { customerId, isPrimary: true },
|
||||
data: { isPrimary: false },
|
||||
});
|
||||
}
|
||||
|
||||
const contact = await prisma.crmContact.create({
|
||||
data: {
|
||||
fullName: payload.fullName,
|
||||
role: payload.role,
|
||||
email: payload.email,
|
||||
phone: payload.phone,
|
||||
isPrimary: payload.isPrimary,
|
||||
customerId,
|
||||
},
|
||||
});
|
||||
|
||||
return mapCrmContact(contact);
|
||||
}
|
||||
|
||||
export async function createVendorContact(vendorId: string, payload: CrmContactInput) {
|
||||
const existingVendor = await prisma.vendor.findUnique({
|
||||
where: { id: vendorId },
|
||||
});
|
||||
|
||||
if (!existingVendor) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (payload.isPrimary) {
|
||||
await prisma.crmContact.updateMany({
|
||||
where: { vendorId, isPrimary: true },
|
||||
data: { isPrimary: false },
|
||||
});
|
||||
}
|
||||
|
||||
const contact = await prisma.crmContact.create({
|
||||
data: {
|
||||
fullName: payload.fullName,
|
||||
role: payload.role,
|
||||
email: payload.email,
|
||||
phone: payload.phone,
|
||||
isPrimary: payload.isPrimary,
|
||||
vendorId,
|
||||
},
|
||||
});
|
||||
|
||||
return mapCrmContact(contact);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user