all the haul
This commit is contained in:
@@ -0,0 +1,609 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { SkipRowError } from "../types";
|
||||
import { createTranslationContext, type TranslationContext } from "../context";
|
||||
|
||||
// ─── Test‑only 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", () => {
|
||||
// Lazy‑load to sidestep top‑level 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" },
|
||||
],
|
||||
};
|
||||
@@ -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" },
|
||||
],
|
||||
};
|
||||
@@ -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" },
|
||||
],
|
||||
};
|
||||
|
||||
@@ -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" },
|
||||
],
|
||||
};
|
||||
@@ -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" },
|
||||
],
|
||||
};
|
||||
@@ -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" },
|
||||
],
|
||||
};
|
||||
@@ -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" },
|
||||
],
|
||||
};
|
||||
@@ -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)),
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -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" },
|
||||
],
|
||||
};
|
||||
@@ -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" },
|
||||
],
|
||||
};
|
||||
@@ -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" },
|
||||
],
|
||||
};
|
||||
@@ -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" },
|
||||
],
|
||||
};
|
||||
@@ -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" },
|
||||
],
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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" },
|
||||
],
|
||||
};
|
||||
@@ -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" },
|
||||
],
|
||||
};
|
||||
Reference in New Issue
Block a user