Files
optima/tests/unit/controllers/ForecastProductController.test.ts
T
HoloPanio d7b374f8ab feat: sales activities, forecast products, catalog categories, member cache, procurement filters, and comprehensive tests
New features:
- ActivityController and manager for CW sales activities (CRUD)
- ForecastProductController for opportunity forecast/product lines
- CW member cache with dual-layer (in-memory + Redis) resolution
- Catalog category/subcategory/ecosystem taxonomy module
- Quote statuses type definitions with CW mapping
- User-defined fields (UDF) module with cache and event refresh
- Company sites CW module with serialization
- Procurement manager filters (category, ecosystem, manufacturer, price, stock)
- Opportunity notes CRUD and product line management via CW API
- Opportunity type definitions endpoint

Updates:
- OpportunityController: CW refresh, company hydration, activities, custom fields
- UserController: cwIdentifier field for CW member linking
- CatalogItemController: category/subcategory fields from CW
- PermissionNodes: sales note/product CRUD nodes, subCategories, collectPermissions
- API routes: procurement categories/filters, sales notes/products, opportunity types
- Global events: UDF and member refresh intervals on startup

Tests (414 passing):
- ActivityController, ForecastProductController, OpportunityController unit tests
- UserController cwIdentifier tests
- catalogCategories, companySites, memberCache, procurement module tests
- activityTypes, opportunityTypes, quoteStatuses type tests
- permissionNodes subCategories and getAllPermissionNodes tests
- Updated test setup with redis mock, API method mocks, and builder helpers
2026-03-01 13:19:00 -06:00

284 lines
10 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();
});
});
// -------------------------------------------------------------------
// 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");
});
});
// -------------------------------------------------------------------
// 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);
});
});
});