all the haul

This commit is contained in:
2026-04-07 23:56:31 +00:00
parent 87cce83030
commit 24f303355b
244 changed files with 33743 additions and 11249 deletions
@@ -0,0 +1,609 @@
import { describe, expect, test } from "bun:test";
import { SkipRowError } from "../types";
import { createTranslationContext, type TranslationContext } from "../context";
// ─── Testonly translator helper ──────────────────────────────────────────
// We cannot import Translation<kF,kT,kC> generically, but every translator
// exposes a `.values` array. We walk the entries exactly the way the real
// `translateRow` function does.
type AnyTranslation = {
values: Array<{
from: string;
to: string;
process?: (value: any, context: any, row: any) => any;
}>;
};
function translateRow(
row: Record<string, unknown>,
translation: AnyTranslation,
context: TranslationContext
): Record<string, unknown> {
const out: Record<string, unknown> = {};
for (const entry of translation.values) {
const input = row[entry.from];
if (input === undefined) continue;
out[entry.to] = entry.process
? entry.process(input, context, row)
: input;
}
return out;
}
function expectSkipRow(fn: () => void): string {
try {
fn();
throw new Error("Expected SkipRowError but nothing was thrown");
} catch (err) {
expect(err).toBeInstanceOf(SkipRowError);
return (err as SkipRowError).message;
}
}
// ─── TranslationContext factory ───────────────────────────────────────────
function makeContext(overrides?: Partial<TranslationContext>): TranslationContext {
const ctx = createTranslationContext();
if (overrides) Object.assign(ctx, overrides);
return ctx;
}
// ═══════════════════════════════════════════════════════════════════════════
// User Translator
// ═══════════════════════════════════════════════════════════════════════════
describe("userTranslation", () => {
// Lazyload to sidestep toplevel Prisma import issues in test env.
let userTranslation: AnyTranslation;
test("module loads", async () => {
const mod = await import("../user");
userTranslation = mod.userTranslation as unknown as AnyTranslation;
expect(userTranslation).toBeDefined();
});
test("valid email is normalised", async () => {
const mod = await import("../user");
userTranslation = mod.userTranslation as unknown as AnyTranslation;
const ctx = makeContext();
const row = {
emailAddress: " John@Example.COM ",
firstName: "John",
lastName: "Doe",
memberRecId: 42,
memberId: "jdoe",
memberClass: "F",
inactiveFlag: false,
dateHire: new Date("2024-01-01"),
lastUpdatedUtc: new Date("2024-06-01"),
};
const result = translateRow(row, userTranslation, ctx);
expect(result.login).toBe("jdoe@cw.local");
expect(result.email).toBe("john@example.com");
expect(result.firstName).toBe("John");
expect(result.active).toBe(true);
expect(result.cwMemberId).toBe(42);
expect(result.cwIdentifier).toBe("jdoe");
});
test("missing email generates placeholder", async () => {
const mod = await import("../user");
userTranslation = mod.userTranslation as unknown as AnyTranslation;
const ctx = makeContext();
const result = translateRow(
{ emailAddress: null, firstName: "A", lastName: "B", memberRecId: 1, memberId: "a", memberClass: "F", inactiveFlag: false, lastUpdatedUtc: new Date(), dateHire: null },
userTranslation,
ctx
);
expect(result.email).toBe("a@cw.local");
});
test("email without @ generates placeholder", async () => {
const mod = await import("../user");
userTranslation = mod.userTranslation as unknown as AnyTranslation;
const ctx = makeContext();
const result = translateRow(
{ emailAddress: "nodomain", firstName: "A", lastName: "B", memberRecId: 1, memberId: "a", memberClass: "F", inactiveFlag: false, lastUpdatedUtc: new Date(), dateHire: null },
userTranslation,
ctx
);
expect(result.email).toBe("a@cw.local");
});
test("empty firstName falls back to Unknown", async () => {
const mod = await import("../user");
userTranslation = mod.userTranslation as unknown as AnyTranslation;
const ctx = makeContext();
const result = translateRow(
{ emailAddress: "x@y.com", firstName: "", lastName: "", memberRecId: 1, memberId: "x", inactiveFlag: false, lastUpdatedUtc: new Date(), dateHire: null },
userTranslation,
ctx
);
expect(result.firstName).toBe("Unknown");
expect(result.lastName).toBe("Unknown");
});
test("inactiveFlag=true maps to active=false", async () => {
const mod = await import("../user");
userTranslation = mod.userTranslation as unknown as AnyTranslation;
const ctx = makeContext();
const result = translateRow(
{ emailAddress: "x@y.com", firstName: "X", lastName: "Y", memberRecId: 1, memberId: "x", inactiveFlag: true, lastUpdatedUtc: new Date(), dateHire: null },
userTranslation,
ctx
);
expect(result.active).toBe(false);
});
test("createdAt falls back to lastUpdatedUtc when dateHire is null", async () => {
const mod = await import("../user");
userTranslation = mod.userTranslation as unknown as AnyTranslation;
const ctx = makeContext();
const updated = new Date("2025-03-01");
const result = translateRow(
{ emailAddress: "x@y.com", firstName: "X", lastName: "Y", memberRecId: 1, memberId: "x", inactiveFlag: false, lastUpdatedUtc: updated, dateHire: null },
userTranslation,
ctx
);
expect(result.createdAt).toEqual(updated);
});
});
// ═══════════════════════════════════════════════════════════════════════════
// Opportunity Translator
// ═══════════════════════════════════════════════════════════════════════════
describe("opportunityTranslation", () => {
let opportunityTranslation: AnyTranslation;
test("module loads", async () => {
const mod = await import("../opportunity");
opportunityTranslation = mod.opportunityTranslation as unknown as AnyTranslation;
expect(opportunityTranslation).toBeDefined();
});
test("interest maps 1->COLD, 2->WARM, 3->HOT", async () => {
const mod = await import("../opportunity");
opportunityTranslation = mod.opportunityTranslation as unknown as AnyTranslation;
const ctx = makeContext({ defaultOpportunityTypeId: 1 });
for (const [input, expected] of [
[1, "COLD"],
[2, "WARM"],
[3, "HOT"],
] as const) {
const result = translateRow(
{ opportunityRecId: 100, soInterestRecId: input, soTypeRecId: 1, opportunityName: "T", notes: null, soPipelineRecId: null, soOppStatusRecId: null, probabilityToClose: 50, source: null, companyRecId: 1, contactRecId: 1, companyAddressRecId: null, poNumber: null, dateCloseExpected: null, datePipelineChange: null, dateBecameLead: null, dateClosed: null, oldCloseFlag: false, closedBy: null, updatedBy: "admin", enteredBy: "admin", dateBecameLeadUtc: new Date(), lastUpdatedUtc: new Date(), members: [] },
opportunityTranslation,
ctx
);
expect(result.interest).toBe(expected);
}
});
test("interest value 0 maps to COLD", async () => {
const mod = await import("../opportunity");
opportunityTranslation = mod.opportunityTranslation as unknown as AnyTranslation;
const ctx = makeContext({ defaultOpportunityTypeId: 1 });
const result = translateRow(
{ opportunityRecId: 100, soInterestRecId: 0, soTypeRecId: 1, opportunityName: "T", notes: null, soPipelineRecId: null, soOppStatusRecId: null, probabilityToClose: 0, source: null, companyRecId: 1, contactRecId: 1, companyAddressRecId: null, poNumber: null, dateCloseExpected: null, datePipelineChange: null, dateBecameLead: null, dateClosed: null, oldCloseFlag: false, closedBy: null, updatedBy: "admin", enteredBy: "admin", dateBecameLeadUtc: new Date(), lastUpdatedUtc: new Date(), members: [] },
opportunityTranslation,
ctx
);
expect(result.interest).toBe("COLD");
});
test("interest value 4+ maps to HOT", async () => {
const mod = await import("../opportunity");
opportunityTranslation = mod.opportunityTranslation as unknown as AnyTranslation;
const ctx = makeContext({ defaultOpportunityTypeId: 1 });
const result = translateRow(
{ opportunityRecId: 100, soInterestRecId: 4, soTypeRecId: 1, opportunityName: "T", notes: null, soPipelineRecId: null, soOppStatusRecId: null, probabilityToClose: 0, source: null, companyRecId: 1, contactRecId: 1, companyAddressRecId: null, poNumber: null, dateCloseExpected: null, datePipelineChange: null, dateBecameLead: null, dateClosed: null, oldCloseFlag: false, closedBy: null, updatedBy: "admin", enteredBy: "admin", dateBecameLeadUtc: new Date(), lastUpdatedUtc: new Date(), members: [] },
opportunityTranslation,
ctx
);
expect(result.interest).toBe("HOT");
});
test("null interest returns null", async () => {
const mod = await import("../opportunity");
opportunityTranslation = mod.opportunityTranslation as unknown as AnyTranslation;
const ctx = makeContext({ defaultOpportunityTypeId: 1 });
const result = translateRow(
{ opportunityRecId: 100, soInterestRecId: null, soTypeRecId: 1, opportunityName: "T", notes: null, soPipelineRecId: null, soOppStatusRecId: null, probabilityToClose: 0, source: null, companyRecId: 1, contactRecId: 1, companyAddressRecId: null, poNumber: null, dateCloseExpected: null, datePipelineChange: null, dateBecameLead: null, dateClosed: null, oldCloseFlag: false, closedBy: null, updatedBy: "admin", enteredBy: "admin", dateBecameLeadUtc: new Date(), lastUpdatedUtc: new Date(), members: [] },
opportunityTranslation,
ctx
);
expect(result.interest).toBeNull();
});
test("missing typeId with default fallback succeeds", async () => {
const mod = await import("../opportunity");
opportunityTranslation = mod.opportunityTranslation as unknown as AnyTranslation;
const ctx = makeContext({ defaultOpportunityTypeId: 99 });
const result = translateRow(
{ opportunityRecId: 100, soInterestRecId: null, soTypeRecId: null, opportunityName: "T", notes: null, soPipelineRecId: null, soOppStatusRecId: null, probabilityToClose: 0, source: null, companyRecId: 1, contactRecId: 1, companyAddressRecId: null, poNumber: null, dateCloseExpected: null, datePipelineChange: null, dateBecameLead: null, dateClosed: null, oldCloseFlag: false, closedBy: null, updatedBy: "admin", enteredBy: "admin", dateBecameLeadUtc: new Date(), lastUpdatedUtc: new Date(), members: [] },
opportunityTranslation,
ctx
);
expect(result.typeId).toBe(99);
});
test("missing typeId with no default triggers skipRow", async () => {
const mod = await import("../opportunity");
opportunityTranslation = mod.opportunityTranslation as unknown as AnyTranslation;
const ctx = makeContext({ defaultOpportunityTypeId: null });
expectSkipRow(() => {
translateRow(
{ opportunityRecId: 100, soInterestRecId: null, soTypeRecId: null, opportunityName: "T", notes: null, soPipelineRecId: null, soOppStatusRecId: null, probabilityToClose: 0, source: null, companyRecId: 1, contactRecId: 1, companyAddressRecId: null, poNumber: null, dateCloseExpected: null, datePipelineChange: null, dateBecameLead: null, dateClosed: null, oldCloseFlag: false, closedBy: null, updatedBy: "admin", enteredBy: "admin", dateBecameLeadUtc: new Date(), lastUpdatedUtc: new Date(), members: [] },
opportunityTranslation,
ctx
);
});
});
test("primary/secondary sales reps resolved from members", async () => {
const mod = await import("../opportunity");
opportunityTranslation = mod.opportunityTranslation as unknown as AnyTranslation;
const identMap = new Map<number, string>([[10, "jdoe"], [20, "jsmith"]]);
const ctx = makeContext({
defaultOpportunityTypeId: 1,
userIdentifiersByMemberRecId: identMap,
});
const result = translateRow(
{ opportunityRecId: 100, soInterestRecId: null, soTypeRecId: 1, opportunityName: "T", notes: null, soPipelineRecId: null, soOppStatusRecId: null, probabilityToClose: 0, source: null, companyRecId: 1, contactRecId: 1, companyAddressRecId: null, poNumber: null, dateCloseExpected: null, datePipelineChange: null, dateBecameLead: null, dateClosed: null, oldCloseFlag: false, closedBy: null, updatedBy: "admin", enteredBy: "admin", dateBecameLeadUtc: new Date(), lastUpdatedUtc: new Date(), members: [{ memberRecId: 10, primarySalesFlag: true, secondarySalesFlag: false }, { memberRecId: 20, primarySalesFlag: false, secondarySalesFlag: true }] },
opportunityTranslation,
ctx
);
expect(result.primarySalesRepId).toBe("jdoe");
expect(result.secondarySalesRepId).toBe("jsmith");
});
});
// ═══════════════════════════════════════════════════════════════════════════
// Service Ticket Translator
// ═══════════════════════════════════════════════════════════════════════════
describe("serviceTicketTranslation", () => {
let serviceTicketTranslation: AnyTranslation;
test("module loads", async () => {
const mod = await import("../service-ticket");
serviceTicketTranslation = mod.serviceTicketTranslation as unknown as AnyTranslation;
expect(serviceTicketTranslation).toBeDefined();
});
test("valid row translates correctly", async () => {
const mod = await import("../service-ticket");
serviceTicketTranslation = mod.serviceTicketTranslation as unknown as AnyTranslation;
const ctx = makeContext({
usersByMemberRecId: new Map([[5, "user-abc"]]),
usersByIdentifier: new Map([["admin", "user-admin"]]),
billingTypeByTicketId: new Map([[42, "Standard"]]),
billingInstructionsByTicketId: new Map([[42, "Bill monthly"]]),
});
const result = translateRow(
{
srServiceRecId: 42, summary: "Server is down", srLocationRecId: 1, srSourceRecId: 2,
srUrgencyRecId: 3, srSeverityRecId: 4, srImpactRecId: 5, srBoardRecId: 10,
companyRecId: 100, contactRecId: 200, companyAddressRecId: 300,
billingCompanyRecId: null, billingAddressRecId: null,
ticketOwnerRecId: 5, enteredBy: "admin", updatedBy: "admin", closedBy: null,
dateEnteredUtc: new Date("2025-01-01"), lastUpdatedUtc: new Date("2025-06-01"),
dateClosedUtc: null, isClosedFlag: false, billMethod: "A",
poNumber: "PO123", billingAmount: 500, rejectedFlag: false, publishFlag: true, redFlag: false,
},
serviceTicketTranslation,
ctx
);
expect(result.id).toBe(42);
expect(result.summary).toBe("Server is down");
expect(result.severityId).toBe(4);
expect(result.ticketOwnerId).toBe("user-abc");
expect(result.createdById).toBe("user-admin");
expect(result.billingMethod).toBe("ACTUAL_RATES");
expect(result.billingType).toBe("STANDARD");
expect(result.billingInstructions).toBe("Bill monthly");
});
test("empty summary triggers skipRow", async () => {
const mod = await import("../service-ticket");
serviceTicketTranslation = mod.serviceTicketTranslation as unknown as AnyTranslation;
const ctx = makeContext();
expectSkipRow(() => {
translateRow(
{ srServiceRecId: 1, summary: "", srLocationRecId: 1, srSourceRecId: 1, srSeverityRecId: 1, srImpactRecId: 1, srBoardRecId: null, companyRecId: null, contactRecId: null, companyAddressRecId: null, billingCompanyRecId: null, billingAddressRecId: null, ticketOwnerRecId: null, enteredBy: null, updatedBy: null, closedBy: null, dateEnteredUtc: null, lastUpdatedUtc: null, dateClosedUtc: null, isClosedFlag: false, billMethod: null, srUrgencyRecId: null, poNumber: null, billingAmount: null, rejectedFlag: false, publishFlag: false, redFlag: false },
serviceTicketTranslation,
ctx
);
});
});
test("null locationId triggers skipRow", async () => {
const mod = await import("../service-ticket");
serviceTicketTranslation = mod.serviceTicketTranslation as unknown as AnyTranslation;
const ctx = makeContext();
expectSkipRow(() => {
translateRow(
{ srServiceRecId: 1, summary: "problem", srLocationRecId: null, srSourceRecId: 1, srSeverityRecId: 1, srImpactRecId: 1, srBoardRecId: null, companyRecId: null, contactRecId: null, companyAddressRecId: null, billingCompanyRecId: null, billingAddressRecId: null, ticketOwnerRecId: null, enteredBy: null, updatedBy: null, closedBy: null, dateEnteredUtc: null, lastUpdatedUtc: null, dateClosedUtc: null, isClosedFlag: false, billMethod: null, srUrgencyRecId: null, poNumber: null, billingAmount: null, rejectedFlag: false, publishFlag: false, redFlag: false },
serviceTicketTranslation,
ctx
);
});
});
test("null dates return null instead of epoch", async () => {
const mod = await import("../service-ticket");
serviceTicketTranslation = mod.serviceTicketTranslation as unknown as AnyTranslation;
const ctx = makeContext({
billingTypeByTicketId: new Map(),
billingInstructionsByTicketId: new Map(),
});
const result = translateRow(
{
srServiceRecId: 1, summary: "test", srLocationRecId: 1, srSourceRecId: 1,
srSeverityRecId: 1, srImpactRecId: 1, srBoardRecId: null,
companyRecId: null, contactRecId: null, companyAddressRecId: null,
billingCompanyRecId: null, billingAddressRecId: null,
ticketOwnerRecId: null, enteredBy: null, updatedBy: null, closedBy: null,
dateEnteredUtc: null, lastUpdatedUtc: null, dateClosedUtc: null,
isClosedFlag: false, billMethod: null, srUrgencyRecId: null,
poNumber: null, billingAmount: null, rejectedFlag: false, publishFlag: false, redFlag: false,
},
serviceTicketTranslation,
ctx
);
expect(result.createdAt).toBeNull();
expect(result.updatedAt).toBeNull();
});
test("billing method maps correctly", async () => {
const mod = await import("../service-ticket");
serviceTicketTranslation = mod.serviceTicketTranslation as unknown as AnyTranslation;
const ctx = makeContext({
billingTypeByTicketId: new Map(),
billingInstructionsByTicketId: new Map(),
});
const base = {
srServiceRecId: 1, summary: "test", srLocationRecId: 1, srSourceRecId: 1,
srSeverityRecId: 1, srImpactRecId: 1, srBoardRecId: null,
companyRecId: null, contactRecId: null, companyAddressRecId: null,
billingCompanyRecId: null, billingAddressRecId: null,
ticketOwnerRecId: null, enteredBy: null, updatedBy: null, closedBy: null,
dateEnteredUtc: new Date(), lastUpdatedUtc: new Date(), dateClosedUtc: null,
isClosedFlag: false, srUrgencyRecId: null,
poNumber: null, billingAmount: 0, rejectedFlag: false, publishFlag: false, redFlag: false,
};
for (const [input, expected] of [
["A", "ACTUAL_RATES"],
["F", "FIXED_FEE"],
["N", "NOT_TO_EXCEED"],
["O", "OVERRIDE_RATE"],
[null, "ACTUAL_RATES"],
["UNKNOWN", "ACTUAL_RATES"],
] as const) {
const result = translateRow({ ...base, billMethod: input }, serviceTicketTranslation, ctx);
expect(result.billingMethod).toBe(expected);
}
});
test("missing billingType defaults to STANDARD", async () => {
const mod = await import("../service-ticket");
serviceTicketTranslation = mod.serviceTicketTranslation as unknown as AnyTranslation;
const ctx = makeContext({
billingTypeByTicketId: new Map(), // No entry for ticket 999
billingInstructionsByTicketId: new Map(),
});
const result = translateRow(
{
srServiceRecId: 999, summary: "test", srLocationRecId: 1, srSourceRecId: 1,
srSeverityRecId: 1, srImpactRecId: 1, srBoardRecId: null,
companyRecId: null, contactRecId: null, companyAddressRecId: null,
billingCompanyRecId: null, billingAddressRecId: null,
ticketOwnerRecId: null, enteredBy: null, updatedBy: null, closedBy: null,
dateEnteredUtc: new Date(), lastUpdatedUtc: new Date(), dateClosedUtc: null,
isClosedFlag: false, billMethod: null, srUrgencyRecId: null,
poNumber: null, billingAmount: 0, rejectedFlag: false, publishFlag: false, redFlag: false,
},
serviceTicketTranslation,
ctx
);
expect(result.billingType).toBe("STANDARD");
});
});
// ═══════════════════════════════════════════════════════════════════════════
// Service Ticket Note Translator
// ═══════════════════════════════════════════════════════════════════════════
describe("serviceTicketNoteTranslation", () => {
let serviceTicketNoteTranslation: AnyTranslation;
test("module loads", async () => {
const mod = await import("../service-ticket-note");
serviceTicketNoteTranslation = mod.serviceTicketNoteTranslation as unknown as AnyTranslation;
expect(serviceTicketNoteTranslation).toBeDefined();
});
test("valid note translates correctly", async () => {
const mod = await import("../service-ticket-note");
serviceTicketNoteTranslation = mod.serviceTicketNoteTranslation as unknown as AnyTranslation;
const ctx = makeContext({
serviceTicketIds: new Set([42]),
usersByMemberRecId: new Map([[10, "user-abc"]]),
});
const result = translateRow(
{ ticketNoteRecId: 1, srServiceRecId: 42, notesMarkdown: "# Title", notes: "Title", memberRecId: 10, problemFlag: true, resolutionFlag: false, internalAnalysisFlag: false, internalMemberFlag: false, mergedFlag: false, bundledFlag: false, createdByParentFlag: false, dateCreatedUtc: new Date("2025-01-01"), lastUpdatedUtc: new Date("2025-06-01") },
serviceTicketNoteTranslation,
ctx
);
expect(result.serviceTicketId).toBe(42);
expect(result.authorId).toBe("user-abc");
expect(result.notesMd).toBe("# Title");
});
test("missing parent ticket triggers skipRow", async () => {
const mod = await import("../service-ticket-note");
serviceTicketNoteTranslation = mod.serviceTicketNoteTranslation as unknown as AnyTranslation;
const ctx = makeContext({
serviceTicketIds: new Set([99]), // 42 NOT in set
usersByMemberRecId: new Map([[10, "user-abc"]]),
});
expectSkipRow(() => {
translateRow(
{ ticketNoteRecId: 1, srServiceRecId: 42, notesMarkdown: "", notes: "", memberRecId: 10, problemFlag: false, resolutionFlag: false, internalAnalysisFlag: false, internalMemberFlag: false, mergedFlag: false, bundledFlag: false, createdByParentFlag: false, dateCreatedUtc: null, lastUpdatedUtc: null },
serviceTicketNoteTranslation,
ctx
);
});
});
test("unknown memberRecId triggers skipRow", async () => {
const mod = await import("../service-ticket-note");
serviceTicketNoteTranslation = mod.serviceTicketNoteTranslation as unknown as AnyTranslation;
const ctx = makeContext({
serviceTicketIds: new Set([42]),
usersByMemberRecId: new Map(), // empty
});
expectSkipRow(() => {
translateRow(
{ ticketNoteRecId: 1, srServiceRecId: 42, notesMarkdown: "", notes: "", memberRecId: 999, problemFlag: false, resolutionFlag: false, internalAnalysisFlag: false, internalMemberFlag: false, mergedFlag: false, bundledFlag: false, createdByParentFlag: false, dateCreatedUtc: null, lastUpdatedUtc: null },
serviceTicketNoteTranslation,
ctx
);
});
});
test("null dates return null", async () => {
const mod = await import("../service-ticket-note");
serviceTicketNoteTranslation = mod.serviceTicketNoteTranslation as unknown as AnyTranslation;
const ctx = makeContext({
serviceTicketIds: new Set([42]),
usersByMemberRecId: new Map([[10, "user-abc"]]),
});
const result = translateRow(
{ ticketNoteRecId: 1, srServiceRecId: 42, notesMarkdown: "", notes: "", memberRecId: 10, problemFlag: false, resolutionFlag: false, internalAnalysisFlag: false, internalMemberFlag: false, mergedFlag: false, bundledFlag: false, createdByParentFlag: false, dateCreatedUtc: null, lastUpdatedUtc: null },
serviceTicketNoteTranslation,
ctx
);
expect(result.createdAt).toBeNull();
expect(result.updatedAt).toBeNull();
});
});
// ═══════════════════════════════════════════════════════════════════════════
// Product Data Translator
// ═══════════════════════════════════════════════════════════════════════════
describe("productDataTranslation", () => {
let productDataTranslation: AnyTranslation;
test("module loads", async () => {
const mod = await import("../product-data");
productDataTranslation = mod.productDataTranslation as unknown as AnyTranslation;
expect(productDataTranslation).toBeDefined();
});
test("missing catalog item triggers skipRow", async () => {
const mod = await import("../product-data");
productDataTranslation = mod.productDataTranslation as unknown as AnyTranslation;
const ctx = makeContext({
catalogItemIds: new Set([1]), // 999 NOT in set
corporateLocationIds: new Set([1]),
});
expectSkipRow(() => {
translateRow(
{ ivProductRecId: 1, quantity: 1, catalogRecId: 999, ownerLevelRecId: 1, internalNote: null, shortDescription: null, sequenceNumber: null, unitPrice: 10, unitCost: 5, listPrice: 15, discountAmount: 0, recurringRevenue: 0, recurringCost: 0, qtyPicked: 0, qtyShipped: 0, cancelReason: null, quantityCancelled: null, billableFlag: true, taxableFlag: false, invoiceFlag: false, recurringFlag: false, poApprovedFlag: false, calcPriceFlag: false, calcCostFlag: false, cancelFlag: false, srServiceRecId: null, opportunityRecId: null, updatedBy: null, enteredBy: null, closedBy: null, cancelBy: null, dateClosedUtc: null, cancelDateUtc: null, lastUpdateUtc: new Date(), dateEnteredUtc: new Date() },
productDataTranslation,
ctx
);
});
});
test("missing corporate location triggers skipRow", async () => {
const mod = await import("../product-data");
productDataTranslation = mod.productDataTranslation as unknown as AnyTranslation;
const ctx = makeContext({
catalogItemIds: new Set([1]),
corporateLocationIds: new Set(), // empty
});
expectSkipRow(() => {
translateRow(
{ ivProductRecId: 1, quantity: 1, catalogRecId: 1, ownerLevelRecId: 999, internalNote: null, shortDescription: null, sequenceNumber: null, unitPrice: 10, unitCost: 5, listPrice: 15, discountAmount: 0, recurringRevenue: 0, recurringCost: 0, qtyPicked: 0, qtyShipped: 0, cancelReason: null, quantityCancelled: null, billableFlag: true, taxableFlag: false, invoiceFlag: false, recurringFlag: false, poApprovedFlag: false, calcPriceFlag: false, calcCostFlag: false, cancelFlag: false, srServiceRecId: null, opportunityRecId: null, updatedBy: null, enteredBy: null, closedBy: null, cancelBy: null, dateClosedUtc: null, cancelDateUtc: null, lastUpdateUtc: new Date(), dateEnteredUtc: new Date() },
productDataTranslation,
ctx
);
});
});
test("numeric conversions handle null and invalid values", async () => {
const mod = await import("../product-data");
productDataTranslation = mod.productDataTranslation as unknown as AnyTranslation;
const ctx = makeContext({
catalogItemIds: new Set([1]),
corporateLocationIds: new Set([1]),
serviceTicketIds: new Set(),
opportunityIds: new Set(),
productCustomByIvProductId: new Map(),
});
const result = translateRow(
{ ivProductRecId: 1, quantity: null, catalogRecId: 1, ownerLevelRecId: 1, internalNote: null, shortDescription: null, sequenceNumber: null, unitPrice: null, unitCost: null, listPrice: null, discountAmount: null, recurringRevenue: null, recurringCost: null, qtyPicked: null, qtyShipped: null, cancelReason: null, quantityCancelled: null, billableFlag: true, taxableFlag: false, invoiceFlag: false, recurringFlag: false, poApprovedFlag: false, calcPriceFlag: false, calcCostFlag: false, cancelFlag: false, srServiceRecId: null, opportunityRecId: null, updatedBy: null, enteredBy: null, closedBy: null, cancelBy: null, dateClosedUtc: null, cancelDateUtc: null, lastUpdateUtc: new Date(), dateEnteredUtc: new Date() },
productDataTranslation,
ctx
);
expect(result.qty).toBe(1); // null quantity defaults to 1
expect(result.unitPrice).toBe(0);
expect(result.unitCost).toBe(0);
expect(result.listPrice).toBe(0);
expect(result.discount).toBe(0);
});
});
// ═══════════════════════════════════════════════════════════════════════════
// Service Ticket Priority Translator (Boolean fix)
// ═══════════════════════════════════════════════════════════════════════════
describe("serviceTicketPriorityTranslation", () => {
test("defaultFlag is coerced to boolean", async () => {
const mod = await import("../service-ticket-priority");
const translation = mod.serviceTicketPriorityTranslation as unknown as AnyTranslation;
const ctx = makeContext();
// Truthy value
const result1 = translateRow(
{ srUrgencyRecId: 1, description: "High", color: "#FF0000", defaultFlag: 1, createdBy: null, updatedBy: null, dateCreatedUtc: new Date(), lastUpdatedUtc: new Date() },
translation, ctx
);
expect(result1.defaultFlag).toBe(true);
// Falsy value
const result2 = translateRow(
{ srUrgencyRecId: 2, description: "Low", color: null, defaultFlag: 0, createdBy: null, updatedBy: null, dateCreatedUtc: new Date(), lastUpdatedUtc: new Date() },
translation, ctx
);
expect(result2.defaultFlag).toBe(false);
// Null value
const result3 = translateRow(
{ srUrgencyRecId: 3, description: "Med", color: null, defaultFlag: null, createdBy: null, updatedBy: null, dateCreatedUtc: new Date(), lastUpdatedUtc: new Date() },
translation, ctx
);
expect(result3.defaultFlag).toBe(false);
});
});
// ═══════════════════════════════════════════════════════════════════════════
// SkipRowError type
// ═══════════════════════════════════════════════════════════════════════════
describe("SkipRowError", () => {
test("is distinct from generic Error", () => {
const err = new SkipRowError("test reason");
expect(err).toBeInstanceOf(SkipRowError);
expect(err).toBeInstanceOf(Error);
expect(err.name).toBe("SkipRowError");
expect(err.message).toBe("test reason");
expect(err.__brand).toBe("SkipRowError");
});
test("does not match SKIP_ROW: prefix check", () => {
const err = new SkipRowError("Something wrong");
// Old code would check: error.message.startsWith("SKIP_ROW:")
// New errors don't have that prefix
expect(err.message.startsWith("SKIP_ROW:")).toBe(false);
});
});
@@ -0,0 +1,23 @@
import { ProductCategory as CwProductCategory } from "../../generated/prisma/client";
import { CatalogCategory as ApiCatalogCategory } from "../../../api/generated/prisma/client";
import { Translation } from "./types";
export const catalogCategoryTranslation: Translation<
CwProductCategory,
ApiCatalogCategory
> = {
values: [
{ from: "categoryRecId", to: "id" },
{
from: "description",
to: "name",
process: (value) => (value ? value : "Unknown Category"),
},
{ from: "description", to: "description" },
{ from: "inactiveFlag", to: "inactiveFlag" },
{ from: "updatedBy", to: "updatedById" },
{ from: "enteredBy", to: "createdById" },
{ from: "lastUpdatedUtc", to: "updatedAt" },
{ from: "dateEnteredUtc", to: "createdAt" },
],
};
@@ -0,0 +1,24 @@
import { ProductType as CwProductType } from "../../generated/prisma/client";
import { CatalogItemType as ApiCatalogItemType } from "../../../api/generated/prisma/client";
import { Translation } from "./types";
export const catalogItemTypeTranslation: Translation<
CwProductType,
ApiCatalogItemType
> = {
values: [
{ from: "typeRecId", to: "id" },
{
from: "description",
to: "name",
process: (value) => (value ? value : "Unknown Type"),
},
{ from: "description", to: "description" },
{ from: "inactiveFlag", to: "inactiveFlag" },
{ from: "defaultFlag", to: "defaultFlag" },
{ from: "updatedBy", to: "updatedById" },
{ from: "enteredBy", to: "createdById" },
{ from: "lastUpdatedUtc", to: "updatedAt" },
{ from: "dateEnteredUtc", to: "createdAt" },
],
};
+58
View File
@@ -0,0 +1,58 @@
import { ProductCatalog as CwProductCatalog } from "../../generated/prisma/client";
import { CatalogItem as ApiCatalogItem } from "../../../api/generated/prisma/client";
import { Translation } from "./types";
export const catalogItemTranslation: Translation<
CwProductCatalog,
ApiCatalogItem
> = {
values: [
{ from: "catalogRecId", to: "id" },
{ from: "itemId", to: "identifier" },
{
from: "description",
to: "name",
process: (value) => (value ? value : "Unnamed Item"),
},
{ from: "longDescription", to: "description" },
{ from: "longDescription", to: "customerDescription" },
{ from: "notes", to: "internalNotes" },
{
from: "subcategoryRecId",
to: "subcategoryId",
process: (value) => {
if (value == null) {
throw new Error(
"CatalogItem subcategoryId is required but subcategoryRecId is null"
);
}
return value;
},
},
{ from: "manufacturerRecId", to: "manufacturerId" },
{ from: "manufacturerPartNum", to: "partNumber" },
{ from: "vendorSku", to: "vendorSku" },
{ from: "vendorRecId", to: "vendorCwId" },
{
from: "listPrice",
to: "price",
process: (value) => Number(value),
},
{
from: "currentCost",
to: "cost",
process: (value) => (value == null ? 0 : Number(value)),
},
{ from: "inactiveFlag", to: "inactive" },
{ from: "taxableFlag", to: "salesTaxable" },
{
from: "minimumStock",
to: "onHand",
process: (value) => (value == null ? 0 : value),
},
{ from: "classId", to: "classId" },
{ from: "dateEnteredUtc", to: "createdAt" },
{ from: "lastUpdatedUtc", to: "cwLastUpdated" },
{ from: "lastUpdatedUtc", to: "updatedAt" },
],
};
@@ -0,0 +1,23 @@
import { Manufacturer as CwManufacturer } from "../../generated/prisma/client";
import { CatalogManufacturer as ApiCatalogManufacturer } from "../../../api/generated/prisma/client";
import { Translation } from "./types";
export const catalogManufacturerTranslation: Translation<
CwManufacturer,
ApiCatalogManufacturer
> = {
values: [
{ from: "manufacturerRecId", to: "id" },
{ from: "manufacturerName", to: "name" },
{ from: "manufacturerName", to: "description" },
{
from: "inactiveFlag",
to: "inactiveFlag",
process: (value) => Boolean(value),
},
{ from: "updatedBy", to: "updatedById" },
{ from: "enteredBy", to: "createdById" },
{ from: "lastUpdatedUtc", to: "updatedAt" },
{ from: "dateEnteredUtc", to: "createdAt" },
],
};
@@ -0,0 +1,24 @@
import { ProductSubcategory as CwProductSubcategory } from "../../generated/prisma/client";
import { CatalogSubcategory as ApiCatalogSubcategory } from "../../../api/generated/prisma/client";
import { Translation } from "./types";
export const catalogSubcategoryTranslation: Translation<
CwProductSubcategory,
ApiCatalogSubcategory
> = {
values: [
{ from: "subcategoryRecId", to: "id" },
{
from: "description",
to: "name",
process: (value) => (value ? value : "Unknown Subcategory"),
},
{ from: "description", to: "description" },
{ from: "categoryRecId", to: "categoryId" },
{ from: "inactiveFlag", to: "inactiveFlag" },
{ from: "updatedBy", to: "updatedById" },
{ from: "enteredBy", to: "createdById" },
{ from: "lastUpdatedUtc", to: "updatedAt" },
{ from: "dateEnteredUtc", to: "createdAt" },
],
};
@@ -0,0 +1,53 @@
import { CompanyAddress as CwCompanyAddress } from "../../generated/prisma/client";
import {
CompanyAddress as ApiCompanyAddress,
Country,
USState,
} from "../../../api/generated/prisma/client";
import { Translation } from "./types";
const toUSState = (value: string | null): USState | null => {
if (!value) return null;
const normalized = value.trim().toUpperCase();
if (normalized in USState) {
return normalized as USState;
}
return null;
};
const toCountry = (value: number | null): Country | null => {
if (value == null) return null;
return Country.US;
};
export const companyAddressTranslation: Translation<
CwCompanyAddress,
ApiCompanyAddress
> = {
values: [
{ from: "companyAddressRecId", to: "id" },
{
from: "description",
to: "name",
process: (value) => (value ? value : "Primary Site"),
},
{ from: "description", to: "description" },
{ from: "addressLine1", to: "addressLine1" },
{ from: "addressLine2", to: "addressLine2" },
{ from: "city", to: "city" },
{ from: "stateId", to: "state", process: toUSState },
{ from: "zip", to: "zipCode" },
{ from: "countryRecId", to: "country", process: toCountry },
{ from: "phoneNbr", to: "phone" },
{ from: "phoneNbrFax", to: "fax" },
{ from: "inactiveFlag", to: "inactiveFlag" },
{ from: "defaultFlag", to: "defaultFlag" },
{ from: "defaultMailFlag", to: "defaultMailFlag" },
{ from: "defaultBillFlag", to: "defaultBillFlag" },
{ from: "defaultShipFlag", to: "defaultShipFlag" },
{ from: "updatedBy", to: "updatedById" },
{ from: "companyRecId", to: "companyId" },
{ from: "dateEnteredUtc", to: "createdAt" },
{ from: "lastUpdatedUtc", to: "updatedAt" },
],
};
+18 -2
View File
@@ -2,6 +2,22 @@ import { Company as CwCompany } from "../../generated/prisma/client";
import { Company as ApiCompany } from "../../../api/generated/prisma/client";
import { Translation } from "./types";
const companyTranslation: Translation<CwCompany, ApiCompany> = {
values: [{ from: "" }],
export const companyTranslation: Translation<CwCompany, ApiCompany> = {
values: [
{ from: "companyRecId", to: "id" },
{
from: "companyName",
to: "name",
process: (value) => (value ? value : "Unnamed Company"),
},
{ from: "phoneNbr", to: "phone" },
{ from: "websiteUrl", to: "website" },
{ from: "deleteFlag", to: "deleteFlag" },
{ from: "dateDeleted", to: "dateDeleted" },
{ from: "taxId", to: "taxId" },
{ from: "deletedBy", to: "deletedById" },
{ from: "enteredBy", to: "enteredById" },
{ from: "dateEntered", to: "createdAt" },
{ from: "lastUpdate", to: "updatedAt" },
],
};
+63
View File
@@ -0,0 +1,63 @@
import { Contact as CwContact } from "../../generated/prisma/client";
import {
Contact as ApiContact,
GenderType,
PhoneType,
} from "../../../api/generated/prisma/client";
import { Translation } from "./types";
const toGender = (value: string | null): GenderType | null => {
if (!value) return null;
const normalized = value.trim().toUpperCase();
if (normalized === "M") return GenderType.MALE;
if (normalized === "F") return GenderType.FEMALE;
return null;
};
const toPhoneType = (value: string | null): PhoneType | null => {
if (!value) return null;
const normalized = value.trim().toUpperCase();
if (normalized === "DIRECT") return PhoneType.DIRECT;
if (normalized === "MOBILE") return PhoneType.MOBILE;
if (normalized === "HOME") return PhoneType.HOME;
if (normalized === "COMPANY") return PhoneType.COMPANY;
if (normalized === "SITE") return PhoneType.SITE;
return null;
};
export const contactTranslation: Translation<CwContact, ApiContact> = {
values: [
{ from: "contactRecId", to: "id" },
{
from: "inactiveFlag",
to: "active",
process: (value) => !value,
},
{
from: "defaultFlag",
to: "default",
process: (value) => Boolean(value),
},
{
from: "firstName",
to: "firstName",
process: (value) => (value ? value : "Unknown"),
},
{
from: "lastName",
to: "lastName",
process: (value) => (value ? value : "Contact"),
},
{ from: "nickName", to: "nickname" },
{ from: "title", to: "title" },
{ from: "gender", to: "gender", process: toGender },
{ from: "dateBirth", to: "birthday" },
{ from: "defaultPhoneNbr", to: "phone" },
{ from: "defaultPhoneExtension", to: "phoneExtension" },
{ from: "defaultPhoneType", to: "phoneType", process: toPhoneType },
{ from: "companyAddressRecId", to: "companyAddressId" },
{ from: "companyRecId", to: "companyId" },
{ from: "dateEnteredUtc", to: "createdAt" },
{ from: "lastUpdatedUtc", to: "updatedAt" },
],
};
+107
View File
@@ -0,0 +1,107 @@
/**
* Global context object passed to all translation process functions
* Contains lookup tables and computed values from external sources
*/
export interface TranslationContext {
// Set of API User.id values for FK validation
userIds: Set<string>;
// Set of API ServiceTicket.id values for FK validation
serviceTicketIds: Set<number>;
// Set of API Opportunity.id values for FK validation
opportunityIds: Set<number>;
// Set of API CatalogItem.id values for FK validation
catalogItemIds: Set<number>;
// Set of API CorporateLocation.id values for FK validation
corporateLocationIds: Set<number>;
// Set of API Company.id values for FK validation
companyIds: Set<number>;
// Set of API CompanyAddress.id values for FK validation
companyAddressIds: Set<number>;
// Set of API Contact.id values for FK validation
contactIds: Set<number>;
// Set of API OpportunityStage.id values for FK validation
opportunityStageIds: Set<number>;
// User lookups: CW memberRecId -> API User.id
usersByMemberRecId: Map<number, string>;
// User lookups: CW memberRecId -> API User.cwIdentifier
userIdentifiersByMemberRecId: Map<number, string>;
// User lookups: CW member identifier string -> API User.id
usersByIdentifier: Map<string, string>;
// Service ticket board lookups: API ServiceTicketBoard.id -> API ServiceTicketBoard.uid
serviceTicketBoardUidsById: Map<number, string>;
// Optional custom-field derived lookups keyed by CW SR_Service_RecID
billingTypeByTicketId: Map<number, string>;
billingInstructionsByTicketId: Map<number, string>;
// Opportunity/Product custom-field lookups loaded from CW reporting views.
opportunityNarrativeByOpportunityId: Map<number, string>;
productCustomByIvProductId: Map<
number,
{
procurementNotes: string | null;
productNarrative: string | null;
}
>;
// Required FK fallback for Opportunity.typeId
defaultOpportunityTypeId: number | null;
// Set of valid OpportunityStatus.id values for FK validation
opportunityStatusIds: Set<number>;
// Set of valid ScheduleStatus.id values for FK validation
scheduleStatusIds: Set<number>;
// Set of valid ScheduleType.id values for FK validation
scheduleTypeIds: Set<number>;
// Set of valid ScheduleSpan.id values for FK validation
scheduleSpanIds: Set<number>;
// Set of API TaxCode.id values for FK validation
taxCodeIds: Set<number>;
}
/**
* Create an empty context object with initialized maps
*/
export function createTranslationContext(): TranslationContext {
return {
userIds: new Set(),
serviceTicketIds: new Set(),
opportunityIds: new Set(),
catalogItemIds: new Set(),
corporateLocationIds: new Set(),
companyIds: new Set(),
companyAddressIds: new Set(),
contactIds: new Set(),
opportunityStageIds: new Set(),
usersByMemberRecId: new Map(),
userIdentifiersByMemberRecId: new Map(),
usersByIdentifier: new Map(),
serviceTicketBoardUidsById: new Map(),
billingTypeByTicketId: new Map(),
billingInstructionsByTicketId: new Map(),
opportunityNarrativeByOpportunityId: new Map(),
productCustomByIvProductId: new Map(),
defaultOpportunityTypeId: null,
opportunityStatusIds: new Set(),
scheduleStatusIds: new Set(),
scheduleTypeIds: new Set(),
scheduleSpanIds: new Set(),
taxCodeIds: new Set(),
};
}
@@ -0,0 +1,45 @@
import { OwnerLevel as CwOwnerLevel } from "../../generated/prisma/client";
import {
CorporateLocation as ApiCorporateLocation,
Country,
USState,
} from "../../../api/generated/prisma/client";
import { Translation } from "./types";
const toUSState = (value: string | null): USState | null => {
if (!value) return null;
const normalized = value.trim().toUpperCase();
if (normalized in USState) {
return normalized as USState;
}
return null;
};
const toCountry = (value: number | null): Country | null => {
if (value == null) return null;
return Country.US;
};
export const corporateLocationTranslation: Translation<
CwOwnerLevel,
ApiCorporateLocation
> = {
values: [
{ from: "ownerLevelRecId", to: "id" },
{
from: "ownerLevelName",
to: "name",
process: (value) => (value ? value : "Unknown Location"),
},
{ from: "description", to: "description" },
{ from: "updatedBy", to: "updatedById" },
{ from: "olAddressLine1", to: "addressLine1" },
{ from: "olAddressLine2", to: "addressLine2" },
{ from: "olCity", to: "city" },
{ from: "olStateId", to: "state", process: toUSState },
{ from: "olZip", to: "zipCode" },
{ from: "olCountryRecId", to: "country", process: toCountry },
{ from: "dateEnteredUtc", to: "createdAt" },
{ from: "lastUpdatedUtc", to: "updatedAt" },
],
};
+29
View File
@@ -0,0 +1,29 @@
import { Member as CwMember } from "../../generated/prisma/client";
import { CwMember as ApiCwMember } from "../../../api/generated/prisma/client";
import { Translation } from "./types";
export const cwMemberTranslation: Translation<CwMember, ApiCwMember> = {
values: [
{ from: "memberRecId", to: "cwMemberId" },
{ from: "memberId", to: "identifier" },
{
from: "firstName",
to: "firstName",
process: (value) => (value ? value : "Unknown"),
},
{
from: "lastName",
to: "lastName",
process: (value) => (value ? value : "Member"),
},
{ from: "emailAddress", to: "officeEmail" },
{ from: "inactiveFlag", to: "inactiveFlag" },
{
from: "dateHire",
to: "createdAt",
process: (value, _context, row) => value ?? row.lastUpdatedUtc,
},
{ from: "lastUpdatedUtc", to: "cwLastUpdated" },
{ from: "lastUpdatedUtc", to: "updatedAt" },
],
};
@@ -0,0 +1,20 @@
import { Department as CwDepartment } from "../../generated/prisma/client";
import { InternalDepartment as ApiInternalDepartment } from "../../../api/generated/prisma/client";
import { Translation } from "./types";
export const internalDepartmentTranslation: Translation<
CwDepartment,
ApiInternalDepartment
> = {
values: [
{ from: "departmentRecId", to: "id" },
{
from: "departmentName",
to: "name",
process: (value) => (value ? value : "Unknown Department"),
},
{ from: "dateEnteredUtc", to: "createdAt" },
{ from: "updatedBy", to: "updatedById" },
{ from: "lastUpdateUtc", to: "updatedAt" },
],
};
@@ -0,0 +1,32 @@
import { SoPipeline as CwSoPipeline } from "../../generated/prisma/client";
import { OpportunityStage as ApiOpportunityStage } from "../../../api/generated/prisma/client";
import { Translation } from "./types";
type ApiOpportunityStageRecord = {
id: number;
name: string;
seqNbr?: number | null;
funnelColor?: string | null;
updatedById?: string | null;
createdAt: Date;
updatedAt: Date;
};
export const opportunityStageTranslation: Translation<
CwSoPipeline,
ApiOpportunityStageRecord
> = {
values: [
{ from: "soPipelineRecId", to: "id" },
{
from: "description",
to: "name",
process: (v: string | null) => v ?? "Unknown Stage",
},
{ from: "seqNbr", to: "seqNbr" },
{ from: "funnelColor", to: "funnelColor" },
{ from: "updatedBy", to: "updatedById" },
{ from: "dateEnteredUtc", to: "createdAt" },
{ from: "lastUpdatedUtc", to: "updatedAt" },
],
};
@@ -0,0 +1,26 @@
import { SoOppStatus as CwOpportunityStatus } from "../../generated/prisma/client";
import { OpportunityStatus as ApiOpportunityStatus } from "../../../api/generated/prisma/client";
import { Translation } from "./types";
export const opportunityStatusTranslation: Translation<
CwOpportunityStatus,
ApiOpportunityStatus
> = {
values: [
{ from: "soOppStatusRecId", to: "id" },
{
from: "description",
to: "name",
process: (value) => (value ? value : "Unknown Status"),
},
{ from: "description", to: "description" },
{ from: "inactiveFlag", to: "inactiveFlag" },
{ from: "defaultFlag", to: "defaultFlag" },
{ from: "wonFlag", to: "wonFlag" },
{ from: "lostFlag", to: "lostFlag" },
{ from: "closedFlag", to: "closeFlag" },
{ from: "dateEnteredUtc", to: "createdAt" },
{ from: "updatedBy", to: "updatedById" },
{ from: "lastUpdatedUtc", to: "updatedAt" },
],
};
@@ -0,0 +1,22 @@
import { SoType as CwOpportunityType } from "../../generated/prisma/client";
import { OpportunityType as ApiOpportunityType } from "../../../api/generated/prisma/client";
import { Translation } from "./types";
export const opportunityTypeTranslation: Translation<
CwOpportunityType,
ApiOpportunityType
> = {
values: [
{ from: "soTypeRecId", to: "id" },
{
from: "description",
to: "name",
process: (value) => (value ? value : "Unknown Type"),
},
{ from: "description", to: "description" },
{ from: "inactiveFlag", to: "inactiveFlag" },
{ from: "dateEnteredUtc", to: "createdAt" },
{ from: "updatedBy", to: "updatedById" },
{ from: "lastUpdatedUtc", to: "updatedAt" },
],
};
+143
View File
@@ -0,0 +1,143 @@
import {
Opportunity as CwOpportunity,
OpportunityMember as CwOpportunityMember,
} from "../../generated/prisma/client";
import { OpportunityInterest } from "../../../api/generated/prisma/client";
import { Translation, skipRow } from "./types";
import { TranslationContext } from "./context";
type ApiOpportunityRecord = {
id: number;
name: string;
notes?: string | null;
oppNarrative?: string | null;
typeId: number;
stageCwId?: number | null;
stageId?: number | null;
statusId?: number | null;
taxCodeId?: number | null;
interest?: OpportunityInterest | null;
probability: number;
source?: string | null;
primarySalesRepId?: string | null;
secondarySalesRepId?: string | null;
companyId?: number | null;
contactId?: number | null;
siteId?: number | null;
customerPO?: string | null;
expectedCloseDate?: Date | null;
pipelineChangeDate?: Date | null;
dateBecameLead?: Date | null;
closedDate?: Date | null;
closedFlag: boolean;
closedById?: string | null;
updatedBy: string;
eneteredBy: string;
createdAt: Date;
updatedAt: Date;
};
type CwOpportunityWithMembers = CwOpportunity & {
members?: Pick<
CwOpportunityMember,
"memberRecId" | "primarySalesFlag" | "secondarySalesFlag"
>[];
};
const toInterest = (value: number | null): OpportunityInterest | null => {
if (value == null) return null;
if (value <= 1) return OpportunityInterest.COLD;
if (value === 2) return OpportunityInterest.WARM;
return OpportunityInterest.HOT;
};
export const opportunityTranslation: Translation<
CwOpportunityWithMembers,
ApiOpportunityRecord,
TranslationContext
> = {
values: [
{ from: "opportunityRecId", to: "id" },
{ from: "opportunityName", to: "name" },
{ from: "notes", to: "notes" },
{
from: "opportunityRecId",
to: "oppNarrative",
process: (value, context) => {
if (value == null) return null;
return context.opportunityNarrativeByOpportunityId.get(value) ?? null;
},
},
{
from: "soTypeRecId",
to: "typeId",
process: (value, context) => {
if (value != null) return value;
if (context.defaultOpportunityTypeId != null)
return context.defaultOpportunityTypeId;
skipRow("Opportunity missing type and no default type available");
},
},
{ from: "soPipelineRecId", to: "stageId" },
{ from: "soOppStatusRecId", to: "statusId" },
{ from: "taxCodeRecId", to: "taxCodeId" },
{
from: "soInterestRecId",
to: "interest",
process: toInterest,
},
{
from: "probabilityToClose",
to: "probability",
process: (value) => (value == null ? 0 : value),
},
{ from: "source", to: "source" },
{
from: "opportunityRecId",
to: "primarySalesRepId",
process: (_value, context, row) => {
const primary = row.members?.find((member) => member.primarySalesFlag);
if (!primary) return null;
return (
context.userIdentifiersByMemberRecId.get(primary.memberRecId) ?? null
);
},
},
{
from: "opportunityRecId",
to: "secondarySalesRepId",
process: (_value, context, row) => {
const secondary = row.members?.find(
(member) => member.secondarySalesFlag
);
if (!secondary) return null;
return (
context.userIdentifiersByMemberRecId.get(secondary.memberRecId) ??
null
);
},
},
{ from: "companyRecId", to: "companyId" },
{ from: "contactRecId", to: "contactId" },
{ from: "companyAddressRecId", to: "siteId" },
{ from: "poNumber", to: "customerPO" },
{ from: "dateCloseExpected", to: "expectedCloseDate" },
{ from: "datePipelineChange", to: "pipelineChangeDate" },
{ from: "dateBecameLead", to: "dateBecameLead" },
{ from: "dateClosed", to: "closedDate" },
{ from: "oldCloseFlag", to: "closedFlag" },
{ from: "closedBy", to: "closedById" },
{
from: "updatedBy",
to: "updatedBy",
process: (value) => (value ? value : "system"),
},
{
from: "enteredBy",
to: "eneteredBy",
process: (value) => (value ? value : "system"),
},
{ from: "dateBecameLeadUtc", to: "createdAt" },
{ from: "lastUpdatedUtc", to: "updatedAt" },
],
};
+218
View File
@@ -0,0 +1,218 @@
import { IV_Product as CwIVProduct } from "../../generated/prisma/client";
import { Translation, skipRow } from "./types";
import { TranslationContext } from "./context";
type ApiProductDataRecord = {
id: number;
qty: number;
internalNote?: string | null;
shortDescription?: string | null;
description?: string | null;
sequenceNumber?: number | null;
procurementNotes?: string | null;
productNarrative?: string | null;
unitPrice: number;
unitCost: number;
listPrice: number;
discount: number;
recurringRevenue: number;
recurringCost: number;
qtyPicked: number;
qtyShipped: number;
cancelReason?: string | null;
cancelQty?: number | null;
billableFlag: boolean;
taxableFlag: boolean;
invoiceFlag: boolean;
recurringFlag: boolean;
poApprovedFlag: boolean;
calcPriceFlag: boolean;
calcCostFlag: boolean;
cancelFlag: boolean;
catalogItemId: number;
corporateLocationId: number;
serviceTicketId?: number | null;
opportunityId?: number | null;
updatedById?: string | null;
createdById?: string | null;
closedById?: string | null;
cancelById: string;
closedAt?: Date | null;
cancelledAt?: Date | null;
createdAt: Date;
updatedAt: Date;
};
const toInt = (value: unknown, fallback = 0): number => {
if (value == null) return fallback;
const numeric = Number(value);
if (!Number.isFinite(numeric)) return fallback;
return Math.trunc(numeric);
};
const toNumber = (value: unknown, fallback = 0): number => {
if (value == null) return fallback;
const numeric = Number(value);
return Number.isFinite(numeric) ? numeric : fallback;
};
export const productDataTranslation: Translation<
CwIVProduct,
ApiProductDataRecord,
TranslationContext
> = {
values: [
{ from: "ivProductRecId", to: "id" },
{
from: "quantity",
to: "qty",
process: (value) => toNumber(value, 1),
},
{ from: "internalNote", to: "internalNote" },
{ from: "shortDescription", to: "shortDescription" },
{ from: "description", to: "description" },
{
from: "ivProductRecId",
to: "procurementNotes",
process: (value, context) => {
const custom = context.productCustomByIvProductId.get(value);
return custom?.procurementNotes ?? null;
},
},
{
from: "ivProductRecId",
to: "productNarrative",
process: (value, context) => {
const custom = context.productCustomByIvProductId.get(value);
return custom?.productNarrative ?? null;
},
},
{
from: "sequenceNumber",
to: "sequenceNumber",
process: (value) => (value == null ? null : toInt(value, 0)),
},
{
from: "unitPrice",
to: "unitPrice",
process: (value) => toNumber(value, 0),
},
{
from: "unitCost",
to: "unitCost",
process: (value) => toNumber(value, 0),
},
{
from: "listPrice",
to: "listPrice",
process: (value) => toNumber(value, 0),
},
{
from: "discountAmount",
to: "discount",
process: (value) => toNumber(value, 0),
},
{
from: "recurringRevenue",
to: "recurringRevenue",
process: (value) => toNumber(value, 0),
},
{
from: "recurringCost",
to: "recurringCost",
process: (value) => toNumber(value, 0),
},
{
from: "qtyPicked",
to: "qtyPicked",
process: (value) => toInt(value, 0),
},
{
from: "qtyShipped",
to: "qtyShipped",
process: (value) => toInt(value, 0),
},
{ from: "cancelReason", to: "cancelReason" },
{
from: "quantityCancelled",
to: "cancelQty",
process: (value) => (value == null ? null : toNumber(value, 0)),
},
{ from: "billableFlag", to: "billableFlag" },
{ from: "taxableFlag", to: "taxableFlag" },
{ from: "invoiceFlag", to: "invoiceFlag" },
{ from: "recurringFlag", to: "recurringFlag" },
{ from: "poApprovedFlag", to: "poApprovedFlag" },
{ from: "calcPriceFlag", to: "calcPriceFlag" },
{ from: "calcCostFlag", to: "calcCostFlag" },
{ from: "cancelFlag", to: "cancelFlag" },
{
from: "catalogRecId",
to: "catalogItemId",
process: (value, context) => {
if (!context.catalogItemIds.has(value)) {
skipRow(
`ProductData catalog item missing for catalogRecId ${value}`
);
}
return value;
},
},
{
from: "ownerLevelRecId",
to: "corporateLocationId",
process: (value, context) => {
if (!context.corporateLocationIds.has(value)) {
skipRow(
`ProductData corporate location missing for ownerLevelRecId ${value}`
);
}
return value;
},
},
{
from: "srServiceRecId",
to: "serviceTicketId",
process: (value, context) => {
if (value == null) return null;
return context.serviceTicketIds.has(value) ? value : null;
},
},
{
from: "opportunityRecId",
to: "opportunityId",
process: (value, context) => {
if (value == null) return null;
return context.opportunityIds.has(value) ? value : null;
},
},
{ from: "updatedBy", to: "updatedById" },
{ from: "enteredBy", to: "createdById" },
{ from: "closedBy", to: "closedById" },
{
from: "cancelBy",
to: "cancelById",
process: (value, context) => {
if (value == null) return "system";
return context.usersByMemberRecId.get(value) ?? `cw-member:${value}`;
},
},
{ from: "dateClosedUtc", to: "closedAt" },
{ from: "cancelDateUtc", to: "cancelledAt" },
{
from: "dateEntered",
to: "createdAt",
process: (value) => value ?? new Date(0),
},
{
from: "dateEnteredUtc",
to: "createdAt",
process: (value) => value ?? new Date(0),
},
{
from: "lastUpdatedUTC",
to: "updatedAt",
process: (value) => value ?? new Date(0),
},
],
};
@@ -0,0 +1,30 @@
import { ProductInventory as CwProductInventory } from "../../generated/prisma/client";
import { ProductInventory as ApiProductInventory } from "../../../api/generated/prisma/client";
import { Translation } from "./types";
export const productInventoryTranslation: Translation<
CwProductInventory,
ApiProductInventory
> = {
values: [
{ from: "inventoryRecId", to: "id" },
{
from: "qtyOnHand",
to: "qtyOnHand",
process: (value) => (value == null ? 0 : Number(value)),
},
{
from: "lastUpdate",
to: "createdAt",
process: (value) => (value ? value : new Date(0)),
},
{ from: "warehouseBinRecId", to: "warehouseBinId" },
{ from: "catalogRecId", to: "itemId" },
{ from: "updatedBy", to: "updatedById" },
{
from: "lastUpdate",
to: "updatedAt",
process: (value) => (value ? value : new Date(0)),
},
],
};
+14
View File
@@ -0,0 +1,14 @@
import { ScheduleSpan as CwScheduleSpan } from "../../generated/prisma/client";
import { ScheduleSpan as ApiScheduleSpan } from "../../../api/generated/prisma/client";
import { Translation } from "./types";
export const scheduleSpanTranslation: Translation<
CwScheduleSpan,
ApiScheduleSpan
> = {
values: [
{ from: "scheduleSpanRecId", to: "id" },
{ from: "scheduleSpanId", to: "scheduleSpanId" },
{ from: "spanDesc", to: "spanDesc" },
],
};
@@ -0,0 +1,33 @@
import { ScheduleStatus as CwScheduleStatus } from "../../generated/prisma/client";
import { ScheduleStatus as ApiScheduleStatus } from "../../../api/generated/prisma/client";
import { Translation } from "./types";
export const scheduleStatusTranslation: Translation<
CwScheduleStatus,
ApiScheduleStatus
> = {
values: [
{ from: "scheduleStatusRecId", to: "id" },
{
from: "description",
to: "name",
process: (value) => (value ? value : "Unknown Status"),
},
{ from: "description", to: "description" },
{ from: "color", to: "color" },
{
from: "softFlag",
to: "softFlag",
process: (value) => Boolean(value),
},
{
from: "defaultFlag",
to: "defaultFlag",
process: (value) => Boolean(value),
},
{ from: "enteredBy", to: "createdById" },
{ from: "updatedBy", to: "updatedById" },
{ from: "dateEnteredUtc", to: "createdAt" },
{ from: "lastUpdateUtc", to: "updatedAt" },
],
};
+36
View File
@@ -0,0 +1,36 @@
import { ScheduleType as CwScheduleType } from "../../generated/prisma/client";
import { ScheduleType as ApiScheduleType } from "../../../api/generated/prisma/client";
import { Translation } from "./types";
export const scheduleTypeTranslation: Translation<
CwScheduleType,
ApiScheduleType
> = {
values: [
{ from: "scheduleTypeRecId", to: "id" },
{
from: "description",
to: "name",
process: (value) => (value ? value : "Unknown Type"),
},
{ from: "description", to: "description" },
{ from: "displayColor", to: "displayColor" },
{ from: "tableReference", to: "tableReference" },
{ from: "moduleId", to: "moduleId" },
{ from: "scheduleTypeId", to: "scheduleTypeId" },
{
from: "systemFlag",
to: "systemFlag",
process: (value) => Boolean(value),
},
{
from: "displayFlag",
to: "displayFlag",
process: (value) => Boolean(value),
},
{ from: "enteredBy", to: "createdById" },
{ from: "updatedBy", to: "updatedById" },
{ from: "dateEnteredUtc", to: "createdAt" },
{ from: "lastUpdateUtc", to: "updatedAt" },
],
};
+114
View File
@@ -0,0 +1,114 @@
import { Schedule as CwSchedule } from "../../generated/prisma/client";
import { Translation } from "./types";
type ApiScheduleRecord = {
id: number;
name: string;
description: string | null;
closedFlag: boolean;
reminderFlag: boolean;
allDayFlag: boolean;
acknowledgementFlag: boolean;
meetingFlag: boolean;
recurringFlag: boolean;
billableFlag: boolean;
acknowledgedById: string | null;
acknowledgedAt: Date | null;
startDate: Date | null;
endDate: Date | null;
hoursScheduled: number | null;
duration: number | null;
hoursPerDay: number | null;
reminderMinutes: number | null;
statusId: number | null;
typeId: number | null;
scheduleSpanId: number | null;
memberId: string | null;
updatedById: string | null;
createdById: string | null;
closedById: string | null;
closedAt: Date | null;
createdAt: Date;
updatedAt: Date;
};
export const scheduleTranslation: Translation<CwSchedule, ApiScheduleRecord> = {
values: [
{ from: "scheduleRecId", to: "id" },
{
from: "scheduleDesc",
to: "name",
process: (value) => (value ? value : ""),
},
{ from: "scheduleDesc", to: "description" },
{
from: "closeFlag",
to: "closedFlag",
process: (value) => Boolean(value),
},
{
from: "reminderFlag",
to: "reminderFlag",
process: (value) => Boolean(value),
},
{
from: "allDayFlag",
to: "allDayFlag",
process: (value) => Boolean(value),
},
{
from: "ackFlag",
to: "acknowledgementFlag",
process: (value) => Boolean(value),
},
{
from: "meetingFlag",
to: "meetingFlag",
process: (value) => Boolean(value),
},
{
from: "recurringFlag",
to: "recurringFlag",
process: (value) => Boolean(value),
},
{
from: "billableFlag",
to: "billableFlag",
process: (value) => Boolean(value),
},
{ from: "acknowledgedBy", to: "acknowledgedById" },
{ from: "ackDateUtc", to: "acknowledgedAt" },
{ from: "dateTimeStartUtc", to: "startDate" },
{ from: "dateTimeEndUtc", to: "endDate" },
{
from: "hoursSched",
to: "hoursScheduled",
process: (value) => (value != null ? Number(value) : null),
},
{ from: "duration", to: "duration" },
{
from: "hoursPerDay",
to: "hoursPerDay",
process: (value) => (value != null ? Number(value) : null),
},
{ from: "reminderMinutes", to: "reminderMinutes" },
{ from: "scheduleStatusRecId", to: "statusId" },
{ from: "scheduleTypeRecId", to: "typeId" },
{ from: "scheduleSpanRecId", to: "scheduleSpanId" },
{ from: "memberId", to: "memberId" },
{ from: "updatedBy", to: "updatedById" },
{ from: "enteredBy", to: "createdById" },
{ from: "closedBy", to: "closedById" },
{ from: "closeDateUtc", to: "closedAt" },
{
from: "dateEnteredUtc",
to: "createdAt",
process: (value) => (value as Date | null) ?? new Date(0),
},
{
from: "lastUpdateUtc",
to: "updatedAt",
process: (value) => (value as Date | null) ?? new Date(0),
},
],
};
@@ -0,0 +1,85 @@
import { SrBoard as CwServiceTicketBoard } from "../../generated/prisma/client";
import { ServiceTicketBoard as ApiServiceTicketBoard } from "../../../api/generated/prisma/client";
import { Translation } from "./types";
export const serviceTicketBoardTranslation: Translation<
CwServiceTicketBoard,
ApiServiceTicketBoard
> = {
values: [
{ from: "srBoardRecId", to: "id" },
{
from: "boardName",
to: "name",
process: (value) => (value ? value : "Unknown Board"),
},
{
from: "timeBillableFlag",
to: "timeBillableFlag",
process: (value) => Boolean(value),
},
{
from: "expBillableFlag",
to: "expenseBillableFlag",
process: (value) => Boolean(value),
},
{
from: "prodBillableFlag",
to: "productBillableFlag",
process: (value) => Boolean(value),
},
{
from: "timeInvoiceFlag",
to: "timeInvoiceableFlag",
process: (value) => Boolean(value),
},
{
from: "expInvoiceFlag",
to: "expenseInvoiceableFlag",
process: (value) => Boolean(value),
},
{
from: "prodInvoiceFlag",
to: "productInvoiceableFlag",
process: (value) => Boolean(value),
},
{
from: "autoAssignNewFlag",
to: "autoAssignNewFlag",
process: (value) => Boolean(value),
},
{
from: "autoAssignEcFlag",
to: "autoAssignEmailCreatedFlag",
process: (value) => Boolean(value),
},
{
from: "autoAssignPortalFlag",
to: "autoAssignPortalCreatedFlag",
process: (value) => Boolean(value),
},
{ from: "projectFlag", to: "projectFlag" },
{
from: "lockDescFlag",
to: "lockDescriptionFlag",
process: (value) => Boolean(value),
},
{
from: "emailContactFlag",
to: "emailContactFlag",
process: (value) => Boolean(value),
},
{
from: "emailResourceFlag",
to: "emailResourceFlag",
process: (value) => Boolean(value),
},
{ from: "resolutionSort", to: "resolutionSortOrder" },
{ from: "internalAnalysisSort", to: "internalAnalysisSortOrder" },
{ from: "ownerLevelRecId", to: "locationId" },
{ from: "enteredBy", to: "createdById" },
{ from: "updatedBy", to: "updatedById" },
{ from: "dateEnteredUtc", to: "createdAt" },
{ from: "lastUpdatedUtc", to: "updatedAt" },
],
};
@@ -0,0 +1,27 @@
import { SrImpact as CwServiceTicketImpact } from "../../generated/prisma/client";
import { ServiceTicketImpact as ApiServiceTicketImpact } from "../../../api/generated/prisma/client";
import { Translation } from "./types";
export const serviceTicketImpactTranslation: Translation<
CwServiceTicketImpact,
ApiServiceTicketImpact
> = {
values: [
{ from: "srImpactRecId", to: "id" },
{
from: "impactName",
to: "name",
process: (value) => (value ? value : "Unknown Impact"),
},
{ from: "impactDesc", to: "description" },
{
from: "defaultFlag",
to: "defaultFlag",
process: (value) => Boolean(value),
},
{ from: "enteredBy", to: "createdById" },
{ from: "updatedBy", to: "updatedById" },
{ from: "dateEnteredUtc", to: "createdAt" },
{ from: "lastUpdatedUtc", to: "updatedAt" },
],
};
@@ -0,0 +1,23 @@
import { SrLocation as CwServiceTicketLocation } from "../../generated/prisma/client";
import { ServiceTicketLocation as ApiServiceTicketLocation } from "../../../api/generated/prisma/client";
import { Translation } from "./types";
export const serviceTicketLocationTranslation: Translation<
CwServiceTicketLocation,
ApiServiceTicketLocation
> = {
values: [
{ from: "srLocationRecId", to: "id" },
{
from: "description",
to: "name",
process: (value) => (value ? value : "Unknown Location"),
},
{ from: "description", to: "description" },
{ from: "defaultFlag", to: "defaultFlag" },
{ from: "enteredBy", to: "createdById" },
{ from: "updatedBy", to: "updatedById" },
{ from: "dateEnteredUtc", to: "createdAt" },
{ from: "lastUpdatedUtc", to: "updatedAt" },
],
};
@@ -0,0 +1,74 @@
import { TicketNote as CwTicketNote } from "../../generated/prisma/client";
import { ServiceTicketNote as ApiServiceTicketNote } from "../../../api/generated/prisma/client";
import { Translation, skipRow } from "./types";
import { TranslationContext } from "./context";
export const serviceTicketNoteTranslation: Translation<
CwTicketNote,
ApiServiceTicketNote,
TranslationContext
> = {
values: [
{ from: "ticketNoteRecId", to: "id" },
{
from: "srServiceRecId",
to: "serviceTicketId",
process: (value: number | null, context: TranslationContext) => {
if (!value) {
skipRow("ServiceTicketNote missing srServiceRecId");
}
if (!context.serviceTicketIds.has(value)) {
skipRow(
`ServiceTicketNote parent ticket missing: ${value}`
);
}
return value;
},
},
{
from: "notesMarkdown",
to: "notesMd",
process: (value: string | null) => value || "",
},
{
from: "notes",
to: "notes",
process: (value: string | null) => value || "",
},
{
from: "memberRecId",
to: "authorId",
process: (value: number | null, context: TranslationContext) => {
if (!value) {
skipRow("ServiceTicketNote missing memberRecId");
}
const userId = context.usersByMemberRecId.get(value);
if (!userId) {
skipRow(
`Cannot find user mapping for memberRecId: ${value}`
);
}
return userId;
},
},
{ from: "problemFlag", to: "problemFlag" },
{ from: "resolutionFlag", to: "resolutionFlag" },
{ from: "internalAnalysisFlag", to: "internalAnalysisFlag" },
{ from: "internalMemberFlag", to: "internalMemberFlag" },
{ from: "mergedFlag", to: "mergedFlag" },
{ from: "bundledFlag", to: "bundledFlag" },
{ from: "createdByParentFlag", to: "createdByParentFlag" },
{
from: "dateCreatedUtc",
to: "createdAt",
process: (value: Date | null) => value || null,
},
{
from: "lastUpdatedUtc",
to: "updatedAt",
process: (value: Date | null) => value || null,
},
],
};
@@ -0,0 +1,28 @@
import { SrUrgency as CwServiceTicketPriority } from "../../generated/prisma/client";
import { ServiceTicketPriority as ApiServiceTicketPriority } from "../../../api/generated/prisma/client";
import { Translation } from "./types";
export const serviceTicketPriorityTranslation: Translation<
CwServiceTicketPriority,
ApiServiceTicketPriority
> = {
values: [
{ from: "srUrgencyRecId", to: "id" },
{
from: "description",
to: "name",
process: (value) => (value ? value : "Unknown Priority"),
},
{ from: "description", to: "description" },
{ from: "color", to: "color" },
{
from: "defaultFlag",
to: "defaultFlag",
process: (value) => Boolean(value),
},
{ from: "createdBy", to: "createdById" },
{ from: "updatedBy", to: "updatedById" },
{ from: "dateCreatedUtc", to: "createdAt" },
{ from: "lastUpdatedUtc", to: "updatedAt" },
],
};
@@ -0,0 +1,27 @@
import { SrSeverity as CwServiceTicketSeverity } from "../../generated/prisma/client";
import { ServiceTicketSeverity as ApiServiceTicketSeverity } from "../../../api/generated/prisma/client";
import { Translation } from "./types";
export const serviceTicketSeverityTranslation: Translation<
CwServiceTicketSeverity,
ApiServiceTicketSeverity
> = {
values: [
{ from: "srSeverityRecId", to: "id" },
{
from: "severityName",
to: "name",
process: (value) => (value ? value : "Unknown Severity"),
},
{ from: "severityDesc", to: "description" },
{
from: "defaultFlag",
to: "defaultFlag",
process: (value) => Boolean(value),
},
{ from: "enteredBy", to: "createdById" },
{ from: "updatedBy", to: "updatedById" },
{ from: "dateEnteredUtc", to: "createdAt" },
{ from: "lastUpdatedUtc", to: "updatedAt" },
],
};
@@ -0,0 +1,23 @@
import { SrSource as CwServiceTicketSource } from "../../generated/prisma/client";
import { ServiceTicketSource as ApiServiceTicketSource } from "../../../api/generated/prisma/client";
import { Translation } from "./types";
export const serviceTicketSourceTranslation: Translation<
CwServiceTicketSource,
ApiServiceTicketSource
> = {
values: [
{ from: "srSourceRecId", to: "id" },
{
from: "description",
to: "name",
process: (value) => (value ? value : "Unknown Source"),
},
{ from: "description", to: "description" },
{ from: "defaultFlag", to: "defaultFlag" },
{ from: "enteredBy", to: "createdById" },
{ from: "updatedBy", to: "updatedById" },
{ from: "dateEnteredUtc", to: "createdAt" },
{ from: "lastUpdatedUtc", to: "updatedAt" },
],
};
@@ -0,0 +1,23 @@
import { SrType as CwServiceTicketType } from "../../generated/prisma/client";
import { ServiceTicketType as ApiServiceTicketType } from "../../../api/generated/prisma/client";
import { Translation } from "./types";
export const serviceTicketTypeTranslation: Translation<
CwServiceTicketType,
ApiServiceTicketType
> = {
values: [
{ from: "srTypeRecId", to: "id" },
{
from: "description",
to: "name",
process: (value) => (value ? value : "Unknown Type"),
},
{ from: "description", to: "description" },
{ from: "inactiveFlag", to: "inactiveFlag" },
{ from: "enteredBy", to: "createdById" },
{ from: "updatedBy", to: "updatedById" },
{ from: "dateEnteredUtc", to: "createdAt" },
{ from: "lastUpdatedUtc", to: "updatedAt" },
],
};
+197
View File
@@ -0,0 +1,197 @@
import { SrService as CwSrService } from "../../generated/prisma/client";
import { ServiceTicket as ApiServiceTicket } from "../../../api/generated/prisma/client";
import { Translation, skipRow } from "./types";
import { TranslationContext } from "./context";
const toBillingMethod = (
value: string | null | undefined
): "ACTUAL_RATES" | "FIXED_FEE" | "NOT_TO_EXCEED" | "OVERRIDE_RATE" => {
if (!value) return "ACTUAL_RATES";
const normalized = value.trim().toUpperCase();
if (normalized === "A") return "ACTUAL_RATES";
if (normalized === "F") return "FIXED_FEE";
if (normalized === "N") return "NOT_TO_EXCEED";
if (normalized === "O") return "OVERRIDE_RATE";
// Keyword fallback for unexpected legacy values.
if (normalized.includes("FIX")) return "FIXED_FEE";
if (normalized.includes("EXCEED")) return "NOT_TO_EXCEED";
if (normalized.includes("OVERRIDE")) return "OVERRIDE_RATE";
return "ACTUAL_RATES";
};
const toBillingType = (value: string | undefined): "STANDARD" | "PROJECT" => {
if (!value) return "STANDARD";
const normalized = value.trim().toUpperCase();
if (normalized.includes("PROJECT")) return "PROJECT";
return "STANDARD";
};
const resolveUserId = (
value: string | null | undefined,
context: TranslationContext
): string | null => {
if (!value) return null;
const byIdentifier = context.usersByIdentifier.get(value);
if (byIdentifier) return byIdentifier;
const asRecId = Number.parseInt(value, 10);
if (!Number.isNaN(asRecId)) {
return context.usersByMemberRecId.get(asRecId) ?? null;
}
return null;
};
export const serviceTicketTranslation: Translation<
CwSrService,
ApiServiceTicket,
TranslationContext
> = {
values: [
{ from: "srServiceRecId", to: "id" },
{
from: "summary",
to: "summary",
process: (value) => {
if (!value || value.trim() === "") {
skipRow(
"ServiceTicket summary is required and cannot be empty"
);
}
return value;
},
},
{
from: "srLocationRecId",
to: "locationId",
process: (value) => {
if (value == null) {
skipRow("ServiceTicket locationId missing");
}
return value;
},
},
{
from: "srSourceRecId",
to: "sourceId",
process: (value) => {
if (value == null) {
skipRow("ServiceTicket sourceId missing");
}
return value;
},
},
{
from: "srUrgencyRecId",
to: "priorityId",
},
{
from: "srSeverityRecId",
to: "severityId",
process: (value) => {
if (value == null) {
skipRow("ServiceTicket severityId missing");
}
return value;
},
},
{
from: "srImpactRecId",
to: "impactId",
process: (value) => {
if (value == null) {
skipRow("ServiceTicket impactId missing");
}
return value;
},
},
{
from: "srBoardRecId",
to: "serviceTicketBoardId",
process: (value) => (value == null ? null : value),
},
{ from: "companyRecId", to: "companyId" },
{ from: "contactRecId", to: "contactId" },
{ from: "companyAddressRecId", to: "companyAddressId" },
{ from: "billingCompanyRecId", to: "billingCompanyId" },
{ from: "billingAddressRecId", to: "billingAddressId" },
{
from: "ticketOwnerRecId",
to: "ticketOwnerId",
process: (value, context) => {
if (!value) return null;
return context.usersByMemberRecId.get(value) || null;
},
},
{
from: "enteredBy",
to: "createdById",
process: (value, context) => {
return resolveUserId(value, context);
},
},
{
from: "updatedBy",
to: "updatedById",
process: (value, context) => {
return resolveUserId(value, context);
},
},
{
from: "closedBy",
to: "closedById",
process: (value, context) => {
return resolveUserId(value, context);
},
},
{
from: "dateEnteredUtc",
to: "createdAt",
process: (value) => value || null,
},
{
from: "lastUpdatedUtc",
to: "updatedAt",
process: (value) => value || null,
},
{ from: "dateClosedUtc", to: "closedAt" },
{
from: "isClosedFlag",
to: "closedFlag",
},
{
from: "billMethod",
to: "billingMethod",
process: (value, context) => {
return toBillingMethod(value);
},
},
{
from: "srServiceRecId",
to: "billingType",
process: (value, context) => {
return toBillingType(context.billingTypeByTicketId.get(value) ?? undefined);
},
},
{
from: "srServiceRecId",
to: "billingInstructions",
process: (value, context) => {
return context.billingInstructionsByTicketId.get(value) ?? null;
},
},
{ from: "poNumber", to: "poNumber" },
{
from: "billingAmount",
to: "billingAmount",
process: (value) => (value == null ? 0 : Number(value)),
},
{ from: "rejectedFlag", to: "rejectedFlag" },
{ from: "publishFlag", to: "publishFlag" },
{ from: "redFlag", to: "redFlag" },
],
};
+47
View File
@@ -0,0 +1,47 @@
import {
TaxCode as CwTaxCode,
TaxCodeLevel as CwTaxCodeLevel,
} from "../../generated/prisma/client";
import { TaxCode as ApiTaxCode } from "../../../api/generated/prisma/client";
import { Translation } from "./types";
import { skipRow } from "./types";
type CwTaxCodeWithLevels = CwTaxCode & {
levels: CwTaxCodeLevel[];
};
export const taxCodeTranslation: Translation<
CwTaxCodeWithLevels,
ApiTaxCode
> = {
values: [
{ from: "taxCodeRecId", to: "id" },
{
from: "taxCodeId",
to: "code",
process: (value) => value ?? skipRow("taxCodeId is null"),
},
{
from: "codeCaption",
to: "codeCaption",
process: (value, _, row) =>
value ?? (row.taxCodeId as string | null) ?? "",
},
{ from: "description", to: "description" },
{
from: "taxCodeRecId",
to: "rate",
process: (_, __, row) => {
const level = (row as unknown as CwTaxCodeWithLevels).levels.find(
(l) => l.taxXref !== null
);
return level ? Number(level.taxRate) : null;
},
},
{ from: "defaultFlag", to: "defaultFlag" },
{ from: "enteredBy", to: "createdBy" },
{ from: "updatedBy", to: "updatedBy" },
{ from: "dateEnteredUtc", to: "createdAt" },
{ from: "lastUpdatedUtc", to: "updatedAt" },
],
};
+48 -10
View File
@@ -1,18 +1,56 @@
/**
* kF - keys From
* kT - keys To
* kC - extra context data available to process functions
*/
export type TranslationEntry<kF, kT> = {
export type TranslationEntry<kF, kT, kC = unknown> = {
from: keyof kF;
to: keyof kT;
} & (kF extends kT
? {
process?: (value: kF[keyof kF]) => kT[keyof kT];
}
: {
process: (value: kF[keyof kF]) => kT[keyof kT];
});
} & {
[kFrom in keyof kF]: {
[kTo in keyof kT]: {
from: kFrom;
to: kTo;
} & (kF[kFrom] extends kT[kTo]
? {
process?: (value: kF[kFrom], context: kC, row: kF) => kT[kTo];
}
: {
process: (value: kF[kFrom], context: kC, row: kF) => kT[kTo];
});
}[keyof kT];
}[keyof kF];
export interface Translation<kF, kT> {
values: TranslationEntry<kF, kT>[];
export interface Translation<kF, kT, kC = unknown> {
values: TranslationEntry<kF, kT, kC>[];
}
/**
* Represents the outcome of translating a single row.
* - ok: true → translation succeeded, `value` contains the translated row
* - ok: false → row should be skipped, `reason` explains why
*/
export type TranslationResult<T> =
| { ok: true; value: T }
| { ok: false; reason: string };
/**
* Sentinel class thrown by `skipRow()` inside process functions.
* Caught by `translateRow()` and converted to a `TranslationResult`.
* This avoids string-prefix matching on generic Error messages.
*/
export class SkipRowError extends Error {
readonly __brand = "SkipRowError" as const;
constructor(reason: string) {
super(reason);
this.name = "SkipRowError";
}
}
/**
* Convenience helper for process functions to signal that a row
* should be skipped. Preferred over `throw new Error("SKIP_ROW: …")`.
*/
export function skipRow(reason: string): never {
throw new SkipRowError(reason);
}
+76
View File
@@ -0,0 +1,76 @@
import { Member as CwMember } from "../../generated/prisma/client";
import { User as ApiUser } from "../../../api/generated/prisma/client";
import { Translation, skipRow } from "./types";
const isValidEmail = (value: string | null): boolean => {
const normalized = value?.trim().toLowerCase();
return !!normalized && normalized.includes("@");
};
const isFullMember = (row: CwMember): boolean =>
row.memberClass === "F" && isValidEmail(row.emailAddress);
/**
* For full members, use their real email.
* For hidden members (non-F or missing valid email), generate a stable
* placeholder that satisfies the unique non-null login/email constraints.
*/
const resolveEmail = (row: CwMember): string => {
if (isFullMember(row)) {
return row.emailAddress!.trim().toLowerCase();
}
return `${row.memberId}@cw.local`;
};
export const userTranslation: Translation<CwMember, ApiUser> = {
values: [
{
from: "memberId",
to: "login",
// Use memberId@cw.local as a stable, unique login for all synced members.
// The real email goes into `email`; Microsoft OAuth overwrites `login` with
// the UPN on first login so this value is only a placeholder.
process: (value) => `${value}@cw.local`,
},
{
from: "firstName",
to: "firstName",
process: (value) => (value && value.trim() ? value.trim() : "Unknown"),
},
{
from: "lastName",
to: "lastName",
process: (value) => (value && value.trim() ? value.trim() : "Unknown"),
},
{
from: "emailAddress",
to: "email",
process: (_value, _context, row) => resolveEmail(row),
},
{ from: "memberRecId", to: "cwMemberId" },
{
from: "memberId",
to: "cwIdentifier",
process: (value) => {
const normalized = value?.trim();
return normalized ? normalized : null;
},
},
{
from: "inactiveFlag",
to: "active",
process: (value) => !value,
},
{
from: "memberClass",
to: "hidden",
process: (_value, _context, row) => !isFullMember(row),
},
{
from: "dateHire",
to: "createdAt",
process: (value, _context, row) => value ?? row.lastUpdatedUtc,
},
{ from: "lastUpdatedUtc", to: "updatedAt" },
],
};
+42
View File
@@ -0,0 +1,42 @@
import { WarehouseBin as CwWarehouseBin } from "../../generated/prisma/client";
import { WarehouseBin as ApiWarehouseBin } from "../../../api/generated/prisma/client";
import { Translation } from "./types";
export const warehouseBinTranslation: Translation<
CwWarehouseBin,
ApiWarehouseBin
> = {
values: [
{ from: "warehouseBinRecId", to: "id" },
{
from: "description",
to: "name",
process: (value) => (value ? value : "Unlabeled Bin"),
},
{ from: "description", to: "description" },
{
from: "minQuantity",
to: "minQuantity",
process: (value) => (value == null ? 0 : Number(value)),
},
{
from: "maxQuantity",
to: "maxQuantity",
process: (value) => (value == null ? 0 : Number(value)),
},
{
from: "inactiveFlag",
to: "inactiveFlag",
process: (value) => Boolean(value),
},
{
from: "defaultFlag",
to: "defaultFlag",
process: (value) => Boolean(value),
},
{ from: "updatedBy", to: "updatedById" },
{ from: "enteredBy", to: "createdById" },
{ from: "lastUpdatedUtc", to: "updatedAt" },
{ from: "dateEnteredUtc", to: "createdAt" },
],
};