Files
optima/tests/unit/controllers/OpportunityController.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 { OpportunityController } from "../../../src/controllers/OpportunityController";
import { ActivityController } from "../../../src/controllers/ActivityController";
import { CompanyController } from "../../../src/controllers/CompanyController";
import {
buildMockOpportunity,
buildMockCompany,
buildMockCWActivity,
} from "../../setup";
describe("OpportunityController", () => {
// -------------------------------------------------------------------
// Constructor
// -------------------------------------------------------------------
describe("constructor", () => {
test("sets core identification fields", () => {
const ctrl = new OpportunityController(buildMockOpportunity());
expect(ctrl.id).toBe("opp-1");
expect(ctrl.cwOpportunityId).toBe(1001);
expect(ctrl.name).toBe("Test Opportunity");
expect(ctrl.notes).toBe("Some notes");
});
test("sets type, stage, status references", () => {
const ctrl = new OpportunityController(buildMockOpportunity());
expect(ctrl.typeName).toBe("New Business");
expect(ctrl.typeCwId).toBe(1);
expect(ctrl.stageName).toBe("Proposal");
expect(ctrl.stageCwId).toBe(2);
expect(ctrl.statusName).toBe("Active");
expect(ctrl.statusCwId).toBe(3);
});
test("sets priority, rating, source", () => {
const ctrl = new OpportunityController(buildMockOpportunity());
expect(ctrl.priorityName).toBe("High");
expect(ctrl.priorityCwId).toBe(4);
expect(ctrl.ratingName).toBe("Hot");
expect(ctrl.ratingCwId).toBe(5);
expect(ctrl.source).toBe("Referral");
});
test("sets sales rep fields", () => {
const ctrl = new OpportunityController(buildMockOpportunity());
expect(ctrl.primarySalesRepName).toBe("John");
expect(ctrl.primarySalesRepIdentifier).toBe("jroberts");
expect(ctrl.primarySalesRepCwId).toBe(10);
expect(ctrl.secondarySalesRepName).toBeNull();
});
test("sets company/contact/site fields", () => {
const ctrl = new OpportunityController(buildMockOpportunity());
expect(ctrl.companyCwId).toBe(123);
expect(ctrl.companyName).toBe("Test Company");
expect(ctrl.contactCwId).toBe(200);
expect(ctrl.contactName).toBe("Jane Doe");
expect(ctrl.siteCwId).toBe(300);
expect(ctrl.siteName).toBe("Main Office");
});
test("sets financial and location fields", () => {
const ctrl = new OpportunityController(buildMockOpportunity());
expect(ctrl.totalSalesTax).toBe(50.0);
expect(ctrl.customerPO).toBe("PO-12345");
expect(ctrl.locationName).toBe("HQ");
expect(ctrl.departmentName).toBe("Sales");
});
test("sets date fields", () => {
const ctrl = new OpportunityController(buildMockOpportunity());
expect(ctrl.expectedCloseDate).toBeInstanceOf(Date);
expect(ctrl.pipelineChangeDate).toBeInstanceOf(Date);
expect(ctrl.dateBecameLead).toBeInstanceOf(Date);
expect(ctrl.closedDate).toBeNull();
expect(ctrl.closedFlag).toBe(false);
});
test("accepts company controller via opts", () => {
const company = new CompanyController(buildMockCompany());
const ctrl = new OpportunityController(buildMockOpportunity(), {
company,
});
const json = ctrl.toJson();
// Company should be a full object, not just {id, name}
expect(json.company.id).toBe("company-1");
expect(json.company.name).toBe("Test Company");
});
test("accepts activities via opts", () => {
const activities = [new ActivityController(buildMockCWActivity())];
const ctrl = new OpportunityController(buildMockOpportunity(), {
activities,
});
const json = ctrl.toJson();
expect(json.activities).toHaveLength(1);
expect(json.activities[0].cwActivityId).toBe(5001);
});
test("accepts customFields via opts", () => {
const customFields = [
{
id: 1,
caption: "Custom1",
type: "Text",
entryMethod: "EntryField",
numberOfDecimals: 0,
value: "test",
},
];
const ctrl = new OpportunityController(buildMockOpportunity(), {
customFields,
});
const json = ctrl.toJson();
expect(json.customFields).toHaveLength(1);
});
test("has empty activities/customFields without opts", () => {
const ctrl = new OpportunityController(buildMockOpportunity());
const json = ctrl.toJson();
expect(json.activities).toEqual([]);
expect(json.customFields).toEqual([]);
});
});
// -------------------------------------------------------------------
// mapCwToDb (static)
// -------------------------------------------------------------------
describe("mapCwToDb()", () => {
const cwOpportunity = {
id: 1001,
name: "CW Opp",
notes: "CW notes",
type: { id: 1, name: "New Business" },
stage: { id: 2, name: "Proposal" },
status: { id: 3, name: "Active" },
priority: { id: 4, name: "High" },
rating: null,
source: "Web",
campaign: null,
primarySalesRep: { id: 10, identifier: "jroberts", name: "John" },
secondarySalesRep: null,
company: { id: 123, identifier: "TestCo", name: "Test Co" },
contact: { id: 200, name: "Jane" },
site: { id: 300, name: "Main" },
customerPO: "PO-1",
totalSalesTax: 25.5,
location: { id: 400, name: "HQ" },
department: { id: 500, name: "Sales" },
expectedCloseDate: "2026-04-01T00:00:00Z",
pipelineChangeDate: "2026-02-15T00:00:00Z",
dateBecameLead: "2026-01-01T00:00:00Z",
closedDate: null,
closedFlag: false,
closedBy: null,
customFields: [],
_info: { lastUpdated: "2026-02-28T12:00:00Z" },
} as any;
test("maps name and notes", () => {
const result = OpportunityController.mapCwToDb(cwOpportunity);
expect(result.name).toBe("CW Opp");
expect(result.notes).toBe("CW notes");
});
test("maps type, stage, status references", () => {
const result = OpportunityController.mapCwToDb(cwOpportunity);
expect(result.typeName).toBe("New Business");
expect(result.typeCwId).toBe(1);
expect(result.stageName).toBe("Proposal");
expect(result.statusName).toBe("Active");
});
test("maps null references to null", () => {
const result = OpportunityController.mapCwToDb(cwOpportunity);
expect(result.ratingName).toBeNull();
expect(result.ratingCwId).toBeNull();
expect(result.campaignName).toBeNull();
});
test("maps sales rep fields", () => {
const result = OpportunityController.mapCwToDb(cwOpportunity);
expect(result.primarySalesRepName).toBe("John");
expect(result.primarySalesRepIdentifier).toBe("jroberts");
expect(result.secondarySalesRepName).toBeNull();
});
test("maps dates to Date objects", () => {
const result = OpportunityController.mapCwToDb(cwOpportunity);
expect(result.expectedCloseDate).toBeInstanceOf(Date);
expect(result.closedDate).toBeNull();
});
test("maps closedFlag", () => {
const result = OpportunityController.mapCwToDb(cwOpportunity);
expect(result.closedFlag).toBe(false);
});
test("maps cwLastUpdated from _info", () => {
const result = OpportunityController.mapCwToDb(cwOpportunity);
expect(result.cwLastUpdated).toBeInstanceOf(Date);
});
});
// -------------------------------------------------------------------
// toJson
// -------------------------------------------------------------------
describe("toJson()", () => {
test("returns core fields", () => {
const ctrl = new OpportunityController(buildMockOpportunity());
const json = ctrl.toJson();
expect(json.id).toBe("opp-1");
expect(json.cwOpportunityId).toBe(1001);
expect(json.name).toBe("Test Opportunity");
});
test("formats type as reference object", () => {
const ctrl = new OpportunityController(buildMockOpportunity());
const json = ctrl.toJson();
expect(json.type).toEqual({ id: 1, name: "New Business" });
});
test("formats stage as reference object", () => {
const ctrl = new OpportunityController(buildMockOpportunity());
const json = ctrl.toJson();
expect(json.stage).toEqual({ id: 2, name: "Proposal" });
});
test("formats status as reference object", () => {
const ctrl = new OpportunityController(buildMockOpportunity());
const json = ctrl.toJson();
expect(json.status).toEqual({ id: 3, name: "Active" });
});
test("formats primarySalesRep with identifier", () => {
const ctrl = new OpportunityController(buildMockOpportunity());
const json = ctrl.toJson();
expect(json.primarySalesRep).toEqual({
id: 10,
identifier: "jroberts",
name: "John",
});
});
test("secondarySalesRep is null when not set", () => {
const ctrl = new OpportunityController(buildMockOpportunity());
const json = ctrl.toJson();
expect(json.secondarySalesRep).toBeNull();
});
test("contact formats as reference object", () => {
const ctrl = new OpportunityController(buildMockOpportunity());
const json = ctrl.toJson();
expect(json.contact).toEqual({ id: 200, name: "Jane Doe" });
});
test("company falls back to CW reference when no controller", () => {
const ctrl = new OpportunityController(buildMockOpportunity());
const json = ctrl.toJson();
expect(json.company).toEqual({ id: 123, name: "Test Company" });
});
test("includes financial data", () => {
const ctrl = new OpportunityController(buildMockOpportunity());
const json = ctrl.toJson();
expect(json.totalSalesTax).toBe(50.0);
expect(json.customerPO).toBe("PO-12345");
});
test("includes dates", () => {
const ctrl = new OpportunityController(buildMockOpportunity());
const json = ctrl.toJson();
expect(json.expectedCloseDate).toBeInstanceOf(Date);
expect(json.closedFlag).toBe(false);
});
test("includes timestamps", () => {
const ctrl = new OpportunityController(buildMockOpportunity());
const json = ctrl.toJson();
expect(json.createdAt).toBeInstanceOf(Date);
expect(json.updatedAt).toBeInstanceOf(Date);
});
});
});