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:
2026-03-01 13:19:00 -06:00
parent 883b648d5e
commit d7b374f8ab
96 changed files with 7752 additions and 205 deletions
+165
View File
@@ -38,6 +38,14 @@ mock.module("../src/constants", () => ({
connectWiseApi: {
get: mock(() => Promise.resolve({ data: {} })),
post: mock(() => Promise.resolve({ data: {} })),
put: mock(() => Promise.resolve({ data: {} })),
patch: mock(() => Promise.resolve({ data: {} })),
delete: mock(() => Promise.resolve({ data: {} })),
},
redis: {
get: mock(() => Promise.resolve(null)),
set: mock(() => Promise.resolve("OK")),
del: mock(() => Promise.resolve(1)),
},
unifi: createMockUnifi(),
unifiControllerBaseUrl: "https://unifi.test.local",
@@ -235,3 +243,160 @@ export function buildMockUnifiSite(overrides: Record<string, any> = {}) {
...overrides,
};
}
/** Build a minimal Prisma-shaped Opportunity row. */
export function buildMockOpportunity(overrides: Record<string, any> = {}) {
return {
id: "opp-1",
cwOpportunityId: 1001,
name: "Test Opportunity",
notes: "Some notes",
typeName: "New Business",
typeCwId: 1,
stageName: "Proposal",
stageCwId: 2,
statusName: "Active",
statusCwId: 3,
priorityName: "High",
priorityCwId: 4,
ratingName: "Hot",
ratingCwId: 5,
source: "Referral",
campaignName: null,
campaignCwId: null,
primarySalesRepName: "John",
primarySalesRepIdentifier: "jroberts",
primarySalesRepCwId: 10,
secondarySalesRepName: null,
secondarySalesRepIdentifier: null,
secondarySalesRepCwId: null,
companyCwId: 123,
companyName: "Test Company",
contactCwId: 200,
contactName: "Jane Doe",
siteCwId: 300,
siteName: "Main Office",
customerPO: "PO-12345",
totalSalesTax: 50.0,
locationName: "HQ",
locationCwId: 400,
departmentName: "Sales",
departmentCwId: 500,
expectedCloseDate: new Date("2026-04-01"),
pipelineChangeDate: new Date("2026-02-15"),
dateBecameLead: new Date("2026-01-01"),
closedDate: null,
closedFlag: false,
closedByName: null,
closedByCwId: null,
companyId: "company-1",
cwLastUpdated: new Date("2026-02-28"),
createdAt: new Date("2026-01-01"),
updatedAt: new Date("2026-02-28"),
company: null,
...overrides,
};
}
/** Build a minimal CW Activity object for ActivityController tests. */
export function buildMockCWActivity(overrides: Record<string, any> = {}) {
return {
id: 5001,
name: "Test Activity",
notes: "Activity notes",
type: { id: 1, name: "Call" },
status: { id: 2, name: "Open" },
company: { id: 123, identifier: "TestCo", name: "Test Company" },
contact: { id: 200, name: "Jane Doe" },
phoneNumber: "555-1234",
email: "jane@test.com",
opportunity: { id: 1001, name: "Test Opportunity" },
ticket: { id: 0, name: "" },
agreement: { id: 0, name: "" },
campaign: { id: 0, name: "" },
assignTo: { id: 10, identifier: "jroberts", name: "John Roberts" },
scheduleStatus: { id: 1, name: "Firm" },
reminder: { id: 1, name: "15 Minutes" },
where: { id: 1, name: "Office" },
dateStart: "2026-03-01T09:00:00Z",
dateEnd: "2026-03-01T10:00:00Z",
notifyFlag: false,
mobileGuid: "guid-abc123",
currency: { id: 1, name: "USD" },
customFields: [],
_info: {
lastUpdated: "2026-02-28T12:00:00Z",
updatedBy: "jroberts",
dateEntered: "2026-01-15T08:00:00Z",
enteredBy: "jroberts",
},
...overrides,
};
}
/** Build a minimal CW Forecast Item for ForecastProductController tests. */
export function buildMockCWForecastItem(overrides: Record<string, any> = {}) {
return {
id: 7001,
forecastDescription: "Network Switch",
opportunity: { id: 1001, name: "Test Opportunity" },
quantity: 5,
status: { id: 1, name: "Won" },
catalogItem: { id: 500, identifier: "USW-Pro-24" },
productDescription: "UniFi Switch Pro 24",
productClass: "Product",
forecastType: "Product",
revenue: 2500.0,
cost: 1800.0,
margin: 700.0,
percentage: 100,
includeFlag: true,
linkFlag: false,
recurringFlag: false,
taxableFlag: true,
recurringRevenue: 0,
recurringCost: 0,
cycles: 0,
sequenceNumber: 1,
subNumber: 0,
quoteWerksQuantity: 0,
_info: {
lastUpdated: "2026-02-28T12:00:00Z",
updatedBy: "jroberts",
},
...overrides,
};
}
/** Build a minimal Prisma-shaped CatalogItem row. */
export function buildMockCatalogItem(overrides: Record<string, any> = {}) {
return {
id: "cat-1",
cwCatalogId: 500,
identifier: "USW-Pro-24",
name: "UniFi Switch Pro 24",
description: "24-port managed switch",
customerDescription: "Enterprise switch",
internalNotes: null,
category: "Technology",
categoryCwId: 18,
subcategory: "Network-Switch",
subcategoryCwId: 112,
manufacturer: "Ubiquiti",
manufactureCwId: 248,
partNumber: "USW-Pro-24",
vendorName: "Ubiquiti Inc",
vendorSku: "USW-Pro-24",
vendorCwId: 100,
price: 500.0,
cost: 360.0,
inactive: false,
salesTaxable: true,
onHand: 10,
cwLastUpdated: new Date("2026-02-28"),
linkedItems: [],
createdAt: new Date("2026-01-01"),
updatedAt: new Date("2026-02-28"),
...overrides,
};
}