Files
optima/api/tests/unit/controllers/ForecastProductController.test.ts

398 lines
15 KiB
TypeScript

import { describe, test, expect } from "bun:test";
import { ForecastProductController } from "../../../src/controllers/ForecastProductController";
import { buildMockCWForecastItem } from "../../setup";
describe("ForecastProductController", () => {
// -------------------------------------------------------------------
// Constructor
// -------------------------------------------------------------------
describe("constructor", () => {
test("sets core identification fields", () => {
const ctrl = new ForecastProductController(buildMockCWForecastItem());
expect(ctrl.cwForecastId).toBe(7001);
expect(ctrl.forecastDescription).toBe("Network Switch");
});
test("maps opportunity reference", () => {
const ctrl = new ForecastProductController(buildMockCWForecastItem());
expect(ctrl.opportunityCwId).toBe(1001);
expect(ctrl.opportunityName).toBe("Test Opportunity");
});
test("maps quantity", () => {
const ctrl = new ForecastProductController(buildMockCWForecastItem());
expect(ctrl.quantity).toBe(5);
});
test("maps status reference", () => {
const ctrl = new ForecastProductController(buildMockCWForecastItem());
expect(ctrl.statusCwId).toBe(1);
expect(ctrl.statusName).toBe("Won");
});
test("maps catalogItem reference", () => {
const ctrl = new ForecastProductController(buildMockCWForecastItem());
expect(ctrl.catalogItemCwId).toBe(500);
expect(ctrl.catalogItemIdentifier).toBe("USW-Pro-24");
});
test("maps product details", () => {
const ctrl = new ForecastProductController(buildMockCWForecastItem());
expect(ctrl.productDescription).toBe("UniFi Switch Pro 24");
expect(ctrl.productClass).toBe("Product");
expect(ctrl.forecastType).toBe("Product");
});
test("maps financials", () => {
const ctrl = new ForecastProductController(buildMockCWForecastItem());
expect(ctrl.revenue).toBe(2500.0);
expect(ctrl.cost).toBe(1800.0);
expect(ctrl.margin).toBe(700.0);
expect(ctrl.percentage).toBe(100);
});
test("maps boolean flags", () => {
const ctrl = new ForecastProductController(buildMockCWForecastItem());
expect(ctrl.includeFlag).toBe(true);
expect(ctrl.linkFlag).toBe(false);
expect(ctrl.recurringFlag).toBe(false);
expect(ctrl.taxableFlag).toBe(true);
});
test("maps sequence and sub number", () => {
const ctrl = new ForecastProductController(buildMockCWForecastItem());
expect(ctrl.sequenceNumber).toBe(1);
expect(ctrl.subNumber).toBe(0);
});
test("maps recurring fields", () => {
const ctrl = new ForecastProductController(buildMockCWForecastItem());
expect(ctrl.recurringRevenue).toBe(0);
expect(ctrl.recurringCost).toBe(0);
expect(ctrl.cycles).toBe(0);
});
test("sets cancellation defaults", () => {
const ctrl = new ForecastProductController(buildMockCWForecastItem());
expect(ctrl.cancelledFlag).toBe(false);
expect(ctrl.quantityCancelled).toBe(0);
expect(ctrl.cancelledReason).toBeNull();
expect(ctrl.cancelledBy).toBeNull();
expect(ctrl.cancelledDate).toBeNull();
});
test("sets inventory defaults", () => {
const ctrl = new ForecastProductController(buildMockCWForecastItem());
expect(ctrl.onHand).toBeNull();
expect(ctrl.inStock).toBeNull();
});
test("maps _info to cwLastUpdated", () => {
const ctrl = new ForecastProductController(buildMockCWForecastItem());
expect(ctrl.cwLastUpdated).toBeInstanceOf(Date);
expect(ctrl.cwUpdatedBy).toBe("jroberts");
});
test("handles missing optional fields", () => {
const ctrl = new ForecastProductController(
buildMockCWForecastItem({
opportunity: undefined,
status: undefined,
catalogItem: undefined,
_info: undefined,
}),
);
expect(ctrl.opportunityCwId).toBeNull();
expect(ctrl.statusCwId).toBeNull();
expect(ctrl.catalogItemCwId).toBeNull();
expect(ctrl.cwLastUpdated).toBeNull();
});
});
// -------------------------------------------------------------------
// applyCancellationData
// -------------------------------------------------------------------
describe("applyCancellationData()", () => {
test("applies cancellation data", () => {
const ctrl = new ForecastProductController(buildMockCWForecastItem());
ctrl.applyCancellationData({
cancelledFlag: true,
quantityCancelled: 3,
cancelledReason: "Out of stock",
cancelledBy: 42,
cancelledDate: "2026-02-20T00:00:00Z",
});
expect(ctrl.cancelledFlag).toBe(true);
expect(ctrl.quantityCancelled).toBe(3);
expect(ctrl.cancelledReason).toBe("Out of stock");
expect(ctrl.cancelledBy).toBe(42);
expect(ctrl.cancelledDate).toBeInstanceOf(Date);
});
test("handles partial cancellation data", () => {
const ctrl = new ForecastProductController(buildMockCWForecastItem());
ctrl.applyCancellationData({});
expect(ctrl.cancelledFlag).toBe(false);
expect(ctrl.quantityCancelled).toBe(0);
expect(ctrl.cancelledReason).toBeNull();
});
});
// -------------------------------------------------------------------
// applyProcurementCustomFields
// -------------------------------------------------------------------
describe("applyProcurementCustomFields()", () => {
test("sets productNarrative from custom field id 46", () => {
const ctrl = new ForecastProductController(buildMockCWForecastItem());
ctrl.applyProcurementCustomFields({
customFields: [{ id: 46, value: "Custom narrative text" }],
});
expect(ctrl.productNarrative).toBe("Custom narrative text");
});
test("does not overwrite productNarrative when field 46 is missing", () => {
const ctrl = new ForecastProductController(buildMockCWForecastItem());
ctrl.productNarrative = "existing";
ctrl.applyProcurementCustomFields({
customFields: [{ id: 99, value: "other" }],
});
expect(ctrl.productNarrative).toBe("existing");
});
test("handles empty customFields array", () => {
const ctrl = new ForecastProductController(buildMockCWForecastItem());
ctrl.applyProcurementCustomFields({ customFields: [] });
expect(ctrl.productNarrative).toBeNull();
});
test("handles undefined customFields", () => {
const ctrl = new ForecastProductController(buildMockCWForecastItem());
ctrl.applyProcurementCustomFields({});
expect(ctrl.productNarrative).toBeNull();
});
});
// -------------------------------------------------------------------
// applyInventoryData
// -------------------------------------------------------------------
describe("applyInventoryData()", () => {
test("sets onHand and inStock true when quantity > 0", () => {
const ctrl = new ForecastProductController(buildMockCWForecastItem());
ctrl.applyInventoryData({ onHand: 10 });
expect(ctrl.onHand).toBe(10);
expect(ctrl.inStock).toBe(true);
});
test("sets inStock false when onHand is 0", () => {
const ctrl = new ForecastProductController(buildMockCWForecastItem());
ctrl.applyInventoryData({ onHand: 0 });
expect(ctrl.onHand).toBe(0);
expect(ctrl.inStock).toBe(false);
});
});
// -------------------------------------------------------------------
// Computed properties
// -------------------------------------------------------------------
describe("computed properties", () => {
test("profit returns revenue - cost", () => {
const ctrl = new ForecastProductController(buildMockCWForecastItem());
expect(ctrl.profit).toBe(700.0);
});
test("cancelled returns false by default", () => {
const ctrl = new ForecastProductController(buildMockCWForecastItem());
expect(ctrl.cancelled).toBe(false);
});
test("cancelled returns true after applyCancellationData", () => {
const ctrl = new ForecastProductController(buildMockCWForecastItem());
ctrl.applyCancellationData({ cancelledFlag: true, quantityCancelled: 1 });
expect(ctrl.cancelled).toBe(true);
});
test("cancellationType returns null when not cancelled", () => {
const ctrl = new ForecastProductController(buildMockCWForecastItem());
expect(ctrl.cancellationType).toBeNull();
});
test("cancellationType returns 'full' when all units cancelled", () => {
const ctrl = new ForecastProductController(
buildMockCWForecastItem({ quantity: 5 }),
);
ctrl.applyCancellationData({
cancelledFlag: true,
quantityCancelled: 5,
});
expect(ctrl.cancellationType).toBe("full");
});
test("cancellationType returns 'partial' when some units cancelled", () => {
const ctrl = new ForecastProductController(
buildMockCWForecastItem({ quantity: 5 }),
);
ctrl.applyCancellationData({
cancelledFlag: true,
quantityCancelled: 2,
});
expect(ctrl.cancellationType).toBe("partial");
});
test("effectiveQuantity returns full quantity when not cancelled", () => {
const ctrl = new ForecastProductController(
buildMockCWForecastItem({ quantity: 5 }),
);
expect(ctrl.effectiveQuantity).toBe(5);
});
test("effectiveQuantity returns reduced quantity for partial cancel", () => {
const ctrl = new ForecastProductController(
buildMockCWForecastItem({ quantity: 5 }),
);
ctrl.applyCancellationData({ cancelledFlag: true, quantityCancelled: 2 });
expect(ctrl.effectiveQuantity).toBe(3);
});
test("effectiveQuantity returns 0 for full cancellation", () => {
const ctrl = new ForecastProductController(
buildMockCWForecastItem({ quantity: 5 }),
);
ctrl.applyCancellationData({ cancelledFlag: true, quantityCancelled: 5 });
expect(ctrl.effectiveQuantity).toBe(0);
});
test("effectiveRevenue returns full revenue when not cancelled", () => {
const ctrl = new ForecastProductController(
buildMockCWForecastItem({ quantity: 5, revenue: 2500 }),
);
expect(ctrl.effectiveRevenue).toBe(2500);
});
test("effectiveRevenue returns proportional revenue for partial cancel", () => {
const ctrl = new ForecastProductController(
buildMockCWForecastItem({ quantity: 5, revenue: 2500 }),
);
ctrl.applyCancellationData({ cancelledFlag: true, quantityCancelled: 2 });
expect(ctrl.effectiveRevenue).toBe(1500);
});
test("effectiveRevenue returns 0 for full cancellation", () => {
const ctrl = new ForecastProductController(
buildMockCWForecastItem({ quantity: 5, revenue: 2500 }),
);
ctrl.applyCancellationData({ cancelledFlag: true, quantityCancelled: 5 });
expect(ctrl.effectiveRevenue).toBe(0);
});
test("effectiveCost returns full cost when not cancelled", () => {
const ctrl = new ForecastProductController(
buildMockCWForecastItem({ quantity: 5, cost: 1800 }),
);
expect(ctrl.effectiveCost).toBe(1800);
});
test("effectiveCost returns 0 for full cancellation", () => {
const ctrl = new ForecastProductController(
buildMockCWForecastItem({ quantity: 5, cost: 1800 }),
);
ctrl.applyCancellationData({ cancelledFlag: true, quantityCancelled: 5 });
expect(ctrl.effectiveCost).toBe(0);
});
});
// -------------------------------------------------------------------
// toJson
// -------------------------------------------------------------------
describe("toJson()", () => {
test("returns id as cwForecastId", () => {
const ctrl = new ForecastProductController(buildMockCWForecastItem());
const json = ctrl.toJson();
expect(json.id).toBe(7001);
});
test("returns financial fields", () => {
const ctrl = new ForecastProductController(buildMockCWForecastItem());
const json = ctrl.toJson();
expect(json.revenue).toBe(2500.0);
expect(json.cost).toBe(1800.0);
expect(json.margin).toBe(700.0);
expect(json.profit).toBe(700.0);
});
test("returns cancellation info", () => {
const ctrl = new ForecastProductController(buildMockCWForecastItem());
const json = ctrl.toJson();
expect(json.cancelled).toBe(false);
expect(json.cancellationType).toBeNull();
});
test("returns status as reference object", () => {
const ctrl = new ForecastProductController(buildMockCWForecastItem());
const json = ctrl.toJson();
expect(json.status).toEqual({ id: 1, name: "Won" });
});
test("returns catalogItem as reference object", () => {
const ctrl = new ForecastProductController(buildMockCWForecastItem());
const json = ctrl.toJson();
expect(json.catalogItem).toEqual({
id: 500,
identifier: "USW-Pro-24",
});
});
test("returns opportunity as reference object", () => {
const ctrl = new ForecastProductController(buildMockCWForecastItem());
const json = ctrl.toJson();
expect(json.opportunity).toEqual({
id: 1001,
name: "Test Opportunity",
});
});
test("includes inventory data", () => {
const ctrl = new ForecastProductController(buildMockCWForecastItem());
ctrl.applyInventoryData({ onHand: 10 });
const json = ctrl.toJson();
expect(json.onHand).toBe(10);
expect(json.inStock).toBe(true);
});
test("includes boolean flags", () => {
const ctrl = new ForecastProductController(buildMockCWForecastItem());
const json = ctrl.toJson();
expect(json.includeFlag).toBe(true);
expect(json.linkFlag).toBe(false);
expect(json.recurringFlag).toBe(false);
expect(json.taxableFlag).toBe(true);
});
test("includes sequence and timing info", () => {
const ctrl = new ForecastProductController(buildMockCWForecastItem());
const json = ctrl.toJson();
expect(json.sequenceNumber).toBe(1);
expect(json.subNumber).toBe(0);
expect(json.cwLastUpdated).toBeInstanceOf(Date);
});
test("includes customerDescription and productNarrative", () => {
const ctrl = new ForecastProductController(
buildMockCWForecastItem({ customerDescription: "Customer desc" }),
);
const json = ctrl.toJson();
expect(json.customerDescription).toBe("Customer desc");
expect(json.productNarrative).toBeNull();
});
test("includes effective* computed fields", () => {
const ctrl = new ForecastProductController(
buildMockCWForecastItem({ quantity: 5, revenue: 2500, cost: 1800 }),
);
const json = ctrl.toJson();
expect(json.effectiveQuantity).toBe(5);
expect(json.effectiveRevenue).toBe(2500);
expect(json.effectiveCost).toBe(1800);
});
});
});