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
This commit is contained in:
@@ -0,0 +1,196 @@
|
||||
import { describe, test, expect } from "bun:test";
|
||||
import { ActivityController } from "../../../src/controllers/ActivityController";
|
||||
import { buildMockCWActivity } from "../../setup";
|
||||
|
||||
describe("ActivityController", () => {
|
||||
// -------------------------------------------------------------------
|
||||
// Constructor
|
||||
// -------------------------------------------------------------------
|
||||
describe("constructor", () => {
|
||||
test("sets all public properties from CW data", () => {
|
||||
const ctrl = new ActivityController(buildMockCWActivity());
|
||||
expect(ctrl.cwActivityId).toBe(5001);
|
||||
expect(ctrl.name).toBe("Test Activity");
|
||||
expect(ctrl.notes).toBe("Activity notes");
|
||||
});
|
||||
|
||||
test("maps type reference", () => {
|
||||
const ctrl = new ActivityController(buildMockCWActivity());
|
||||
expect(ctrl.typeCwId).toBe(1);
|
||||
expect(ctrl.typeName).toBe("Call");
|
||||
});
|
||||
|
||||
test("maps status reference", () => {
|
||||
const ctrl = new ActivityController(buildMockCWActivity());
|
||||
expect(ctrl.statusCwId).toBe(2);
|
||||
expect(ctrl.statusName).toBe("Open");
|
||||
});
|
||||
|
||||
test("maps company reference", () => {
|
||||
const ctrl = new ActivityController(buildMockCWActivity());
|
||||
expect(ctrl.companyCwId).toBe(123);
|
||||
expect(ctrl.companyName).toBe("Test Company");
|
||||
expect(ctrl.companyIdentifier).toBe("TestCo");
|
||||
});
|
||||
|
||||
test("maps contact reference", () => {
|
||||
const ctrl = new ActivityController(buildMockCWActivity());
|
||||
expect(ctrl.contactCwId).toBe(200);
|
||||
expect(ctrl.contactName).toBe("Jane Doe");
|
||||
});
|
||||
|
||||
test("maps opportunity reference", () => {
|
||||
const ctrl = new ActivityController(buildMockCWActivity());
|
||||
expect(ctrl.opportunityCwId).toBe(1001);
|
||||
expect(ctrl.opportunityName).toBe("Test Opportunity");
|
||||
});
|
||||
|
||||
test("maps assignTo reference", () => {
|
||||
const ctrl = new ActivityController(buildMockCWActivity());
|
||||
expect(ctrl.assignToCwId).toBe(10);
|
||||
expect(ctrl.assignToName).toBe("John Roberts");
|
||||
expect(ctrl.assignToIdentifier).toBe("jroberts");
|
||||
});
|
||||
|
||||
test("maps dates correctly", () => {
|
||||
const ctrl = new ActivityController(buildMockCWActivity());
|
||||
expect(ctrl.dateStart).toBeInstanceOf(Date);
|
||||
expect(ctrl.dateEnd).toBeInstanceOf(Date);
|
||||
});
|
||||
|
||||
test("maps _info dates", () => {
|
||||
const ctrl = new ActivityController(buildMockCWActivity());
|
||||
expect(ctrl.cwLastUpdated).toBeInstanceOf(Date);
|
||||
expect(ctrl.cwDateEntered).toBeInstanceOf(Date);
|
||||
expect(ctrl.cwEnteredBy).toBe("jroberts");
|
||||
expect(ctrl.cwUpdatedBy).toBe("jroberts");
|
||||
});
|
||||
|
||||
test("handles null optional fields gracefully", () => {
|
||||
const ctrl = new ActivityController(
|
||||
buildMockCWActivity({
|
||||
type: undefined,
|
||||
status: undefined,
|
||||
company: undefined,
|
||||
contact: undefined,
|
||||
opportunity: undefined,
|
||||
assignTo: undefined,
|
||||
dateStart: undefined,
|
||||
dateEnd: undefined,
|
||||
notes: undefined,
|
||||
_info: {},
|
||||
}),
|
||||
);
|
||||
expect(ctrl.typeCwId).toBeNull();
|
||||
expect(ctrl.typeName).toBeNull();
|
||||
expect(ctrl.statusCwId).toBeNull();
|
||||
expect(ctrl.companyCwId).toBeNull();
|
||||
expect(ctrl.contactCwId).toBeNull();
|
||||
expect(ctrl.opportunityCwId).toBeNull();
|
||||
expect(ctrl.assignToCwId).toBeNull();
|
||||
expect(ctrl.dateStart).toBeNull();
|
||||
expect(ctrl.dateEnd).toBeNull();
|
||||
expect(ctrl.notes).toBeNull();
|
||||
expect(ctrl.cwLastUpdated).toBeNull();
|
||||
});
|
||||
|
||||
test("maps phoneNumber and email", () => {
|
||||
const ctrl = new ActivityController(buildMockCWActivity());
|
||||
expect(ctrl.phoneNumber).toBe("555-1234");
|
||||
expect(ctrl.email).toBe("jane@test.com");
|
||||
});
|
||||
|
||||
test("maps notifyFlag", () => {
|
||||
const ctrl = new ActivityController(buildMockCWActivity());
|
||||
expect(ctrl.notifyFlag).toBe(false);
|
||||
});
|
||||
|
||||
test("maps customFields", () => {
|
||||
const ctrl = new ActivityController(buildMockCWActivity());
|
||||
expect(ctrl.customFields).toEqual([]);
|
||||
});
|
||||
|
||||
test("maps mobileGuid", () => {
|
||||
const ctrl = new ActivityController(buildMockCWActivity());
|
||||
expect(ctrl.mobileGuid).toBe("guid-abc123");
|
||||
});
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
// toJson
|
||||
// -------------------------------------------------------------------
|
||||
describe("toJson()", () => {
|
||||
test("returns cwActivityId", () => {
|
||||
const ctrl = new ActivityController(buildMockCWActivity());
|
||||
const json = ctrl.toJson();
|
||||
expect(json.cwActivityId).toBe(5001);
|
||||
});
|
||||
|
||||
test("returns name and notes", () => {
|
||||
const ctrl = new ActivityController(buildMockCWActivity());
|
||||
const json = ctrl.toJson();
|
||||
expect(json.name).toBe("Test Activity");
|
||||
expect(json.notes).toBe("Activity notes");
|
||||
});
|
||||
|
||||
test("formats type as reference object", () => {
|
||||
const ctrl = new ActivityController(buildMockCWActivity());
|
||||
const json = ctrl.toJson();
|
||||
expect(json.type).toEqual({ id: 1, name: "Call" });
|
||||
});
|
||||
|
||||
test("type is null when no type set", () => {
|
||||
const ctrl = new ActivityController(
|
||||
buildMockCWActivity({ type: undefined }),
|
||||
);
|
||||
const json = ctrl.toJson();
|
||||
expect(json.type).toBeNull();
|
||||
});
|
||||
|
||||
test("formats company as reference object with identifier", () => {
|
||||
const ctrl = new ActivityController(buildMockCWActivity());
|
||||
const json = ctrl.toJson();
|
||||
expect(json.company).toEqual({
|
||||
id: 123,
|
||||
identifier: "TestCo",
|
||||
name: "Test Company",
|
||||
});
|
||||
});
|
||||
|
||||
test("formats assignTo as reference object with identifier", () => {
|
||||
const ctrl = new ActivityController(buildMockCWActivity());
|
||||
const json = ctrl.toJson();
|
||||
expect(json.assignTo).toEqual({
|
||||
id: 10,
|
||||
identifier: "jroberts",
|
||||
name: "John Roberts",
|
||||
});
|
||||
});
|
||||
|
||||
test("formats opportunity as reference object", () => {
|
||||
const ctrl = new ActivityController(buildMockCWActivity());
|
||||
const json = ctrl.toJson();
|
||||
expect(json.opportunity).toEqual({
|
||||
id: 1001,
|
||||
name: "Test Opportunity",
|
||||
});
|
||||
});
|
||||
|
||||
test("includes dates and meta", () => {
|
||||
const ctrl = new ActivityController(buildMockCWActivity());
|
||||
const json = ctrl.toJson();
|
||||
expect(json.dateStart).toBeInstanceOf(Date);
|
||||
expect(json.dateEnd).toBeInstanceOf(Date);
|
||||
expect(json.cwLastUpdated).toBeInstanceOf(Date);
|
||||
expect(json.cwDateEntered).toBeInstanceOf(Date);
|
||||
expect(json.cwEnteredBy).toBe("jroberts");
|
||||
expect(json.cwUpdatedBy).toBe("jroberts");
|
||||
});
|
||||
|
||||
test("includes customFields array", () => {
|
||||
const ctrl = new ActivityController(buildMockCWActivity());
|
||||
const json = ctrl.toJson();
|
||||
expect(json.customFields).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,283 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,283 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -14,6 +14,14 @@ describe("UserController", () => {
|
||||
expect(ctrl.login).toBe("test@example.com");
|
||||
expect(ctrl.email).toBe("test@example.com");
|
||||
expect(ctrl.image).toBeNull();
|
||||
expect(ctrl.cwIdentifier).toBeNull();
|
||||
});
|
||||
|
||||
test("sets cwIdentifier when provided", () => {
|
||||
const ctrl = new UserController(
|
||||
buildMockUser({ cwIdentifier: "jroberts" }),
|
||||
);
|
||||
expect(ctrl.cwIdentifier).toBe("jroberts");
|
||||
});
|
||||
|
||||
test("sets timestamps", () => {
|
||||
@@ -61,10 +69,19 @@ describe("UserController", () => {
|
||||
expect(json.name).toBe("Test User");
|
||||
expect(json.login).toBeUndefined();
|
||||
expect(json.email).toBeUndefined();
|
||||
expect(json.cwIdentifier).toBeUndefined();
|
||||
expect(json.roles).toBeUndefined();
|
||||
expect(json.permissions).toBeUndefined();
|
||||
});
|
||||
|
||||
test("cwIdentifier included in full JSON", () => {
|
||||
const ctrl = new UserController(
|
||||
buildMockUser({ cwIdentifier: "jroberts" }),
|
||||
);
|
||||
const json = ctrl.toJson();
|
||||
expect(json.cwIdentifier).toBe("jroberts");
|
||||
});
|
||||
|
||||
test("roles is undefined when user has no roles", () => {
|
||||
const ctrl = new UserController(buildMockUser({ roles: [] }));
|
||||
const json = ctrl.toJson();
|
||||
|
||||
Reference in New Issue
Block a user