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:
+165
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user