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:
+187
-12
@@ -117,22 +117,26 @@ Admin-specific UI permissions that control visibility and data loading for admin
|
||||
|
||||
### Procurement Permissions
|
||||
|
||||
| Permission Node | Description | Used In | Dependencies |
|
||||
| --------------------------------------- | ------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------- |
|
||||
| `procurement.catalog.fetch` | Fetch a single catalog item | [src/api/procurement/[id]/fetch.ts](src/api/procurement/[id]/fetch.ts) | |
|
||||
| `procurement.catalog.fetch.many` | Fetch multiple catalog items or count | [src/api/procurement/fetchAll.ts](src/api/procurement/fetchAll.ts), [src/api/procurement/count.ts](src/api/procurement/count.ts) | |
|
||||
| `procurement.catalog.inventory.refresh` | Refresh on-hand inventory for a catalog item from ConnectWise | [src/api/procurement/[id]/refreshInventory.ts](src/api/procurement/[id]/refreshInventory.ts) | `procurement.catalog.fetch` |
|
||||
| `procurement.catalog.link` | Link or unlink catalog items to each other | [src/api/procurement/[id]/link.ts](src/api/procurement/[id]/link.ts), [src/api/procurement/[id]/unlink.ts](src/api/procurement/[id]/unlink.ts) | `procurement.catalog.fetch` |
|
||||
| Permission Node | Description | Used In | Dependencies |
|
||||
| --------------------------------------- | ---------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------- |
|
||||
| `procurement.catalog.fetch` | Fetch a single catalog item | [src/api/procurement/[id]/fetch.ts](src/api/procurement/[id]/fetch.ts) | |
|
||||
| `procurement.catalog.fetch.many` | Fetch multiple catalog items, count, categories/ecosystems, or filter values | [src/api/procurement/fetchAll.ts](src/api/procurement/fetchAll.ts), [src/api/procurement/count.ts](src/api/procurement/count.ts), [src/api/procurement/categories.ts](src/api/procurement/categories.ts), [src/api/procurement/filters.ts](src/api/procurement/filters.ts) | |
|
||||
| `procurement.catalog.inventory.refresh` | Refresh on-hand inventory for a catalog item from ConnectWise | [src/api/procurement/[id]/refreshInventory.ts](src/api/procurement/[id]/refreshInventory.ts) | `procurement.catalog.fetch` |
|
||||
| `procurement.catalog.link` | Link or unlink catalog items to each other | [src/api/procurement/[id]/link.ts](src/api/procurement/[id]/link.ts), [src/api/procurement/[id]/unlink.ts](src/api/procurement/[id]/unlink.ts) | `procurement.catalog.fetch` |
|
||||
|
||||
### Sales Permissions
|
||||
|
||||
Permissions for accessing and managing sales opportunities. Opportunities are synced from ConnectWise and stored locally; sub-resources (forecasts, notes, contacts) are fetched live from CW.
|
||||
Permissions for accessing and managing sales opportunities. Opportunities are synced from ConnectWise and stored locally; sub-resources (products, notes, contacts) are fetched live from CW.
|
||||
|
||||
| Permission Node | Description | Used In | Dependencies |
|
||||
| ------------------------------ | -------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------- |
|
||||
| `sales.opportunity.fetch` | Fetch a single opportunity and its CW sub-resources (forecasts, notes, contacts) | [src/api/sales/[id]/fetch.ts](src/api/sales/[id]/fetch.ts), [src/api/sales/[id]/forecasts.ts](src/api/sales/[id]/forecasts.ts), [src/api/sales/[id]/notes.ts](src/api/sales/[id]/notes.ts), [src/api/sales/[id]/contacts.ts](src/api/sales/[id]/contacts.ts) | |
|
||||
| `sales.opportunity.fetch.many` | Fetch multiple opportunities (paginated/searchable) or get opportunity count | [src/api/sales/fetchAll.ts](src/api/sales/fetchAll.ts), [src/api/sales/count.ts](src/api/sales/count.ts) | |
|
||||
| `sales.opportunity.refresh` | Refresh a single opportunity's local data from ConnectWise | [src/api/sales/[id]/refresh.ts](src/api/sales/[id]/refresh.ts) | `sales.opportunity.fetch` |
|
||||
| Permission Node | Description | Used In | Dependencies |
|
||||
| ---------------------------------- | -------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------- |
|
||||
| `sales.opportunity.fetch` | Fetch a single opportunity and its CW sub-resources (products, notes, contacts) | [src/api/sales/[id]/fetch.ts](src/api/sales/[id]/fetch.ts), [src/api/sales/[id]/products.ts](src/api/sales/[id]/products.ts), [src/api/sales/[id]/notes.ts](src/api/sales/[id]/notes.ts), [src/api/sales/[id]/fetchNote.ts](src/api/sales/[id]/fetchNote.ts), [src/api/sales/[id]/contacts.ts](src/api/sales/[id]/contacts.ts) | |
|
||||
| `sales.opportunity.fetch.many` | Fetch multiple opportunities (paginated/searchable), count, or opportunity types | [src/api/sales/fetchAll.ts](src/api/sales/fetchAll.ts), [src/api/sales/count.ts](src/api/sales/count.ts), [src/api/sales/fetchOpportunityTypes.ts](src/api/sales/fetchOpportunityTypes.ts) | |
|
||||
| `sales.opportunity.refresh` | Refresh a single opportunity's local data from ConnectWise | [src/api/sales/[id]/refresh.ts](src/api/sales/[id]/refresh.ts) | `sales.opportunity.fetch` |
|
||||
| `sales.opportunity.note.create` | Create a new note on an opportunity | [src/api/sales/[id]/createNote.ts](src/api/sales/[id]/createNote.ts) | `sales.opportunity.fetch` |
|
||||
| `sales.opportunity.note.update` | Update an existing note on an opportunity | [src/api/sales/[id]/updateNote.ts](src/api/sales/[id]/updateNote.ts) | `sales.opportunity.fetch` |
|
||||
| `sales.opportunity.note.delete` | Delete a note from an opportunity | [src/api/sales/[id]/deleteNote.ts](src/api/sales/[id]/deleteNote.ts) | `sales.opportunity.fetch` |
|
||||
| `sales.opportunity.product.update` | Update products (forecast items) on an opportunity, including resequencing | [src/api/sales/[id]/resequenceProducts.ts](src/api/sales/[id]/resequenceProducts.ts) | `sales.opportunity.fetch` |
|
||||
|
||||
### UniFi Permissions
|
||||
|
||||
@@ -171,6 +175,177 @@ The WiFi fetch route uses `processObjectValuePerms` to filter each WLAN object o
|
||||
| `unifi.site.wifi.ppsk` | View private pre-shared keys (PPSKs) for a specific WiFi network | [src/api/unifi/site/wifi/ppskFetchAll.ts](src/api/unifi/site/wifi/ppskFetchAll.ts), [src/api/unifi/site/wifi/ppskCreate.ts](src/api/unifi/site/wifi/ppskCreate.ts) | `unifi.access`, `unifi.site.wifi` |
|
||||
| `unifi.site.wifi.ppsk.create` | Create a private pre-shared key on a specific WiFi network | [src/api/unifi/site/wifi/ppskCreate.ts](src/api/unifi/site/wifi/ppskCreate.ts) | `unifi.access`, `unifi.site.wifi`, `unifi.site.wifi.ppsk` |
|
||||
|
||||
---
|
||||
|
||||
## Object Type Permissions (Field-Level Gating)
|
||||
|
||||
All fetch and fetchAll routes gate response object keys using `processObjectValuePerms`. For each object type, only fields whose corresponding `<scope>.<field>` permission the user holds are included in the response. Grant `<scope>.*` to allow all fields on that object type.
|
||||
|
||||
### Company (`obj.company`)
|
||||
|
||||
| Field Permission | Description |
|
||||
| --------------------------- | ----------------------------------------- |
|
||||
| `obj.company.id` | View company ID |
|
||||
| `obj.company.name` | View company name |
|
||||
| `obj.company.cw_Identifier` | View ConnectWise identifier |
|
||||
| `obj.company.cw_CompanyId` | View ConnectWise company ID |
|
||||
| `obj.company.cw_Data` | View ConnectWise data (address, contacts) |
|
||||
| `obj.company.createdAt` | View creation timestamp |
|
||||
| `obj.company.updatedAt` | View last-updated timestamp |
|
||||
|
||||
**Used in:** [src/api/companies/[id]/fetch.ts](src/api/companies/[id]/fetch.ts), [src/api/companies/fetchAll.ts](src/api/companies/fetchAll.ts)
|
||||
|
||||
### Credential (`obj.credential`)
|
||||
|
||||
| Field Permission | Description |
|
||||
| ---------------------------------- | ----------------------------- |
|
||||
| `obj.credential.id` | View credential ID |
|
||||
| `obj.credential.name` | View credential name |
|
||||
| `obj.credential.notes` | View credential notes |
|
||||
| `obj.credential.typeId` | View credential type ID |
|
||||
| `obj.credential.companyId` | View linked company ID |
|
||||
| `obj.credential.subCredentialOfId` | View parent credential ID |
|
||||
| `obj.credential.fields` | View credential field values |
|
||||
| `obj.credential.type` | View credential type object |
|
||||
| `obj.credential.company` | View linked company object |
|
||||
| `obj.credential.subCredentials` | View sub-credentials array |
|
||||
| `obj.credential.secureFieldIds` | View secure field identifiers |
|
||||
| `obj.credential.createdAt` | View creation timestamp |
|
||||
| `obj.credential.updatedAt` | View last-updated timestamp |
|
||||
|
||||
**Used in:** [src/api/credentials/fetch.ts](src/api/credentials/fetch.ts), [src/api/credentials/fetchByCompany.ts](src/api/credentials/fetchByCompany.ts), [src/api/credentials/fetchSubCredentials.ts](src/api/credentials/fetchSubCredentials.ts), [src/api/credential-types/fetchCredentials.ts](src/api/credential-types/fetchCredentials.ts)
|
||||
|
||||
### Credential Type (`obj.credentialType`)
|
||||
|
||||
| Field Permission | Description |
|
||||
| ------------------------------------ | ----------------------------------------- |
|
||||
| `obj.credentialType.id` | View credential type ID |
|
||||
| `obj.credentialType.name` | View credential type name |
|
||||
| `obj.credentialType.permissionScope` | View permission scope |
|
||||
| `obj.credentialType.icon` | View icon |
|
||||
| `obj.credentialType.fields` | View field definitions |
|
||||
| `obj.credentialType.credentialCount` | View count of credentials using this type |
|
||||
| `obj.credentialType.createdAt` | View creation timestamp |
|
||||
| `obj.credentialType.updatedAt` | View last-updated timestamp |
|
||||
|
||||
**Used in:** [src/api/credential-types/fetch.ts](src/api/credential-types/fetch.ts), [src/api/credential-types/fetchAll.ts](src/api/credential-types/fetchAll.ts)
|
||||
|
||||
### User (`obj.user`)
|
||||
|
||||
| Field Permission | Description |
|
||||
| ---------------------- | -------------------------------- |
|
||||
| `obj.user.id` | View user ID |
|
||||
| `obj.user.name` | View user display name |
|
||||
| `obj.user.roles` | View assigned role monikers |
|
||||
| `obj.user.permissions` | View aggregated permission nodes |
|
||||
| `obj.user.login` | View login identifier |
|
||||
| `obj.user.email` | View email address |
|
||||
| `obj.user.image` | View profile image URL |
|
||||
| `obj.user.createdAt` | View creation timestamp |
|
||||
| `obj.user.updatedAt` | View last-updated timestamp |
|
||||
|
||||
**Used in:** [src/api/user/@me/fetch.ts](src/api/user/@me/fetch.ts), [src/api/user/fetch.ts](src/api/user/fetch.ts), [src/api/user/fetchAll.ts](src/api/user/fetchAll.ts), [src/api/roles/getUsers.ts](src/api/roles/getUsers.ts)
|
||||
|
||||
### Role (`obj.role`)
|
||||
|
||||
| Field Permission | Description |
|
||||
| ---------------------- | -------------------------------- |
|
||||
| `obj.role.id` | View role ID |
|
||||
| `obj.role.title` | View role title |
|
||||
| `obj.role.moniker` | View role moniker |
|
||||
| `obj.role.permissions` | View role permission nodes |
|
||||
| `obj.role.users` | View users assigned to this role |
|
||||
| `obj.role.createdAt` | View creation timestamp |
|
||||
| `obj.role.updatedAt` | View last-updated timestamp |
|
||||
|
||||
**Used in:** [src/api/roles/fetch.ts](src/api/roles/fetch.ts), [src/api/roles/fetchAll.ts](src/api/roles/fetchAll.ts), [src/api/user/fetchRoles.ts](src/api/user/fetchRoles.ts)
|
||||
|
||||
### Catalog Item (`obj.catalogItem`)
|
||||
|
||||
| Field Permission | Description |
|
||||
| ------------------------------------- | -------------------------------- |
|
||||
| `obj.catalogItem.id` | View catalog item ID |
|
||||
| `obj.catalogItem.cwCatalogId` | View ConnectWise catalog ID |
|
||||
| `obj.catalogItem.identifier` | View item identifier |
|
||||
| `obj.catalogItem.name` | View item name |
|
||||
| `obj.catalogItem.description` | View description |
|
||||
| `obj.catalogItem.customerDescription` | View customer-facing description |
|
||||
| `obj.catalogItem.internalNotes` | View internal notes |
|
||||
| `obj.catalogItem.manufacturer` | View manufacturer name |
|
||||
| `obj.catalogItem.manufactureCwId` | View manufacturer ConnectWise ID |
|
||||
| `obj.catalogItem.partNumber` | View part number |
|
||||
| `obj.catalogItem.vendorName` | View vendor name |
|
||||
| `obj.catalogItem.vendorSku` | View vendor SKU |
|
||||
| `obj.catalogItem.vendorCwId` | View vendor ConnectWise ID |
|
||||
| `obj.catalogItem.price` | View price |
|
||||
| `obj.catalogItem.cost` | View cost |
|
||||
| `obj.catalogItem.inactive` | View inactive flag |
|
||||
| `obj.catalogItem.salesTaxable` | View sales-taxable flag |
|
||||
| `obj.catalogItem.onHand` | View on-hand inventory count |
|
||||
| `obj.catalogItem.cwLastUpdated` | View CW last-updated timestamp |
|
||||
| `obj.catalogItem.linkedItems` | View linked catalog items |
|
||||
| `obj.catalogItem.createdAt` | View creation timestamp |
|
||||
| `obj.catalogItem.updatedAt` | View last-updated timestamp |
|
||||
|
||||
**Used in:** [src/api/procurement/fetchAll.ts](src/api/procurement/fetchAll.ts), [src/api/procurement/[id]/fetch.ts](src/api/procurement/[id]/fetch.ts), [src/api/procurement/[id]/fetchLinked.ts](src/api/procurement/[id]/fetchLinked.ts)
|
||||
|
||||
### Opportunity (`obj.opportunity`)
|
||||
|
||||
| Field Permission | Description |
|
||||
| ------------------------------------ | ------------------------------- |
|
||||
| `obj.opportunity.id` | View opportunity ID |
|
||||
| `obj.opportunity.cwOpportunityId` | View ConnectWise opportunity ID |
|
||||
| `obj.opportunity.name` | View opportunity name |
|
||||
| `obj.opportunity.notes` | View notes |
|
||||
| `obj.opportunity.type` | View opportunity type |
|
||||
| `obj.opportunity.stage` | View stage |
|
||||
| `obj.opportunity.status` | View status |
|
||||
| `obj.opportunity.priority` | View priority |
|
||||
| `obj.opportunity.rating` | View rating |
|
||||
| `obj.opportunity.source` | View source |
|
||||
| `obj.opportunity.campaign` | View campaign |
|
||||
| `obj.opportunity.primarySalesRep` | View primary sales rep |
|
||||
| `obj.opportunity.secondarySalesRep` | View secondary sales rep |
|
||||
| `obj.opportunity.company` | View company |
|
||||
| `obj.opportunity.contact` | View contact |
|
||||
| `obj.opportunity.site` | View site |
|
||||
| `obj.opportunity.customerPO` | View customer PO |
|
||||
| `obj.opportunity.totalSalesTax` | View total sales tax |
|
||||
| `obj.opportunity.location` | View location |
|
||||
| `obj.opportunity.department` | View department |
|
||||
| `obj.opportunity.expectedCloseDate` | View expected close date |
|
||||
| `obj.opportunity.pipelineChangeDate` | View pipeline change date |
|
||||
| `obj.opportunity.dateBecameLead` | View date became lead |
|
||||
| `obj.opportunity.closedDate` | View closed date |
|
||||
| `obj.opportunity.closedFlag` | View closed flag |
|
||||
| `obj.opportunity.closedBy` | View closed-by member |
|
||||
| `obj.opportunity.companyId` | View linked company ID |
|
||||
| `obj.opportunity.cwLastUpdated` | View CW last-updated timestamp |
|
||||
| `obj.opportunity.createdAt` | View creation timestamp |
|
||||
| `obj.opportunity.updatedAt` | View last-updated timestamp |
|
||||
|
||||
**Used in:** [src/api/sales/fetchAll.ts](src/api/sales/fetchAll.ts), [src/api/sales/[id]/fetch.ts](src/api/sales/[id]/fetch.ts)
|
||||
|
||||
### UniFi Site (`obj.unifiSite`)
|
||||
|
||||
| Field Permission | Description |
|
||||
| ------------------------- | ----------------------------- |
|
||||
| `obj.unifiSite.id` | View site internal ID |
|
||||
| `obj.unifiSite.name` | View site name |
|
||||
| `obj.unifiSite.siteId` | View UniFi controller site ID |
|
||||
| `obj.unifiSite.companyId` | View linked company ID |
|
||||
| `obj.unifiSite.company` | View linked company object |
|
||||
| `obj.unifiSite.createdAt` | View creation timestamp |
|
||||
| `obj.unifiSite.updatedAt` | View last-updated timestamp |
|
||||
|
||||
**Used in:** [src/api/unifi/sites/fetchAll.ts](src/api/unifi/sites/fetchAll.ts), [src/api/unifi/site/fetch.ts](src/api/unifi/site/fetch.ts), [src/api/companies/[id]/unifiSites.ts](src/api/companies/[id]/unifiSites.ts)
|
||||
|
||||
### WiFi Network (`unifi.site.wifi.read`)
|
||||
|
||||
See **UniFi Permissions > Field-Level Permission Gating** above for the full list of `unifi.site.wifi.read.<field>` nodes.
|
||||
|
||||
---
|
||||
|
||||
## Permission Issuers
|
||||
|
||||
Permissions can be issued by different sources:
|
||||
|
||||
Reference in New Issue
Block a user