backfill from projects

This commit is contained in:
2026-03-18 11:54:22 -05:00
parent c18de77640
commit f12744f05d
15 changed files with 281 additions and 9 deletions

View File

@@ -126,6 +126,11 @@ type PurchaseOrderRevisionRecord = {
type PurchaseOrderRecord = {
id: string;
documentNumber: string;
project: {
id: string;
projectNumber: string;
name: string;
} | null;
status: string;
issueDate: Date;
taxPercent: number;
@@ -145,6 +150,17 @@ type PurchaseOrderRecord = {
revisions: PurchaseOrderRevisionRecord[];
};
type NormalizedPurchaseLine = {
itemId: string;
salesOrderId: string | null;
salesOrderLineId: string | null;
description: string;
quantity: number;
unitOfMeasure: PurchaseLineInput["unitOfMeasure"];
unitCost: number;
position: number;
};
function roundMoney(value: number) {
return Math.round(value * 100) / 100;
}
@@ -322,6 +338,63 @@ async function validateLines(lines: PurchaseLineInput[]) {
return { ok: true as const, lines: normalized };
}
async function resolvePurchaseOrderProjectId(projectId: string | null | undefined, lines: NormalizedPurchaseLine[]) {
let explicitProjectId = projectId ?? null;
if (explicitProjectId) {
const project = await prisma.project.findUnique({
where: { id: explicitProjectId },
select: {
id: true,
salesOrderId: true,
},
});
if (!project) {
return { ok: false as const, reason: "Linked project was not found." };
}
const linkedSalesOrderIds = [...new Set(lines.flatMap((line) => (line.salesOrderId ? [line.salesOrderId] : [])))];
if (linkedSalesOrderIds.length > 0 && project.salesOrderId && linkedSalesOrderIds.some((salesOrderId) => salesOrderId !== project.salesOrderId)) {
return { ok: false as const, reason: "Linked project does not match the sales-order demand attached to this purchase order." };
}
return { ok: true as const, projectId: project.id };
}
const linkedSalesOrderIds = [...new Set(lines.flatMap((line) => (line.salesOrderId ? [line.salesOrderId] : [])))];
if (linkedSalesOrderIds.length === 0) {
return { ok: true as const, projectId: null };
}
const matchingProjects = await prisma.project.findMany({
where: {
salesOrderId: {
in: linkedSalesOrderIds,
},
},
select: {
id: true,
salesOrderId: true,
createdAt: true,
},
orderBy: [{ createdAt: "asc" }],
});
const projectBySalesOrderId = new Map<string, string>();
for (const project of matchingProjects) {
if (project.salesOrderId && !projectBySalesOrderId.has(project.salesOrderId)) {
projectBySalesOrderId.set(project.salesOrderId, project.id);
}
}
const derivedProjectIds = [...new Set(linkedSalesOrderIds.map((salesOrderId) => projectBySalesOrderId.get(salesOrderId)).filter((value): value is string => Boolean(value)))];
if (derivedProjectIds.length > 1) {
return { ok: false as const, reason: "Purchase orders can only auto-link to one project. Split the document or set the project intentionally." };
}
return { ok: true as const, projectId: derivedProjectIds[0] ?? null };
}
function mapPurchaseOrder(record: PurchaseOrderRecord): PurchaseOrderDetailDto {
const receivedByLineId = new Map<string, number>();
@@ -370,6 +443,9 @@ function mapPurchaseOrder(record: PurchaseOrderRecord): PurchaseOrderDetailDto {
documentNumber: record.documentNumber,
vendorId: record.vendor.id,
vendorName: record.vendor.name,
projectId: record.project?.id ?? null,
projectNumber: record.project?.projectNumber ?? null,
projectName: record.project?.name ?? null,
vendorEmail: record.vendor.email,
paymentTerms: record.vendor.paymentTerms,
currencyCode: record.vendor.currencyCode,
@@ -487,6 +563,13 @@ const purchaseOrderInclude = Prisma.validator<Prisma.PurchaseOrderInclude>()({
currencyCode: true,
},
},
project: {
select: {
id: true,
projectNumber: true,
name: true,
},
},
lines: {
include: {
item: {
@@ -715,6 +798,9 @@ export async function listPurchaseOrders(filters: { q?: string; status?: Purchas
documentNumber: detail.documentNumber,
vendorId: detail.vendorId,
vendorName: detail.vendorName,
projectId: detail.projectId,
projectNumber: detail.projectNumber,
projectName: detail.projectName,
status: detail.status,
subtotal: detail.subtotal,
taxPercent: detail.taxPercent,
@@ -762,6 +848,11 @@ export async function createPurchaseOrder(payload: PurchaseOrderInput, actorId?:
return { ok: false as const, reason: validatedLines.reason };
}
const resolvedProject = await resolvePurchaseOrderProjectId(payload.projectId, validatedLines.lines);
if (!resolvedProject.ok) {
return { ok: false as const, reason: resolvedProject.reason };
}
const vendor = await prisma.vendor.findUnique({
where: { id: payload.vendorId },
select: { id: true },
@@ -777,6 +868,7 @@ export async function createPurchaseOrder(payload: PurchaseOrderInput, actorId?:
data: {
documentNumber,
vendorId: payload.vendorId,
projectId: resolvedProject.projectId,
status: payload.status,
issueDate: new Date(payload.issueDate),
taxPercent: payload.taxPercent,
@@ -824,6 +916,11 @@ export async function updatePurchaseOrder(documentId: string, payload: PurchaseO
return { ok: false as const, reason: validatedLines.reason };
}
const resolvedProject = await resolvePurchaseOrderProjectId(payload.projectId, validatedLines.lines);
if (!resolvedProject.ok) {
return { ok: false as const, reason: resolvedProject.reason };
}
const vendor = await prisma.vendor.findUnique({
where: { id: payload.vendorId },
select: { id: true },
@@ -837,6 +934,7 @@ export async function updatePurchaseOrder(documentId: string, payload: PurchaseO
where: { id: documentId },
data: {
vendorId: payload.vendorId,
projectId: resolvedProject.projectId,
status: payload.status,
issueDate: new Date(payload.issueDate),
taxPercent: payload.taxPercent,