601 lines
21 KiB
TypeScript
601 lines
21 KiB
TypeScript
/**
|
||
* Test Script: CW Forecast Item Edit & Partial Cancellation
|
||
*
|
||
* This script performs read-write operations against the ConnectWise API:
|
||
*
|
||
* 1. Search all open opportunities for a forecast item with description
|
||
* matching "labor Special Order" (case-insensitive).
|
||
* 2. Report the current state of that item (price, cost, qty, etc.).
|
||
* 3. PATCH the item: revenue → 72,000 | cost → 8,500 | quantity → 67
|
||
* 4. Verify the update by re-fetching the forecast.
|
||
* 5. Cancel 13 units via the linked procurement product
|
||
* (partial cancellation: quantityCancelled = 13).
|
||
* 6. Verify the cancellation by re-fetching procurement data.
|
||
* 7. Report on every step.
|
||
*
|
||
* Usage: bun run test-cw-edit-item.ts
|
||
*/
|
||
|
||
import axios from "axios";
|
||
|
||
const cw = axios.create({
|
||
baseURL: "https://ttscw.totaltech.net/v4_6_release/apis/3.0/",
|
||
headers: {
|
||
Authorization: `Basic ${process.env.CW_BASIC_TOKEN}`,
|
||
clientId: `${process.env.CW_CLIENT_ID}`,
|
||
"Content-Type": "application/json",
|
||
},
|
||
timeout: 30_000,
|
||
});
|
||
|
||
// ── Helpers ───────────────────────────────────────────────────────────────────
|
||
|
||
const log = (label: string, ...args: unknown[]) =>
|
||
console.log(`\n[${label}]`, ...args);
|
||
|
||
const divider = () => console.log("─".repeat(72));
|
||
|
||
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
|
||
|
||
const fmt = (n: number) =>
|
||
n.toLocaleString("en-US", {
|
||
minimumFractionDigits: 2,
|
||
maximumFractionDigits: 2,
|
||
});
|
||
|
||
// ── Types (minimal, for this script) ──────────────────────────────────────────
|
||
|
||
interface ForecastItem {
|
||
id: number;
|
||
forecastDescription: string;
|
||
productDescription: string;
|
||
quantity: number;
|
||
revenue: number;
|
||
cost: number;
|
||
margin: number;
|
||
forecastType: string;
|
||
sequenceNumber: number;
|
||
catalogItem?: { id: number; identifier: string };
|
||
status?: { id: number; name: string };
|
||
opportunity?: { id: number; name: string };
|
||
[key: string]: unknown;
|
||
}
|
||
|
||
interface Forecast {
|
||
id: number;
|
||
forecastItems: ForecastItem[];
|
||
[key: string]: unknown;
|
||
}
|
||
|
||
interface ProcurementProduct {
|
||
id: number;
|
||
forecastDetailId: number;
|
||
description: string;
|
||
quantity: number;
|
||
price: number;
|
||
cost: number;
|
||
cancelledFlag: boolean;
|
||
quantityCancelled: number;
|
||
cancelledReason: string | null;
|
||
cancelledBy: string | null;
|
||
cancelledDate: string | null;
|
||
opportunity?: { id: number };
|
||
[key: string]: unknown;
|
||
}
|
||
|
||
// ── Main ──────────────────────────────────────────────────────────────────────
|
||
|
||
async function main() {
|
||
divider();
|
||
log("START", "CW Forecast Item Edit & Cancellation Test");
|
||
log("START", `Timestamp: ${new Date().toISOString()}`);
|
||
divider();
|
||
|
||
// ── Step 1: Find the "labor Special Order" forecast item ────────────────
|
||
|
||
const OPP_ID = 5150;
|
||
log(
|
||
"SEARCH",
|
||
`Looking for forecast item matching "labor Special Order" on opportunity ${OPP_ID}...`,
|
||
);
|
||
|
||
// Fetch the forecast for opportunity 5150 directly
|
||
let targetOppId: number = OPP_ID;
|
||
let targetItem: ForecastItem | null = null;
|
||
let targetForecast: Forecast | null = null;
|
||
|
||
const forecastRes = await cw.get(`/sales/opportunities/${OPP_ID}/forecast`);
|
||
targetForecast = forecastRes.data as Forecast;
|
||
const match = (targetForecast.forecastItems ?? []).find(
|
||
(fi: ForecastItem) =>
|
||
fi.forecastDescription?.toLowerCase().includes("special order") ||
|
||
fi.productDescription?.toLowerCase().includes("special order"),
|
||
);
|
||
|
||
if (match) {
|
||
targetItem = match;
|
||
log("SEARCH", `✓ FOUND forecast item on opportunity ${OPP_ID}`);
|
||
}
|
||
|
||
if (!targetItem || !targetForecast) {
|
||
log(
|
||
"SEARCH",
|
||
`✗ No "labor Special Order" item found on opportunity ${OPP_ID}.`,
|
||
);
|
||
log("SEARCH", "All forecast items on this opportunity:");
|
||
for (const fi of targetForecast.forecastItems ?? []) {
|
||
console.log(
|
||
` id=${fi.id} "${fi.forecastDescription}" / "${fi.productDescription}"`,
|
||
);
|
||
}
|
||
log("SEARCH", "Aborting.");
|
||
process.exit(1);
|
||
}
|
||
|
||
// ── Step 2: Report current state ────────────────────────────────────────
|
||
|
||
divider();
|
||
log("CURRENT STATE", "Forecast item details BEFORE edit:");
|
||
console.log(` Opportunity ID: ${targetOppId}`);
|
||
console.log(` Forecast Item ID: ${targetItem.id}`);
|
||
console.log(` Forecast Description: ${targetItem.forecastDescription}`);
|
||
console.log(` Product Description: ${targetItem.productDescription}`);
|
||
console.log(
|
||
` Catalog Item: ${targetItem.catalogItem?.identifier ?? "(none)"} (cwId=${targetItem.catalogItem?.id ?? "N/A"})`,
|
||
);
|
||
console.log(` Forecast Type: ${targetItem.forecastType}`);
|
||
console.log(
|
||
` Status: ${targetItem.status?.name ?? "?"} (id=${targetItem.status?.id ?? "?"})`,
|
||
);
|
||
console.log(` Sequence Number: ${targetItem.sequenceNumber}`);
|
||
console.log(` ──────────────────────────────────`);
|
||
console.log(` Quantity: ${targetItem.quantity}`);
|
||
console.log(` Revenue (Price): $${fmt(targetItem.revenue)}`);
|
||
console.log(` Cost: $${fmt(targetItem.cost)}`);
|
||
console.log(` Margin: $${fmt(targetItem.margin)}`);
|
||
|
||
// Also report all items on this opportunity for context
|
||
const allItems = targetForecast.forecastItems ?? [];
|
||
log(
|
||
"CONTEXT",
|
||
`Total forecast items on this opportunity: ${allItems.length}`,
|
||
);
|
||
for (const fi of allItems) {
|
||
const marker = fi.id === targetItem.id ? " ◀ TARGET" : "";
|
||
console.log(
|
||
` [${fi.sequenceNumber}] id=${fi.id} "${fi.forecastDescription}" ` +
|
||
`qty=${fi.quantity} rev=$${fmt(fi.revenue)} cost=$${fmt(fi.cost)}${marker}`,
|
||
);
|
||
}
|
||
|
||
// ── Step 3: PATCH the forecast item ─────────────────────────────────────
|
||
|
||
divider();
|
||
const UNIT_PRICE = 72_000;
|
||
const UNIT_COST = 8_500;
|
||
const QTY = 67;
|
||
const TOTAL_REVENUE = UNIT_PRICE * QTY; // $4,824,000
|
||
const TOTAL_COST = UNIT_COST * QTY; // $569,500
|
||
|
||
log("EDIT", "Patching forecast item...");
|
||
log(
|
||
"EDIT",
|
||
` Unit price: $${fmt(UNIT_PRICE)} × ${QTY} = $${fmt(TOTAL_REVENUE)} (revenue)`,
|
||
);
|
||
log(
|
||
"EDIT",
|
||
` Unit cost: $${fmt(UNIT_COST)} × ${QTY} = $${fmt(TOTAL_COST)} (cost)`,
|
||
);
|
||
log("EDIT", ` Quantity: ${QTY}`);
|
||
|
||
// Find the index of our target item in the forecast array
|
||
const forecastItems = targetForecast.forecastItems ?? [];
|
||
const targetIdx = forecastItems.findIndex((fi) => fi.id === targetItem!.id);
|
||
|
||
if (targetIdx === -1) {
|
||
log(
|
||
"EDIT",
|
||
"✗ Could not find target item index in forecast array. Aborting.",
|
||
);
|
||
process.exit(1);
|
||
}
|
||
|
||
log("EDIT", `Target item is at index ${targetIdx} in forecastItems array.`);
|
||
|
||
const patchOps = [
|
||
{
|
||
op: "replace",
|
||
path: `/forecastItems/${targetIdx}/revenue`,
|
||
value: TOTAL_REVENUE,
|
||
},
|
||
{
|
||
op: "replace",
|
||
path: `/forecastItems/${targetIdx}/cost`,
|
||
value: TOTAL_COST,
|
||
},
|
||
{ op: "replace", path: `/forecastItems/${targetIdx}/quantity`, value: QTY },
|
||
];
|
||
|
||
log("EDIT", "Patch operations:");
|
||
for (const op of patchOps) {
|
||
console.log(` ${op.op} ${op.path} → ${op.value}`);
|
||
}
|
||
|
||
try {
|
||
const patchRes = await cw.patch(
|
||
`/sales/opportunities/${targetOppId}/forecast`,
|
||
patchOps,
|
||
);
|
||
const updatedForecast: Forecast = patchRes.data;
|
||
const updatedItem = (updatedForecast.forecastItems ?? [])[targetIdx];
|
||
|
||
if (!updatedItem) {
|
||
log("EDIT", "✗ Item not found at expected index after PATCH.");
|
||
} else {
|
||
log("EDIT", "✓ PATCH successful. Updated item:");
|
||
console.log(` Forecast Item ID: ${updatedItem.id}`);
|
||
console.log(` Forecast Description: ${updatedItem.forecastDescription}`);
|
||
console.log(` Quantity: ${updatedItem.quantity}`);
|
||
console.log(` Revenue (Price): $${fmt(updatedItem.revenue)}`);
|
||
console.log(` Cost: $${fmt(updatedItem.cost)}`);
|
||
console.log(` Margin: $${fmt(updatedItem.margin)}`);
|
||
|
||
// Verify values match what we set
|
||
const checks = [
|
||
{
|
||
field: "revenue",
|
||
expected: TOTAL_REVENUE,
|
||
actual: updatedItem.revenue,
|
||
},
|
||
{ field: "cost", expected: TOTAL_COST, actual: updatedItem.cost },
|
||
{ field: "quantity", expected: QTY, actual: updatedItem.quantity },
|
||
];
|
||
|
||
log("VERIFY EDIT", "Checking values match requested:");
|
||
for (const check of checks) {
|
||
const ok = check.actual === check.expected;
|
||
console.log(
|
||
` ${ok ? "✓" : "✗"} ${check.field}: expected=${check.expected}, actual=${check.actual}`,
|
||
);
|
||
}
|
||
|
||
// Update our reference for the cancellation step
|
||
targetItem = updatedItem;
|
||
}
|
||
} catch (err: any) {
|
||
log("EDIT", `✗ PATCH failed: ${err.response?.status ?? err.message}`);
|
||
if (err.response?.data) {
|
||
console.log(" Response:", JSON.stringify(err.response.data, null, 2));
|
||
}
|
||
|
||
// If quantity PATCH failed (read-only), try without quantity
|
||
if (err.response?.status === 400 || err.response?.status === 422) {
|
||
log(
|
||
"EDIT",
|
||
"Retrying without quantity (may be read-only on forecast items)...",
|
||
);
|
||
const retryOps = patchOps.filter((op) => !op.path.endsWith("/quantity"));
|
||
try {
|
||
const retryRes = await cw.patch(
|
||
`/sales/opportunities/${targetOppId}/forecast`,
|
||
retryOps,
|
||
);
|
||
const retryForecast: Forecast = retryRes.data;
|
||
const retryItem = (retryForecast.forecastItems ?? [])[targetIdx];
|
||
|
||
if (retryItem) {
|
||
log(
|
||
"EDIT",
|
||
"✓ Retry PATCH successful (without quantity). Updated item:",
|
||
);
|
||
console.log(
|
||
` Quantity: ${retryItem.quantity} (unchanged — read-only)`,
|
||
);
|
||
console.log(` Revenue (Price): $${fmt(retryItem.revenue)}`);
|
||
console.log(` Cost: $${fmt(retryItem.cost)}`);
|
||
console.log(` Margin: $${fmt(retryItem.margin)}`);
|
||
targetItem = retryItem;
|
||
}
|
||
} catch (retryErr: any) {
|
||
log(
|
||
"EDIT",
|
||
`✗ Retry also failed: ${retryErr.response?.status ?? retryErr.message}`,
|
||
);
|
||
if (retryErr.response?.data) {
|
||
console.log(
|
||
" Response:",
|
||
JSON.stringify(retryErr.response.data, null, 2),
|
||
);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// ── Step 4: Re-fetch and confirm final forecast state ───────────────────
|
||
|
||
divider();
|
||
log("RE-FETCH", "Fetching forecast to confirm final state...");
|
||
await sleep(500);
|
||
|
||
const confirmRes = await cw.get(
|
||
`/sales/opportunities/${targetOppId}/forecast`,
|
||
);
|
||
const confirmedForecast: Forecast = confirmRes.data;
|
||
const confirmedItem = (confirmedForecast.forecastItems ?? []).find(
|
||
(fi) => fi.id === targetItem!.id,
|
||
);
|
||
|
||
if (confirmedItem) {
|
||
log("CONFIRMED STATE", "Forecast item after edit:");
|
||
console.log(` Forecast Item ID: ${confirmedItem.id}`);
|
||
console.log(` Forecast Description: ${confirmedItem.forecastDescription}`);
|
||
console.log(` Quantity: ${confirmedItem.quantity}`);
|
||
console.log(` Revenue (Price): $${fmt(confirmedItem.revenue)}`);
|
||
console.log(` Cost: $${fmt(confirmedItem.cost)}`);
|
||
console.log(` Margin: $${fmt(confirmedItem.margin)}`);
|
||
} else {
|
||
log(
|
||
"CONFIRMED STATE",
|
||
"⚠ Could not find item by original ID — it may have been regenerated.",
|
||
);
|
||
log("CONFIRMED STATE", "All current forecast items:");
|
||
for (const fi of confirmedForecast.forecastItems ?? []) {
|
||
console.log(
|
||
` id=${fi.id} "${fi.forecastDescription}" qty=${fi.quantity} rev=$${fmt(fi.revenue)} cost=$${fmt(fi.cost)}`,
|
||
);
|
||
}
|
||
}
|
||
|
||
// ── Step 5: Cancel 13 items via procurement product ─────────────────────
|
||
|
||
divider();
|
||
log("CANCEL", "Cancelling 13 units on this item via procurement product...");
|
||
|
||
// First, find existing procurement products linked to this opportunity
|
||
const procRes = await cw.get(
|
||
`/procurement/products?conditions=${encodeURIComponent(`opportunity/id=${targetOppId}`)}&pageSize=1000`,
|
||
);
|
||
const procProducts: ProcurementProduct[] = procRes.data;
|
||
|
||
log(
|
||
"CANCEL",
|
||
`Found ${procProducts.length} procurement product(s) on this opportunity.`,
|
||
);
|
||
|
||
if (procProducts.length > 0) {
|
||
for (const pp of procProducts) {
|
||
console.log(
|
||
` Proc id=${pp.id} forecastDetailId=${pp.forecastDetailId} ` +
|
||
`"${pp.description}" qty=${pp.quantity} price=$${fmt(pp.price ?? 0)} ` +
|
||
`cancelled=${pp.cancelledFlag} qtyCancelled=${pp.quantityCancelled}`,
|
||
);
|
||
}
|
||
}
|
||
|
||
// Find the procurement product linked to our forecast item
|
||
const linkedProc = procProducts.find(
|
||
(pp) => pp.forecastDetailId === targetItem!.id,
|
||
);
|
||
|
||
if (linkedProc) {
|
||
log("CANCEL", `Found linked procurement product: id=${linkedProc.id}`);
|
||
log(
|
||
"CANCEL",
|
||
`Current state: cancelled=${linkedProc.cancelledFlag}, quantityCancelled=${linkedProc.quantityCancelled}`,
|
||
);
|
||
log("CANCEL", "Patching: quantityCancelled → 13, cancelledFlag → true");
|
||
|
||
try {
|
||
const cancelRes = await cw.patch(
|
||
`/procurement/products/${linkedProc.id}`,
|
||
[
|
||
{ op: "replace", path: "cancelledFlag", value: true },
|
||
{ op: "replace", path: "quantityCancelled", value: 13 },
|
||
{
|
||
op: "replace",
|
||
path: "cancelledReason",
|
||
value: "Test cancellation — 13 units",
|
||
},
|
||
],
|
||
);
|
||
|
||
log("CANCEL", "✓ Cancellation PATCH successful.");
|
||
console.log(` cancelledFlag: ${cancelRes.data.cancelledFlag}`);
|
||
console.log(` quantityCancelled: ${cancelRes.data.quantityCancelled}`);
|
||
console.log(` cancelledReason: ${cancelRes.data.cancelledReason}`);
|
||
console.log(
|
||
` cancelledBy: ${cancelRes.data.cancelledBy ?? "N/A"}`,
|
||
);
|
||
console.log(
|
||
` cancelledDate: ${cancelRes.data.cancelledDate ?? "N/A"}`,
|
||
);
|
||
} catch (err: any) {
|
||
log(
|
||
"CANCEL",
|
||
`✗ Cancellation PATCH failed: ${err.response?.status ?? err.message}`,
|
||
);
|
||
if (err.response?.data) {
|
||
console.log(" Response:", JSON.stringify(err.response.data, null, 2));
|
||
}
|
||
}
|
||
} else {
|
||
log(
|
||
"CANCEL",
|
||
`No procurement product linked to forecast item id=${targetItem!.id}.`,
|
||
);
|
||
log(
|
||
"CANCEL",
|
||
"Creating a procurement product first, then cancelling 13...",
|
||
);
|
||
|
||
try {
|
||
// Create a procurement product linked to this forecast item
|
||
const createProcRes = await cw.post("/procurement/products", {
|
||
catalogItem: targetItem!.catalogItem?.id
|
||
? { id: targetItem!.catalogItem.id }
|
||
: undefined,
|
||
description:
|
||
targetItem!.forecastDescription || targetItem!.productDescription,
|
||
quantity: targetItem!.quantity || 67,
|
||
price: targetItem!.revenue || 72_000,
|
||
cost: targetItem!.cost || 8_500,
|
||
billableOption: "Billable",
|
||
opportunity: { id: targetOppId },
|
||
forecastDetailId: targetItem!.id,
|
||
});
|
||
|
||
const newProc = createProcRes.data;
|
||
log("CANCEL", `✓ Created procurement product id=${newProc.id}`);
|
||
console.log(` forecastDetailId: ${newProc.forecastDetailId}`);
|
||
console.log(` description: ${newProc.description}`);
|
||
console.log(` quantity: ${newProc.quantity}`);
|
||
console.log(` price: $${fmt(newProc.price ?? 0)}`);
|
||
console.log(` cost: $${fmt(newProc.cost ?? 0)}`);
|
||
|
||
// Now cancel 13 units
|
||
log("CANCEL", "Patching procurement product: quantityCancelled → 13...");
|
||
const cancelRes = await cw.patch(`/procurement/products/${newProc.id}`, [
|
||
{ op: "replace", path: "cancelledFlag", value: true },
|
||
{ op: "replace", path: "quantityCancelled", value: 13 },
|
||
{
|
||
op: "replace",
|
||
path: "cancelledReason",
|
||
value: "Test cancellation — 13 units",
|
||
},
|
||
]);
|
||
|
||
log("CANCEL", "✓ Cancellation PATCH successful.");
|
||
console.log(` cancelledFlag: ${cancelRes.data.cancelledFlag}`);
|
||
console.log(` quantityCancelled: ${cancelRes.data.quantityCancelled}`);
|
||
console.log(` cancelledReason: ${cancelRes.data.cancelledReason}`);
|
||
console.log(
|
||
` cancelledBy: ${cancelRes.data.cancelledBy ?? "N/A"}`,
|
||
);
|
||
console.log(
|
||
` cancelledDate: ${cancelRes.data.cancelledDate ?? "N/A"}`,
|
||
);
|
||
} catch (err: any) {
|
||
log("CANCEL", `✗ Failed: ${err.response?.status ?? err.message}`);
|
||
if (err.response?.data) {
|
||
console.log(" Response:", JSON.stringify(err.response.data, null, 2));
|
||
}
|
||
}
|
||
}
|
||
|
||
// ── Step 6: Final verification ──────────────────────────────────────────
|
||
|
||
divider();
|
||
log("FINAL VERIFY", "Re-fetching all data for final report...");
|
||
await sleep(500);
|
||
|
||
// Re-fetch forecast
|
||
const finalForecastRes = await cw.get(
|
||
`/sales/opportunities/${targetOppId}/forecast`,
|
||
);
|
||
const finalForecast: Forecast = finalForecastRes.data;
|
||
const finalItem =
|
||
(finalForecast.forecastItems ?? []).find(
|
||
(fi) => fi.id === targetItem!.id,
|
||
) ??
|
||
(finalForecast.forecastItems ?? []).find(
|
||
(fi) =>
|
||
fi.forecastDescription?.toLowerCase().includes("special order") ||
|
||
fi.productDescription?.toLowerCase().includes("special order"),
|
||
);
|
||
|
||
// Re-fetch procurement
|
||
const finalProcRes = await cw.get(
|
||
`/procurement/products?conditions=${encodeURIComponent(`opportunity/id=${targetOppId}`)}&pageSize=1000`,
|
||
);
|
||
const finalProcs: ProcurementProduct[] = finalProcRes.data;
|
||
|
||
log("FINAL STATE — FORECAST ITEM", "");
|
||
if (finalItem) {
|
||
console.log(` Forecast Item ID: ${finalItem.id}`);
|
||
console.log(` Forecast Description: ${finalItem.forecastDescription}`);
|
||
console.log(` Quantity: ${finalItem.quantity}`);
|
||
console.log(` Revenue (Price): $${fmt(finalItem.revenue)}`);
|
||
console.log(` Cost: $${fmt(finalItem.cost)}`);
|
||
console.log(` Margin: $${fmt(finalItem.margin)}`);
|
||
} else {
|
||
console.log(" ⚠ Target item not found in final forecast.");
|
||
}
|
||
|
||
log("FINAL STATE — PROCUREMENT", `${finalProcs.length} product(s):`);
|
||
for (const pp of finalProcs) {
|
||
console.log(
|
||
` id=${pp.id} forecastDetailId=${pp.forecastDetailId} ` +
|
||
`"${pp.description}" qty=${pp.quantity} cancelled=${pp.cancelledFlag} ` +
|
||
`qtyCancelled=${pp.quantityCancelled} reason="${pp.cancelledReason ?? ""}"`,
|
||
);
|
||
}
|
||
|
||
// ── Summary ─────────────────────────────────────────────────────────────
|
||
|
||
divider();
|
||
log("SUMMARY", "");
|
||
|
||
// After cancelling 13 of 67, CW recalculates totals for remaining 54 units
|
||
const expectedFinalRevenue = Math.round(UNIT_PRICE * (QTY - 13) * 100) / 100;
|
||
const expectedFinalCost = Math.round(UNIT_COST * (QTY - 13) * 100) / 100;
|
||
|
||
const editOk = finalItem
|
||
? Math.abs(finalItem.revenue - expectedFinalRevenue) < 1 &&
|
||
Math.abs(finalItem.cost - expectedFinalCost) < 1
|
||
: false;
|
||
const qtyOk = finalItem ? finalItem.quantity === QTY : false;
|
||
|
||
if (finalItem) {
|
||
console.log(
|
||
` Expected final revenue ($${fmt(UNIT_PRICE)} × ${QTY - 13}): $${fmt(expectedFinalRevenue)}`,
|
||
);
|
||
console.log(
|
||
` Actual final revenue: $${fmt(finalItem.revenue)}`,
|
||
);
|
||
console.log(
|
||
` Expected final cost ($${fmt(UNIT_COST)} × ${QTY - 13}): $${fmt(expectedFinalCost)}`,
|
||
);
|
||
console.log(
|
||
` Actual final cost: $${fmt(finalItem.cost)}`,
|
||
);
|
||
}
|
||
const cancelOk = finalProcs.some(
|
||
(pp) =>
|
||
pp.forecastDetailId === targetItem!.id &&
|
||
pp.cancelledFlag === true &&
|
||
pp.quantityCancelled === 13,
|
||
);
|
||
|
||
console.log(
|
||
` Unit price $${fmt(UNIT_PRICE)}/ea: `,
|
||
editOk ? "✓ PASS" : "✗ FAIL",
|
||
);
|
||
console.log(
|
||
` Unit cost $${fmt(UNIT_COST)}/ea: `,
|
||
editOk ? "✓ PASS" : "✗ FAIL",
|
||
);
|
||
console.log(
|
||
` Quantity set to ${QTY}: `,
|
||
qtyOk ? "✓ PASS" : "✗ FAIL (may be read-only)",
|
||
);
|
||
console.log(
|
||
" 13 units cancelled: ",
|
||
cancelOk ? "✓ PASS" : "✗ FAIL",
|
||
);
|
||
|
||
const allPass = editOk && qtyOk && cancelOk;
|
||
divider();
|
||
log(
|
||
"RESULT",
|
||
allPass
|
||
? "✓ ALL CHECKS PASSED"
|
||
: "⚠ SOME CHECKS DID NOT PASS — review output above",
|
||
);
|
||
divider();
|
||
}
|
||
|
||
main().catch((err) => {
|
||
console.error("\n[FATAL]", err.response?.data ?? err.message);
|
||
process.exit(1);
|
||
});
|