Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fe71248e88 | |||
| 7411310083 | |||
| 30b408e0db | |||
| d7b374f8ab | |||
| 883b648d5e | |||
| b787120461 | |||
| 1326725995 | |||
| 508fa39835 | |||
| b1f6462ac3 | |||
| 51eb36f4a6 |
@@ -24,9 +24,10 @@ Keep each layer focused:
|
|||||||
|
|
||||||
## Runtime / tooling
|
## Runtime / tooling
|
||||||
|
|
||||||
The project runs on **Bun**. DB tooling uses **Prisma**; the generated client lives under `generated/prisma` (do NOT edit generated files). Key scripts in `package.json`:
|
The project runs on **Bun** exclusively — **always use `bun` commands, never `npm`, `npx`, or `yarn`**. DB tooling uses **Prisma**; the generated client lives under `generated/prisma` (do NOT edit generated files). Test preloads are configured in `bunfig.toml` so bare `bun test` works. Key scripts in `package.json`:
|
||||||
|
|
||||||
- `dev` — `NODE_ENV=development bun --watch src/index.ts` (start dev server with hot reload)
|
- `dev` — `NODE_ENV=development bun --watch src/index.ts` (start dev server with hot reload)
|
||||||
|
- `test` — `bun test` (runs all tests with preload from `bunfig.toml`)
|
||||||
- `db:gen` — `prisma generate`
|
- `db:gen` — `prisma generate`
|
||||||
- `db:push` — `prisma migrate dev --skip-generate`
|
- `db:push` — `prisma migrate dev --skip-generate`
|
||||||
- `utils:dev` — `docker compose -f .docker/docker-compose.yml up --build`
|
- `utils:dev` — `docker compose -f .docker/docker-compose.yml up --build`
|
||||||
@@ -36,7 +37,7 @@ The project runs on **Bun**. DB tooling uses **Prisma**; the generated client li
|
|||||||
|
|
||||||
## Data layer
|
## Data layer
|
||||||
|
|
||||||
Prisma schema is at `prisma/schema.prisma`. The app imports the generated Prisma client from `generated/prisma/client.ts` (or `generated/prisma/browser.ts` for browser type contexts). The shared `prisma` instance is exported from `src/constants.ts`. Always run `npm run db:gen` after updating `schema.prisma`.
|
Prisma schema is at `prisma/schema.prisma`. The app imports the generated Prisma client from `generated/prisma/client.ts` (or `generated/prisma/browser.ts` for browser type contexts). The shared `prisma` instance is exported from `src/constants.ts`. Always run `bun run db:gen` after updating `schema.prisma`.
|
||||||
|
|
||||||
## Shared constants (`src/constants.ts`)
|
## Shared constants (`src/constants.ts`)
|
||||||
|
|
||||||
@@ -174,13 +175,14 @@ The `UnifiClient` class in `src/modules/unifi-api/UnifiClient.ts` wraps all UniF
|
|||||||
|
|
||||||
## Local dev / quick checks
|
## Local dev / quick checks
|
||||||
|
|
||||||
- Start dev server: `npm run dev`
|
- Start dev server: `bun run dev`
|
||||||
- Regenerate Prisma client: `npm run db:gen`
|
- Run tests: `bun test`
|
||||||
- Apply DB migrations locally: `npm run db:push`
|
- Regenerate Prisma client: `bun run db:gen`
|
||||||
- Docker dev utilities: `npm run utils:dev`
|
- Apply DB migrations locally: `bun run db:push`
|
||||||
- Generate private keys: `npm run utils:gen_private_keys`
|
- Docker dev utilities: `bun run utils:dev`
|
||||||
- Create admin role: `npm run utils:create_admin_role`
|
- Generate private keys: `bun run utils:gen_private_keys`
|
||||||
- Assign user role: `npm run utils:assign_user_role`
|
- Create admin role: `bun run utils:create_admin_role`
|
||||||
|
- Assign user role: `bun run utils:assign_user_role`
|
||||||
|
|
||||||
## When editing generated or infra files
|
## When editing generated or infra files
|
||||||
|
|
||||||
|
|||||||
@@ -5,8 +5,28 @@ on:
|
|||||||
types: [created]
|
types: [created]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
test:
|
||||||
|
name: Test
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout source code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Bun
|
||||||
|
uses: oven-sh/setup-bun@v2
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: bun install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Generate Prisma client
|
||||||
|
run: DATABASE_URL="postgresql://dummy:dummy@localhost:5432/dummy" bunx prisma generate
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
run: bun test --preload ./tests/setup.ts
|
||||||
|
|
||||||
build:
|
build:
|
||||||
name: Build
|
name: Build
|
||||||
|
needs: [test]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
name: Tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: ["**"]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
name: Test
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout source code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Bun
|
||||||
|
uses: oven-sh/setup-bun@v2
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: bun install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Generate Prisma client
|
||||||
|
run: DATABASE_URL="postgresql://dummy:dummy@localhost:5432/dummy" bunx prisma generate
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
run: bun test --preload ./tests/setup.ts
|
||||||
Vendored
+5
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"chat.tools.terminal.autoApprove": {
|
||||||
|
"bun": true
|
||||||
|
}
|
||||||
|
}
|
||||||
+1487
File diff suppressed because it is too large
Load Diff
+4
-1
@@ -67,4 +67,7 @@ RUN bun install --frozen-lockfile
|
|||||||
COPY prisma/ prisma/
|
COPY prisma/ prisma/
|
||||||
COPY prisma.config.ts ./
|
COPY prisma.config.ts ./
|
||||||
|
|
||||||
CMD ["bunx", "prisma", "migrate", "deploy"]
|
COPY prisma/migrate-entrypoint.sh ./prisma/migrate-entrypoint.sh
|
||||||
|
RUN chmod +x prisma/migrate-entrypoint.sh
|
||||||
|
|
||||||
|
CMD ["sh", "prisma/migrate-entrypoint.sh"]
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
setInternalReview - The quote is ready to be review before it is ready to be sent.
|
||||||
|
setInternalApproved - The quote has been approved and is ready to be sent out.
|
||||||
|
setQuoteSent - The Quote has been sent to the customer.
|
||||||
|
setQuoteConfirmed - The quote has been recieved by the customer.
|
||||||
|
setRevisionNeeded - The quote needs to be revised and is set to stage revision
|
||||||
|
setFinalized - This locks any non-admins from modifying the quote saying that is the final iteration of the quote.
|
||||||
|
convert - This converts the quote to a ticket. It will also update all the necessary fields.
|
||||||
|
|
||||||
|
addTime(activityId, user: string)
|
||||||
|
|
||||||
|
fetchProducts
|
||||||
|
updateProduct
|
||||||
|
addProduct
|
||||||
|
|
||||||
|
fetchNotes
|
||||||
|
addNotes(note: string, user: string)
|
||||||
|
|
||||||
|
# Cat/SubCat/Bucket
|
||||||
|
|
||||||
|
## Ecosystems vs Categories
|
||||||
|
|
||||||
|
## Ecosystem Tree
|
||||||
|
|
||||||
|
- Networking
|
||||||
|
- Manufacturer: Ubiquiti
|
||||||
|
- Category: Technology
|
||||||
|
- Subcategory: Network-\*
|
||||||
|
- Manufacturer: TP-Link
|
||||||
|
- Category: Technology
|
||||||
|
- Subcategory: Network-\*
|
||||||
|
- Video Surveillance
|
||||||
|
- Manufacturer: Uniview
|
||||||
|
- Category: Field
|
||||||
|
- Subcategory: Surveillance-\*
|
||||||
|
- Manufacturer: Hikvision
|
||||||
|
- Category: Field
|
||||||
|
- Subcategory: Surveillance-\*
|
||||||
|
- Manufacturer: Alarm.com
|
||||||
|
- Category: Field
|
||||||
|
- Subcategory: Surveillance-\*
|
||||||
|
- Burg/Alarm
|
||||||
|
- Manufacturer: Qolsys
|
||||||
|
- Category: Field
|
||||||
|
- Subcategory: AlarmBurg-\*
|
||||||
|
- DSC
|
||||||
|
- Category: Field
|
||||||
|
- Subcategory: AlarmBurg-\*
|
||||||
|
|
||||||
|
## Category Tree
|
||||||
|
|
||||||
|
- Technology
|
||||||
|
- GeneralEquip
|
||||||
|
- Home Entertainment
|
||||||
|
- Monitor
|
||||||
|
- Printers
|
||||||
|
- Storage
|
||||||
|
- Network
|
||||||
|
- Network-Other
|
||||||
|
- Network-Router
|
||||||
|
- Network-Switch
|
||||||
|
- Network-Wireless
|
||||||
|
- Computer
|
||||||
|
- Computer-Components
|
||||||
|
- Computer-Desktop
|
||||||
|
- Computer-Laptop
|
||||||
|
- Recurring
|
||||||
|
- Recurring - Online
|
||||||
|
- Recurring - Other
|
||||||
|
- Recurring - Protection
|
||||||
|
- Recurring - Telephone
|
||||||
|
- Telephone
|
||||||
|
- Tele-HSet-Digital
|
||||||
|
- Tele-HSet-IP
|
||||||
|
- Tele-HSet-SLT
|
||||||
|
- Tele-Misc
|
||||||
|
- Tele-Paging
|
||||||
|
- Tele-SystemCards
|
||||||
|
- Tele-Systems
|
||||||
|
- General
|
||||||
|
- Batteries
|
||||||
|
- Battery Backups
|
||||||
|
- BulkWire
|
||||||
|
- Cables
|
||||||
|
- Cables-Adapters
|
||||||
|
- Cables-HDMI
|
||||||
|
- Cables-Network
|
||||||
|
- Cables-Other
|
||||||
|
- Cables-USB
|
||||||
|
- Cables-VGA
|
||||||
|
- Elec Cords & Adapters
|
||||||
|
- Enclosures
|
||||||
|
- PowerSupply
|
||||||
|
- RackEquip
|
||||||
|
- RackEquip-Rack
|
||||||
|
- RackEquip-Shelves
|
||||||
|
- Field
|
||||||
|
- Conduit
|
||||||
|
- Electric
|
||||||
|
- GateControl
|
||||||
|
- Locksets
|
||||||
|
- Other
|
||||||
|
- Relays
|
||||||
|
- AccessControl
|
||||||
|
- AccessControl-Controllers
|
||||||
|
- AccessControl-Credential
|
||||||
|
- AccessControl-LockDevices
|
||||||
|
- AccessControl-Other
|
||||||
|
- AccessControl-Readers
|
||||||
|
- AccessControl-VideoEntry
|
||||||
|
- AlarmBurg
|
||||||
|
- AlarmBurg-Communicators
|
||||||
|
- AlarmBurg-Keypads
|
||||||
|
- AlarmBurg-Modules
|
||||||
|
- AlarmBurg-Other
|
||||||
|
- AlarmBurg-Panels
|
||||||
|
- AlarmBurg-Sensors
|
||||||
|
- AlarmBurg-Sensors-Wireless
|
||||||
|
- AlarmBurg-Sensors-Wired
|
||||||
|
- AlarmBurg-Siren
|
||||||
|
- AlarmFire
|
||||||
|
- AlarmFire-Communicators
|
||||||
|
- AlarmFire-Devices
|
||||||
|
- AlarmFire-Modules
|
||||||
|
- AlarmFire-Other
|
||||||
|
- AlarmFire-Panels
|
||||||
|
- AlarmFire-Sensors
|
||||||
|
- Automation
|
||||||
|
- Automation-General
|
||||||
|
- Automation-HVAC
|
||||||
|
- Automation-Lights
|
||||||
|
- Automation-Locks
|
||||||
|
- Automation-Thermostat
|
||||||
|
- AV
|
||||||
|
- AV-Adapters&Cables
|
||||||
|
- AV-Components
|
||||||
|
- AV-Mounts
|
||||||
|
- AV-Other
|
||||||
|
- AV-Speakers
|
||||||
|
- AV-Television
|
||||||
|
- StrCbl?
|
||||||
|
- StrCbl-Jacks
|
||||||
|
- StrCbl-PatchPanel
|
||||||
|
- StrCbl-Plates
|
||||||
|
- Surveillance
|
||||||
|
- Surveillance-Accs
|
||||||
|
- Surveillance-CamerasAnalog
|
||||||
|
- Surveillance-CamerasIP
|
||||||
|
- Surveillance-NVR
|
||||||
+222
@@ -115,6 +115,57 @@ Admin-specific UI permissions that control visibility and data loading for admin
|
|||||||
- **Combine with API permissions**: A user with an admin UI permission should also have the corresponding API permission (e.g., `role.list`) to actually load data.
|
- **Combine with API permissions**: A user with an admin UI permission should also have the corresponding API permission (e.g., `role.list`) to actually load data.
|
||||||
- **Use wildcards for flexibility**: Grant `ui.navigation.*.view` to allow all navigation sections.
|
- **Use wildcards for flexibility**: Grant `ui.navigation.*.view` to allow all navigation sections.
|
||||||
|
|
||||||
|
### 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, 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 (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 (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` |
|
||||||
|
| `sales.opportunity.product.add` | Add a new product (forecast item) to an opportunity. Individual fields gated by `sales.opportunity.product.field.<field>` permissions. | [src/api/sales/[id]/addProduct.ts](src/api/sales/[id]/addProduct.ts) | `sales.opportunity.fetch` |
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><strong>Field-level permissions for <code>sales.opportunity.product.add</code></strong></summary>
|
||||||
|
|
||||||
|
Each submitted field is gated by a `sales.opportunity.product.field.<field>` permission node. Only fields the user has permission for are forwarded to ConnectWise.
|
||||||
|
|
||||||
|
| Field Permission Node | Description |
|
||||||
|
| ----------------------------------------------------- | -------------------------------------------------------- |
|
||||||
|
| `sales.opportunity.product.field.catalogItem` | Set the catalog item reference |
|
||||||
|
| `sales.opportunity.product.field.forecastDescription` | Set the forecast description |
|
||||||
|
| `sales.opportunity.product.field.productDescription` | Set the product description |
|
||||||
|
| `sales.opportunity.product.field.quantity` | Set the quantity |
|
||||||
|
| `sales.opportunity.product.field.status` | Set the status reference |
|
||||||
|
| `sales.opportunity.product.field.productClass` | Set the product class (e.g. Product, Service, Agreement) |
|
||||||
|
| `sales.opportunity.product.field.forecastType` | Set the forecast type |
|
||||||
|
| `sales.opportunity.product.field.revenue` | Set the revenue amount |
|
||||||
|
| `sales.opportunity.product.field.cost` | Set the cost amount |
|
||||||
|
| `sales.opportunity.product.field.includeFlag` | Set the include flag |
|
||||||
|
| `sales.opportunity.product.field.linkFlag` | Set the link flag |
|
||||||
|
| `sales.opportunity.product.field.recurringFlag` | Set the recurring flag |
|
||||||
|
| `sales.opportunity.product.field.taxableFlag` | Set the taxable flag |
|
||||||
|
| `sales.opportunity.product.field.recurringRevenue` | Set the recurring revenue amount |
|
||||||
|
| `sales.opportunity.product.field.recurringCost` | Set the recurring cost amount |
|
||||||
|
| `sales.opportunity.product.field.cycles` | Set the number of recurring cycles |
|
||||||
|
| `sales.opportunity.product.field.sequenceNumber` | Set the sequence number (display order) |
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
### UniFi Permissions
|
### UniFi Permissions
|
||||||
|
|
||||||
Permissions for accessing and managing UniFi network infrastructure. The `unifi.access` permission is a gate permission required for **all** UniFi routes.
|
Permissions for accessing and managing UniFi network infrastructure. The `unifi.access` permission is a gate permission required for **all** UniFi routes.
|
||||||
@@ -152,6 +203,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` | 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` |
|
| `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
|
## Permission Issuers
|
||||||
|
|
||||||
Permissions can be issued by different sources:
|
Permissions can be issued by different sources:
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
"cors": "^2.8.6",
|
"cors": "^2.8.6",
|
||||||
"cuid": "^3.0.0",
|
"cuid": "^3.0.0",
|
||||||
"hono": "^4.11.5",
|
"hono": "^4.11.5",
|
||||||
|
"ioredis": "^5.10.0",
|
||||||
"jsonwebtoken": "^9.0.3",
|
"jsonwebtoken": "^9.0.3",
|
||||||
"keypair": "^1.0.4",
|
"keypair": "^1.0.4",
|
||||||
"prisma": "^7.3.0",
|
"prisma": "^7.3.0",
|
||||||
@@ -57,6 +58,8 @@
|
|||||||
|
|
||||||
"@hono/node-server": ["@hono/node-server@1.19.9", "", { "peerDependencies": { "hono": "^4" } }, "sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw=="],
|
"@hono/node-server": ["@hono/node-server@1.19.9", "", { "peerDependencies": { "hono": "^4" } }, "sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw=="],
|
||||||
|
|
||||||
|
"@ioredis/commands": ["@ioredis/commands@1.5.1", "", {}, "sha512-JH8ZL/ywcJyR9MmJ5BNqZllXNZQqQbnVZOqpPQqE1vHiFgAw4NHbvE0FOduNU8IX9babitBT46571OnPTT0Zcw=="],
|
||||||
|
|
||||||
"@mrleebo/prisma-ast": ["@mrleebo/prisma-ast@0.13.1", "", { "dependencies": { "chevrotain": "^10.5.0", "lilconfig": "^2.1.0" } }, "sha512-XyroGQXcHrZdvmrGJvsA9KNeOOgGMg1Vg9OlheUsBOSKznLMDl+YChxbkboRHvtFYJEMRYmlV3uoo/njCw05iw=="],
|
"@mrleebo/prisma-ast": ["@mrleebo/prisma-ast@0.13.1", "", { "dependencies": { "chevrotain": "^10.5.0", "lilconfig": "^2.1.0" } }, "sha512-XyroGQXcHrZdvmrGJvsA9KNeOOgGMg1Vg9OlheUsBOSKznLMDl+YChxbkboRHvtFYJEMRYmlV3uoo/njCw05iw=="],
|
||||||
|
|
||||||
"@prisma/adapter-pg": ["@prisma/adapter-pg@7.3.0", "", { "dependencies": { "@prisma/driver-adapter-utils": "7.3.0", "pg": "^8.16.3", "postgres-array": "3.0.4" } }, "sha512-iuYQMbIPO6i9O45Fv8TB7vWu00BXhCaNAShenqF7gLExGDbnGp5BfFB4yz1K59zQ59jF6tQ9YHrg0P6/J3OoLg=="],
|
"@prisma/adapter-pg": ["@prisma/adapter-pg@7.3.0", "", { "dependencies": { "@prisma/driver-adapter-utils": "7.3.0", "pg": "^8.16.3", "postgres-array": "3.0.4" } }, "sha512-iuYQMbIPO6i9O45Fv8TB7vWu00BXhCaNAShenqF7gLExGDbnGp5BfFB4yz1K59zQ59jF6tQ9YHrg0P6/J3OoLg=="],
|
||||||
@@ -131,6 +134,8 @@
|
|||||||
|
|
||||||
"citty": ["citty@0.1.6", "", { "dependencies": { "consola": "^3.2.3" } }, "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ=="],
|
"citty": ["citty@0.1.6", "", { "dependencies": { "consola": "^3.2.3" } }, "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ=="],
|
||||||
|
|
||||||
|
"cluster-key-slot": ["cluster-key-slot@1.1.2", "", {}, "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA=="],
|
||||||
|
|
||||||
"combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="],
|
"combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="],
|
||||||
|
|
||||||
"confbox": ["confbox@0.2.2", "", {}, "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ=="],
|
"confbox": ["confbox@0.2.2", "", {}, "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ=="],
|
||||||
@@ -223,6 +228,8 @@
|
|||||||
|
|
||||||
"iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="],
|
"iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="],
|
||||||
|
|
||||||
|
"ioredis": ["ioredis@5.10.0", "", { "dependencies": { "@ioredis/commands": "1.5.1", "cluster-key-slot": "^1.1.0", "debug": "^4.3.4", "denque": "^2.1.0", "lodash.defaults": "^4.2.0", "lodash.isarguments": "^3.1.0", "redis-errors": "^1.2.0", "redis-parser": "^3.0.0", "standard-as-callback": "^2.1.0" } }, "sha512-HVBe9OFuqs+Z6n64q09PQvP1/R4Bm+30PAyyD4wIEqssh3v9L21QjCVk4kRLucMBcDokJTcLjsGeVRlq/nH6DA=="],
|
||||||
|
|
||||||
"is-property": ["is-property@1.0.2", "", {}, "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g=="],
|
"is-property": ["is-property@1.0.2", "", {}, "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g=="],
|
||||||
|
|
||||||
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
||||||
@@ -241,8 +248,12 @@
|
|||||||
|
|
||||||
"lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="],
|
"lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="],
|
||||||
|
|
||||||
|
"lodash.defaults": ["lodash.defaults@4.2.0", "", {}, "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ=="],
|
||||||
|
|
||||||
"lodash.includes": ["lodash.includes@4.3.0", "", {}, "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w=="],
|
"lodash.includes": ["lodash.includes@4.3.0", "", {}, "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w=="],
|
||||||
|
|
||||||
|
"lodash.isarguments": ["lodash.isarguments@3.1.0", "", {}, "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg=="],
|
||||||
|
|
||||||
"lodash.isboolean": ["lodash.isboolean@3.0.3", "", {}, "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg=="],
|
"lodash.isboolean": ["lodash.isboolean@3.0.3", "", {}, "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg=="],
|
||||||
|
|
||||||
"lodash.isinteger": ["lodash.isinteger@4.0.4", "", {}, "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA=="],
|
"lodash.isinteger": ["lodash.isinteger@4.0.4", "", {}, "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA=="],
|
||||||
@@ -331,6 +342,10 @@
|
|||||||
|
|
||||||
"readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
|
"readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
|
||||||
|
|
||||||
|
"redis-errors": ["redis-errors@1.2.0", "", {}, "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w=="],
|
||||||
|
|
||||||
|
"redis-parser": ["redis-parser@3.0.0", "", { "dependencies": { "redis-errors": "^1.0.0" } }, "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A=="],
|
||||||
|
|
||||||
"regexp-to-ast": ["regexp-to-ast@0.5.0", "", {}, "sha512-tlbJqcMHnPKI9zSrystikWKwHkBqu2a/Sgw01h3zFjvYrMxEDYHzzoMZnUrbIfpTFEsoRnnviOXNCzFiSc54Qw=="],
|
"regexp-to-ast": ["regexp-to-ast@0.5.0", "", {}, "sha512-tlbJqcMHnPKI9zSrystikWKwHkBqu2a/Sgw01h3zFjvYrMxEDYHzzoMZnUrbIfpTFEsoRnnviOXNCzFiSc54Qw=="],
|
||||||
|
|
||||||
"remeda": ["remeda@2.33.4", "", {}, "sha512-ygHswjlc/opg2VrtiYvUOPLjxjtdKvjGz1/plDhkG66hjNjFr1xmfrs2ClNFo/E6TyUFiwYNh53bKV26oBoMGQ=="],
|
"remeda": ["remeda@2.33.4", "", {}, "sha512-ygHswjlc/opg2VrtiYvUOPLjxjtdKvjGz1/plDhkG66hjNjFr1xmfrs2ClNFo/E6TyUFiwYNh53bKV26oBoMGQ=="],
|
||||||
@@ -363,6 +378,8 @@
|
|||||||
|
|
||||||
"sqlstring": ["sqlstring@2.3.3", "", {}, "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg=="],
|
"sqlstring": ["sqlstring@2.3.3", "", {}, "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg=="],
|
||||||
|
|
||||||
|
"standard-as-callback": ["standard-as-callback@2.1.0", "", {}, "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A=="],
|
||||||
|
|
||||||
"std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="],
|
"std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="],
|
||||||
|
|
||||||
"tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="],
|
"tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="],
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
[test]
|
||||||
|
preload = ["./tests/setup.ts"]
|
||||||
@@ -47,6 +47,11 @@ export type Company = Prisma.CompanyModel
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export type CatalogItem = Prisma.CatalogItemModel
|
export type CatalogItem = Prisma.CatalogItemModel
|
||||||
|
/**
|
||||||
|
* Model Opportunity
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export type Opportunity = Prisma.OpportunityModel
|
||||||
/**
|
/**
|
||||||
* Model CredentialType
|
* Model CredentialType
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -69,6 +69,11 @@ export type Company = Prisma.CompanyModel
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export type CatalogItem = Prisma.CatalogItemModel
|
export type CatalogItem = Prisma.CatalogItemModel
|
||||||
|
/**
|
||||||
|
* Model Opportunity
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export type Opportunity = Prisma.OpportunityModel
|
||||||
/**
|
/**
|
||||||
* Model CredentialType
|
* Model CredentialType
|
||||||
*
|
*
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -390,6 +390,7 @@ export const ModelName = {
|
|||||||
UnifiSite: 'UnifiSite',
|
UnifiSite: 'UnifiSite',
|
||||||
Company: 'Company',
|
Company: 'Company',
|
||||||
CatalogItem: 'CatalogItem',
|
CatalogItem: 'CatalogItem',
|
||||||
|
Opportunity: 'Opportunity',
|
||||||
CredentialType: 'CredentialType',
|
CredentialType: 'CredentialType',
|
||||||
SecureValue: 'SecureValue',
|
SecureValue: 'SecureValue',
|
||||||
Credential: 'Credential'
|
Credential: 'Credential'
|
||||||
@@ -408,7 +409,7 @@ export type TypeMap<ExtArgs extends runtime.Types.Extensions.InternalArgs = runt
|
|||||||
omit: GlobalOmitOptions
|
omit: GlobalOmitOptions
|
||||||
}
|
}
|
||||||
meta: {
|
meta: {
|
||||||
modelProps: "session" | "user" | "role" | "unifiSite" | "company" | "catalogItem" | "credentialType" | "secureValue" | "credential"
|
modelProps: "session" | "user" | "role" | "unifiSite" | "company" | "catalogItem" | "opportunity" | "credentialType" | "secureValue" | "credential"
|
||||||
txIsolationLevel: TransactionIsolationLevel
|
txIsolationLevel: TransactionIsolationLevel
|
||||||
}
|
}
|
||||||
model: {
|
model: {
|
||||||
@@ -856,6 +857,80 @@ export type TypeMap<ExtArgs extends runtime.Types.Extensions.InternalArgs = runt
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Opportunity: {
|
||||||
|
payload: Prisma.$OpportunityPayload<ExtArgs>
|
||||||
|
fields: Prisma.OpportunityFieldRefs
|
||||||
|
operations: {
|
||||||
|
findUnique: {
|
||||||
|
args: Prisma.OpportunityFindUniqueArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$OpportunityPayload> | null
|
||||||
|
}
|
||||||
|
findUniqueOrThrow: {
|
||||||
|
args: Prisma.OpportunityFindUniqueOrThrowArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$OpportunityPayload>
|
||||||
|
}
|
||||||
|
findFirst: {
|
||||||
|
args: Prisma.OpportunityFindFirstArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$OpportunityPayload> | null
|
||||||
|
}
|
||||||
|
findFirstOrThrow: {
|
||||||
|
args: Prisma.OpportunityFindFirstOrThrowArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$OpportunityPayload>
|
||||||
|
}
|
||||||
|
findMany: {
|
||||||
|
args: Prisma.OpportunityFindManyArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$OpportunityPayload>[]
|
||||||
|
}
|
||||||
|
create: {
|
||||||
|
args: Prisma.OpportunityCreateArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$OpportunityPayload>
|
||||||
|
}
|
||||||
|
createMany: {
|
||||||
|
args: Prisma.OpportunityCreateManyArgs<ExtArgs>
|
||||||
|
result: BatchPayload
|
||||||
|
}
|
||||||
|
createManyAndReturn: {
|
||||||
|
args: Prisma.OpportunityCreateManyAndReturnArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$OpportunityPayload>[]
|
||||||
|
}
|
||||||
|
delete: {
|
||||||
|
args: Prisma.OpportunityDeleteArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$OpportunityPayload>
|
||||||
|
}
|
||||||
|
update: {
|
||||||
|
args: Prisma.OpportunityUpdateArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$OpportunityPayload>
|
||||||
|
}
|
||||||
|
deleteMany: {
|
||||||
|
args: Prisma.OpportunityDeleteManyArgs<ExtArgs>
|
||||||
|
result: BatchPayload
|
||||||
|
}
|
||||||
|
updateMany: {
|
||||||
|
args: Prisma.OpportunityUpdateManyArgs<ExtArgs>
|
||||||
|
result: BatchPayload
|
||||||
|
}
|
||||||
|
updateManyAndReturn: {
|
||||||
|
args: Prisma.OpportunityUpdateManyAndReturnArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$OpportunityPayload>[]
|
||||||
|
}
|
||||||
|
upsert: {
|
||||||
|
args: Prisma.OpportunityUpsertArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$OpportunityPayload>
|
||||||
|
}
|
||||||
|
aggregate: {
|
||||||
|
args: Prisma.OpportunityAggregateArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.Optional<Prisma.AggregateOpportunity>
|
||||||
|
}
|
||||||
|
groupBy: {
|
||||||
|
args: Prisma.OpportunityGroupByArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.Optional<Prisma.OpportunityGroupByOutputType>[]
|
||||||
|
}
|
||||||
|
count: {
|
||||||
|
args: Prisma.OpportunityCountArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.Optional<Prisma.OpportunityCountAggregateOutputType> | number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
CredentialType: {
|
CredentialType: {
|
||||||
payload: Prisma.$CredentialTypePayload<ExtArgs>
|
payload: Prisma.$CredentialTypePayload<ExtArgs>
|
||||||
fields: Prisma.CredentialTypeFieldRefs
|
fields: Prisma.CredentialTypeFieldRefs
|
||||||
@@ -1138,6 +1213,7 @@ export const UserScalarFieldEnum = {
|
|||||||
email: 'email',
|
email: 'email',
|
||||||
emailVerified: 'emailVerified',
|
emailVerified: 'emailVerified',
|
||||||
image: 'image',
|
image: 'image',
|
||||||
|
cwIdentifier: 'cwIdentifier',
|
||||||
userId: 'userId',
|
userId: 'userId',
|
||||||
token: 'token',
|
token: 'token',
|
||||||
createdAt: 'createdAt',
|
createdAt: 'createdAt',
|
||||||
@@ -1186,10 +1262,15 @@ export type CompanyScalarFieldEnum = (typeof CompanyScalarFieldEnum)[keyof typeo
|
|||||||
export const CatalogItemScalarFieldEnum = {
|
export const CatalogItemScalarFieldEnum = {
|
||||||
id: 'id',
|
id: 'id',
|
||||||
cwCatalogId: 'cwCatalogId',
|
cwCatalogId: 'cwCatalogId',
|
||||||
|
identifier: 'identifier',
|
||||||
name: 'name',
|
name: 'name',
|
||||||
description: 'description',
|
description: 'description',
|
||||||
customerDescription: 'customerDescription',
|
customerDescription: 'customerDescription',
|
||||||
internalNotes: 'internalNotes',
|
internalNotes: 'internalNotes',
|
||||||
|
category: 'category',
|
||||||
|
categoryCwId: 'categoryCwId',
|
||||||
|
subcategory: 'subcategory',
|
||||||
|
subcategoryCwId: 'subcategoryCwId',
|
||||||
manufacturer: 'manufacturer',
|
manufacturer: 'manufacturer',
|
||||||
manufactureCwId: 'manufactureCwId',
|
manufactureCwId: 'manufactureCwId',
|
||||||
partNumber: 'partNumber',
|
partNumber: 'partNumber',
|
||||||
@@ -1209,6 +1290,59 @@ export const CatalogItemScalarFieldEnum = {
|
|||||||
export type CatalogItemScalarFieldEnum = (typeof CatalogItemScalarFieldEnum)[keyof typeof CatalogItemScalarFieldEnum]
|
export type CatalogItemScalarFieldEnum = (typeof CatalogItemScalarFieldEnum)[keyof typeof CatalogItemScalarFieldEnum]
|
||||||
|
|
||||||
|
|
||||||
|
export const OpportunityScalarFieldEnum = {
|
||||||
|
id: 'id',
|
||||||
|
cwOpportunityId: 'cwOpportunityId',
|
||||||
|
name: 'name',
|
||||||
|
notes: 'notes',
|
||||||
|
typeName: 'typeName',
|
||||||
|
typeCwId: 'typeCwId',
|
||||||
|
stageName: 'stageName',
|
||||||
|
stageCwId: 'stageCwId',
|
||||||
|
statusName: 'statusName',
|
||||||
|
statusCwId: 'statusCwId',
|
||||||
|
priorityName: 'priorityName',
|
||||||
|
priorityCwId: 'priorityCwId',
|
||||||
|
ratingName: 'ratingName',
|
||||||
|
ratingCwId: 'ratingCwId',
|
||||||
|
source: 'source',
|
||||||
|
campaignName: 'campaignName',
|
||||||
|
campaignCwId: 'campaignCwId',
|
||||||
|
primarySalesRepName: 'primarySalesRepName',
|
||||||
|
primarySalesRepIdentifier: 'primarySalesRepIdentifier',
|
||||||
|
primarySalesRepCwId: 'primarySalesRepCwId',
|
||||||
|
secondarySalesRepName: 'secondarySalesRepName',
|
||||||
|
secondarySalesRepIdentifier: 'secondarySalesRepIdentifier',
|
||||||
|
secondarySalesRepCwId: 'secondarySalesRepCwId',
|
||||||
|
companyCwId: 'companyCwId',
|
||||||
|
companyName: 'companyName',
|
||||||
|
contactCwId: 'contactCwId',
|
||||||
|
contactName: 'contactName',
|
||||||
|
siteCwId: 'siteCwId',
|
||||||
|
siteName: 'siteName',
|
||||||
|
customerPO: 'customerPO',
|
||||||
|
totalSalesTax: 'totalSalesTax',
|
||||||
|
locationName: 'locationName',
|
||||||
|
locationCwId: 'locationCwId',
|
||||||
|
departmentName: 'departmentName',
|
||||||
|
departmentCwId: 'departmentCwId',
|
||||||
|
expectedCloseDate: 'expectedCloseDate',
|
||||||
|
pipelineChangeDate: 'pipelineChangeDate',
|
||||||
|
dateBecameLead: 'dateBecameLead',
|
||||||
|
closedDate: 'closedDate',
|
||||||
|
closedFlag: 'closedFlag',
|
||||||
|
closedByName: 'closedByName',
|
||||||
|
closedByCwId: 'closedByCwId',
|
||||||
|
companyId: 'companyId',
|
||||||
|
productSequence: 'productSequence',
|
||||||
|
cwLastUpdated: 'cwLastUpdated',
|
||||||
|
createdAt: 'createdAt',
|
||||||
|
updatedAt: 'updatedAt'
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type OpportunityScalarFieldEnum = (typeof OpportunityScalarFieldEnum)[keyof typeof OpportunityScalarFieldEnum]
|
||||||
|
|
||||||
|
|
||||||
export const CredentialTypeScalarFieldEnum = {
|
export const CredentialTypeScalarFieldEnum = {
|
||||||
id: 'id',
|
id: 'id',
|
||||||
name: 'name',
|
name: 'name',
|
||||||
@@ -1473,6 +1607,7 @@ export type GlobalOmitConfig = {
|
|||||||
unifiSite?: Prisma.UnifiSiteOmit
|
unifiSite?: Prisma.UnifiSiteOmit
|
||||||
company?: Prisma.CompanyOmit
|
company?: Prisma.CompanyOmit
|
||||||
catalogItem?: Prisma.CatalogItemOmit
|
catalogItem?: Prisma.CatalogItemOmit
|
||||||
|
opportunity?: Prisma.OpportunityOmit
|
||||||
credentialType?: Prisma.CredentialTypeOmit
|
credentialType?: Prisma.CredentialTypeOmit
|
||||||
secureValue?: Prisma.SecureValueOmit
|
secureValue?: Prisma.SecureValueOmit
|
||||||
credential?: Prisma.CredentialOmit
|
credential?: Prisma.CredentialOmit
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ export const ModelName = {
|
|||||||
UnifiSite: 'UnifiSite',
|
UnifiSite: 'UnifiSite',
|
||||||
Company: 'Company',
|
Company: 'Company',
|
||||||
CatalogItem: 'CatalogItem',
|
CatalogItem: 'CatalogItem',
|
||||||
|
Opportunity: 'Opportunity',
|
||||||
CredentialType: 'CredentialType',
|
CredentialType: 'CredentialType',
|
||||||
SecureValue: 'SecureValue',
|
SecureValue: 'SecureValue',
|
||||||
Credential: 'Credential'
|
Credential: 'Credential'
|
||||||
@@ -99,6 +100,7 @@ export const UserScalarFieldEnum = {
|
|||||||
email: 'email',
|
email: 'email',
|
||||||
emailVerified: 'emailVerified',
|
emailVerified: 'emailVerified',
|
||||||
image: 'image',
|
image: 'image',
|
||||||
|
cwIdentifier: 'cwIdentifier',
|
||||||
userId: 'userId',
|
userId: 'userId',
|
||||||
token: 'token',
|
token: 'token',
|
||||||
createdAt: 'createdAt',
|
createdAt: 'createdAt',
|
||||||
@@ -147,10 +149,15 @@ export type CompanyScalarFieldEnum = (typeof CompanyScalarFieldEnum)[keyof typeo
|
|||||||
export const CatalogItemScalarFieldEnum = {
|
export const CatalogItemScalarFieldEnum = {
|
||||||
id: 'id',
|
id: 'id',
|
||||||
cwCatalogId: 'cwCatalogId',
|
cwCatalogId: 'cwCatalogId',
|
||||||
|
identifier: 'identifier',
|
||||||
name: 'name',
|
name: 'name',
|
||||||
description: 'description',
|
description: 'description',
|
||||||
customerDescription: 'customerDescription',
|
customerDescription: 'customerDescription',
|
||||||
internalNotes: 'internalNotes',
|
internalNotes: 'internalNotes',
|
||||||
|
category: 'category',
|
||||||
|
categoryCwId: 'categoryCwId',
|
||||||
|
subcategory: 'subcategory',
|
||||||
|
subcategoryCwId: 'subcategoryCwId',
|
||||||
manufacturer: 'manufacturer',
|
manufacturer: 'manufacturer',
|
||||||
manufactureCwId: 'manufactureCwId',
|
manufactureCwId: 'manufactureCwId',
|
||||||
partNumber: 'partNumber',
|
partNumber: 'partNumber',
|
||||||
@@ -170,6 +177,59 @@ export const CatalogItemScalarFieldEnum = {
|
|||||||
export type CatalogItemScalarFieldEnum = (typeof CatalogItemScalarFieldEnum)[keyof typeof CatalogItemScalarFieldEnum]
|
export type CatalogItemScalarFieldEnum = (typeof CatalogItemScalarFieldEnum)[keyof typeof CatalogItemScalarFieldEnum]
|
||||||
|
|
||||||
|
|
||||||
|
export const OpportunityScalarFieldEnum = {
|
||||||
|
id: 'id',
|
||||||
|
cwOpportunityId: 'cwOpportunityId',
|
||||||
|
name: 'name',
|
||||||
|
notes: 'notes',
|
||||||
|
typeName: 'typeName',
|
||||||
|
typeCwId: 'typeCwId',
|
||||||
|
stageName: 'stageName',
|
||||||
|
stageCwId: 'stageCwId',
|
||||||
|
statusName: 'statusName',
|
||||||
|
statusCwId: 'statusCwId',
|
||||||
|
priorityName: 'priorityName',
|
||||||
|
priorityCwId: 'priorityCwId',
|
||||||
|
ratingName: 'ratingName',
|
||||||
|
ratingCwId: 'ratingCwId',
|
||||||
|
source: 'source',
|
||||||
|
campaignName: 'campaignName',
|
||||||
|
campaignCwId: 'campaignCwId',
|
||||||
|
primarySalesRepName: 'primarySalesRepName',
|
||||||
|
primarySalesRepIdentifier: 'primarySalesRepIdentifier',
|
||||||
|
primarySalesRepCwId: 'primarySalesRepCwId',
|
||||||
|
secondarySalesRepName: 'secondarySalesRepName',
|
||||||
|
secondarySalesRepIdentifier: 'secondarySalesRepIdentifier',
|
||||||
|
secondarySalesRepCwId: 'secondarySalesRepCwId',
|
||||||
|
companyCwId: 'companyCwId',
|
||||||
|
companyName: 'companyName',
|
||||||
|
contactCwId: 'contactCwId',
|
||||||
|
contactName: 'contactName',
|
||||||
|
siteCwId: 'siteCwId',
|
||||||
|
siteName: 'siteName',
|
||||||
|
customerPO: 'customerPO',
|
||||||
|
totalSalesTax: 'totalSalesTax',
|
||||||
|
locationName: 'locationName',
|
||||||
|
locationCwId: 'locationCwId',
|
||||||
|
departmentName: 'departmentName',
|
||||||
|
departmentCwId: 'departmentCwId',
|
||||||
|
expectedCloseDate: 'expectedCloseDate',
|
||||||
|
pipelineChangeDate: 'pipelineChangeDate',
|
||||||
|
dateBecameLead: 'dateBecameLead',
|
||||||
|
closedDate: 'closedDate',
|
||||||
|
closedFlag: 'closedFlag',
|
||||||
|
closedByName: 'closedByName',
|
||||||
|
closedByCwId: 'closedByCwId',
|
||||||
|
companyId: 'companyId',
|
||||||
|
productSequence: 'productSequence',
|
||||||
|
cwLastUpdated: 'cwLastUpdated',
|
||||||
|
createdAt: 'createdAt',
|
||||||
|
updatedAt: 'updatedAt'
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type OpportunityScalarFieldEnum = (typeof OpportunityScalarFieldEnum)[keyof typeof OpportunityScalarFieldEnum]
|
||||||
|
|
||||||
|
|
||||||
export const CredentialTypeScalarFieldEnum = {
|
export const CredentialTypeScalarFieldEnum = {
|
||||||
id: 'id',
|
id: 'id',
|
||||||
name: 'name',
|
name: 'name',
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ export type * from './models/Role.ts'
|
|||||||
export type * from './models/UnifiSite.ts'
|
export type * from './models/UnifiSite.ts'
|
||||||
export type * from './models/Company.ts'
|
export type * from './models/Company.ts'
|
||||||
export type * from './models/CatalogItem.ts'
|
export type * from './models/CatalogItem.ts'
|
||||||
|
export type * from './models/Opportunity.ts'
|
||||||
export type * from './models/CredentialType.ts'
|
export type * from './models/CredentialType.ts'
|
||||||
export type * from './models/SecureValue.ts'
|
export type * from './models/SecureValue.ts'
|
||||||
export type * from './models/Credential.ts'
|
export type * from './models/Credential.ts'
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ export type AggregateCatalogItem = {
|
|||||||
|
|
||||||
export type CatalogItemAvgAggregateOutputType = {
|
export type CatalogItemAvgAggregateOutputType = {
|
||||||
cwCatalogId: number | null
|
cwCatalogId: number | null
|
||||||
|
categoryCwId: number | null
|
||||||
|
subcategoryCwId: number | null
|
||||||
manufactureCwId: number | null
|
manufactureCwId: number | null
|
||||||
vendorCwId: number | null
|
vendorCwId: number | null
|
||||||
price: number | null
|
price: number | null
|
||||||
@@ -37,6 +39,8 @@ export type CatalogItemAvgAggregateOutputType = {
|
|||||||
|
|
||||||
export type CatalogItemSumAggregateOutputType = {
|
export type CatalogItemSumAggregateOutputType = {
|
||||||
cwCatalogId: number | null
|
cwCatalogId: number | null
|
||||||
|
categoryCwId: number | null
|
||||||
|
subcategoryCwId: number | null
|
||||||
manufactureCwId: number | null
|
manufactureCwId: number | null
|
||||||
vendorCwId: number | null
|
vendorCwId: number | null
|
||||||
price: number | null
|
price: number | null
|
||||||
@@ -47,10 +51,15 @@ export type CatalogItemSumAggregateOutputType = {
|
|||||||
export type CatalogItemMinAggregateOutputType = {
|
export type CatalogItemMinAggregateOutputType = {
|
||||||
id: string | null
|
id: string | null
|
||||||
cwCatalogId: number | null
|
cwCatalogId: number | null
|
||||||
|
identifier: string | null
|
||||||
name: string | null
|
name: string | null
|
||||||
description: string | null
|
description: string | null
|
||||||
customerDescription: string | null
|
customerDescription: string | null
|
||||||
internalNotes: string | null
|
internalNotes: string | null
|
||||||
|
category: string | null
|
||||||
|
categoryCwId: number | null
|
||||||
|
subcategory: string | null
|
||||||
|
subcategoryCwId: number | null
|
||||||
manufacturer: string | null
|
manufacturer: string | null
|
||||||
manufactureCwId: number | null
|
manufactureCwId: number | null
|
||||||
partNumber: string | null
|
partNumber: string | null
|
||||||
@@ -70,10 +79,15 @@ export type CatalogItemMinAggregateOutputType = {
|
|||||||
export type CatalogItemMaxAggregateOutputType = {
|
export type CatalogItemMaxAggregateOutputType = {
|
||||||
id: string | null
|
id: string | null
|
||||||
cwCatalogId: number | null
|
cwCatalogId: number | null
|
||||||
|
identifier: string | null
|
||||||
name: string | null
|
name: string | null
|
||||||
description: string | null
|
description: string | null
|
||||||
customerDescription: string | null
|
customerDescription: string | null
|
||||||
internalNotes: string | null
|
internalNotes: string | null
|
||||||
|
category: string | null
|
||||||
|
categoryCwId: number | null
|
||||||
|
subcategory: string | null
|
||||||
|
subcategoryCwId: number | null
|
||||||
manufacturer: string | null
|
manufacturer: string | null
|
||||||
manufactureCwId: number | null
|
manufactureCwId: number | null
|
||||||
partNumber: string | null
|
partNumber: string | null
|
||||||
@@ -93,10 +107,15 @@ export type CatalogItemMaxAggregateOutputType = {
|
|||||||
export type CatalogItemCountAggregateOutputType = {
|
export type CatalogItemCountAggregateOutputType = {
|
||||||
id: number
|
id: number
|
||||||
cwCatalogId: number
|
cwCatalogId: number
|
||||||
|
identifier: number
|
||||||
name: number
|
name: number
|
||||||
description: number
|
description: number
|
||||||
customerDescription: number
|
customerDescription: number
|
||||||
internalNotes: number
|
internalNotes: number
|
||||||
|
category: number
|
||||||
|
categoryCwId: number
|
||||||
|
subcategory: number
|
||||||
|
subcategoryCwId: number
|
||||||
manufacturer: number
|
manufacturer: number
|
||||||
manufactureCwId: number
|
manufactureCwId: number
|
||||||
partNumber: number
|
partNumber: number
|
||||||
@@ -117,6 +136,8 @@ export type CatalogItemCountAggregateOutputType = {
|
|||||||
|
|
||||||
export type CatalogItemAvgAggregateInputType = {
|
export type CatalogItemAvgAggregateInputType = {
|
||||||
cwCatalogId?: true
|
cwCatalogId?: true
|
||||||
|
categoryCwId?: true
|
||||||
|
subcategoryCwId?: true
|
||||||
manufactureCwId?: true
|
manufactureCwId?: true
|
||||||
vendorCwId?: true
|
vendorCwId?: true
|
||||||
price?: true
|
price?: true
|
||||||
@@ -126,6 +147,8 @@ export type CatalogItemAvgAggregateInputType = {
|
|||||||
|
|
||||||
export type CatalogItemSumAggregateInputType = {
|
export type CatalogItemSumAggregateInputType = {
|
||||||
cwCatalogId?: true
|
cwCatalogId?: true
|
||||||
|
categoryCwId?: true
|
||||||
|
subcategoryCwId?: true
|
||||||
manufactureCwId?: true
|
manufactureCwId?: true
|
||||||
vendorCwId?: true
|
vendorCwId?: true
|
||||||
price?: true
|
price?: true
|
||||||
@@ -136,10 +159,15 @@ export type CatalogItemSumAggregateInputType = {
|
|||||||
export type CatalogItemMinAggregateInputType = {
|
export type CatalogItemMinAggregateInputType = {
|
||||||
id?: true
|
id?: true
|
||||||
cwCatalogId?: true
|
cwCatalogId?: true
|
||||||
|
identifier?: true
|
||||||
name?: true
|
name?: true
|
||||||
description?: true
|
description?: true
|
||||||
customerDescription?: true
|
customerDescription?: true
|
||||||
internalNotes?: true
|
internalNotes?: true
|
||||||
|
category?: true
|
||||||
|
categoryCwId?: true
|
||||||
|
subcategory?: true
|
||||||
|
subcategoryCwId?: true
|
||||||
manufacturer?: true
|
manufacturer?: true
|
||||||
manufactureCwId?: true
|
manufactureCwId?: true
|
||||||
partNumber?: true
|
partNumber?: true
|
||||||
@@ -159,10 +187,15 @@ export type CatalogItemMinAggregateInputType = {
|
|||||||
export type CatalogItemMaxAggregateInputType = {
|
export type CatalogItemMaxAggregateInputType = {
|
||||||
id?: true
|
id?: true
|
||||||
cwCatalogId?: true
|
cwCatalogId?: true
|
||||||
|
identifier?: true
|
||||||
name?: true
|
name?: true
|
||||||
description?: true
|
description?: true
|
||||||
customerDescription?: true
|
customerDescription?: true
|
||||||
internalNotes?: true
|
internalNotes?: true
|
||||||
|
category?: true
|
||||||
|
categoryCwId?: true
|
||||||
|
subcategory?: true
|
||||||
|
subcategoryCwId?: true
|
||||||
manufacturer?: true
|
manufacturer?: true
|
||||||
manufactureCwId?: true
|
manufactureCwId?: true
|
||||||
partNumber?: true
|
partNumber?: true
|
||||||
@@ -182,10 +215,15 @@ export type CatalogItemMaxAggregateInputType = {
|
|||||||
export type CatalogItemCountAggregateInputType = {
|
export type CatalogItemCountAggregateInputType = {
|
||||||
id?: true
|
id?: true
|
||||||
cwCatalogId?: true
|
cwCatalogId?: true
|
||||||
|
identifier?: true
|
||||||
name?: true
|
name?: true
|
||||||
description?: true
|
description?: true
|
||||||
customerDescription?: true
|
customerDescription?: true
|
||||||
internalNotes?: true
|
internalNotes?: true
|
||||||
|
category?: true
|
||||||
|
categoryCwId?: true
|
||||||
|
subcategory?: true
|
||||||
|
subcategoryCwId?: true
|
||||||
manufacturer?: true
|
manufacturer?: true
|
||||||
manufactureCwId?: true
|
manufactureCwId?: true
|
||||||
partNumber?: true
|
partNumber?: true
|
||||||
@@ -292,10 +330,15 @@ export type CatalogItemGroupByArgs<ExtArgs extends runtime.Types.Extensions.Inte
|
|||||||
export type CatalogItemGroupByOutputType = {
|
export type CatalogItemGroupByOutputType = {
|
||||||
id: string
|
id: string
|
||||||
cwCatalogId: number
|
cwCatalogId: number
|
||||||
|
identifier: string | null
|
||||||
name: string
|
name: string
|
||||||
description: string | null
|
description: string | null
|
||||||
customerDescription: string | null
|
customerDescription: string | null
|
||||||
internalNotes: string | null
|
internalNotes: string | null
|
||||||
|
category: string | null
|
||||||
|
categoryCwId: number | null
|
||||||
|
subcategory: string | null
|
||||||
|
subcategoryCwId: number | null
|
||||||
manufacturer: string | null
|
manufacturer: string | null
|
||||||
manufactureCwId: number | null
|
manufactureCwId: number | null
|
||||||
partNumber: string | null
|
partNumber: string | null
|
||||||
@@ -338,10 +381,15 @@ export type CatalogItemWhereInput = {
|
|||||||
NOT?: Prisma.CatalogItemWhereInput | Prisma.CatalogItemWhereInput[]
|
NOT?: Prisma.CatalogItemWhereInput | Prisma.CatalogItemWhereInput[]
|
||||||
id?: Prisma.StringFilter<"CatalogItem"> | string
|
id?: Prisma.StringFilter<"CatalogItem"> | string
|
||||||
cwCatalogId?: Prisma.IntFilter<"CatalogItem"> | number
|
cwCatalogId?: Prisma.IntFilter<"CatalogItem"> | number
|
||||||
|
identifier?: Prisma.StringNullableFilter<"CatalogItem"> | string | null
|
||||||
name?: Prisma.StringFilter<"CatalogItem"> | string
|
name?: Prisma.StringFilter<"CatalogItem"> | string
|
||||||
description?: Prisma.StringNullableFilter<"CatalogItem"> | string | null
|
description?: Prisma.StringNullableFilter<"CatalogItem"> | string | null
|
||||||
customerDescription?: Prisma.StringNullableFilter<"CatalogItem"> | string | null
|
customerDescription?: Prisma.StringNullableFilter<"CatalogItem"> | string | null
|
||||||
internalNotes?: Prisma.StringNullableFilter<"CatalogItem"> | string | null
|
internalNotes?: Prisma.StringNullableFilter<"CatalogItem"> | string | null
|
||||||
|
category?: Prisma.StringNullableFilter<"CatalogItem"> | string | null
|
||||||
|
categoryCwId?: Prisma.IntNullableFilter<"CatalogItem"> | number | null
|
||||||
|
subcategory?: Prisma.StringNullableFilter<"CatalogItem"> | string | null
|
||||||
|
subcategoryCwId?: Prisma.IntNullableFilter<"CatalogItem"> | number | null
|
||||||
manufacturer?: Prisma.StringNullableFilter<"CatalogItem"> | string | null
|
manufacturer?: Prisma.StringNullableFilter<"CatalogItem"> | string | null
|
||||||
manufactureCwId?: Prisma.IntNullableFilter<"CatalogItem"> | number | null
|
manufactureCwId?: Prisma.IntNullableFilter<"CatalogItem"> | number | null
|
||||||
partNumber?: Prisma.StringNullableFilter<"CatalogItem"> | string | null
|
partNumber?: Prisma.StringNullableFilter<"CatalogItem"> | string | null
|
||||||
@@ -363,10 +411,15 @@ export type CatalogItemWhereInput = {
|
|||||||
export type CatalogItemOrderByWithRelationInput = {
|
export type CatalogItemOrderByWithRelationInput = {
|
||||||
id?: Prisma.SortOrder
|
id?: Prisma.SortOrder
|
||||||
cwCatalogId?: Prisma.SortOrder
|
cwCatalogId?: Prisma.SortOrder
|
||||||
|
identifier?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||||
name?: Prisma.SortOrder
|
name?: Prisma.SortOrder
|
||||||
description?: Prisma.SortOrderInput | Prisma.SortOrder
|
description?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||||
customerDescription?: Prisma.SortOrderInput | Prisma.SortOrder
|
customerDescription?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||||
internalNotes?: Prisma.SortOrderInput | Prisma.SortOrder
|
internalNotes?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||||
|
category?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||||
|
categoryCwId?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||||
|
subcategory?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||||
|
subcategoryCwId?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||||
manufacturer?: Prisma.SortOrderInput | Prisma.SortOrder
|
manufacturer?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||||
manufactureCwId?: Prisma.SortOrderInput | Prisma.SortOrder
|
manufactureCwId?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||||
partNumber?: Prisma.SortOrderInput | Prisma.SortOrder
|
partNumber?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||||
@@ -388,6 +441,7 @@ export type CatalogItemOrderByWithRelationInput = {
|
|||||||
export type CatalogItemWhereUniqueInput = Prisma.AtLeast<{
|
export type CatalogItemWhereUniqueInput = Prisma.AtLeast<{
|
||||||
id?: string
|
id?: string
|
||||||
cwCatalogId?: number
|
cwCatalogId?: number
|
||||||
|
identifier?: string
|
||||||
AND?: Prisma.CatalogItemWhereInput | Prisma.CatalogItemWhereInput[]
|
AND?: Prisma.CatalogItemWhereInput | Prisma.CatalogItemWhereInput[]
|
||||||
OR?: Prisma.CatalogItemWhereInput[]
|
OR?: Prisma.CatalogItemWhereInput[]
|
||||||
NOT?: Prisma.CatalogItemWhereInput | Prisma.CatalogItemWhereInput[]
|
NOT?: Prisma.CatalogItemWhereInput | Prisma.CatalogItemWhereInput[]
|
||||||
@@ -395,6 +449,10 @@ export type CatalogItemWhereUniqueInput = Prisma.AtLeast<{
|
|||||||
description?: Prisma.StringNullableFilter<"CatalogItem"> | string | null
|
description?: Prisma.StringNullableFilter<"CatalogItem"> | string | null
|
||||||
customerDescription?: Prisma.StringNullableFilter<"CatalogItem"> | string | null
|
customerDescription?: Prisma.StringNullableFilter<"CatalogItem"> | string | null
|
||||||
internalNotes?: Prisma.StringNullableFilter<"CatalogItem"> | string | null
|
internalNotes?: Prisma.StringNullableFilter<"CatalogItem"> | string | null
|
||||||
|
category?: Prisma.StringNullableFilter<"CatalogItem"> | string | null
|
||||||
|
categoryCwId?: Prisma.IntNullableFilter<"CatalogItem"> | number | null
|
||||||
|
subcategory?: Prisma.StringNullableFilter<"CatalogItem"> | string | null
|
||||||
|
subcategoryCwId?: Prisma.IntNullableFilter<"CatalogItem"> | number | null
|
||||||
manufacturer?: Prisma.StringNullableFilter<"CatalogItem"> | string | null
|
manufacturer?: Prisma.StringNullableFilter<"CatalogItem"> | string | null
|
||||||
manufactureCwId?: Prisma.IntNullableFilter<"CatalogItem"> | number | null
|
manufactureCwId?: Prisma.IntNullableFilter<"CatalogItem"> | number | null
|
||||||
partNumber?: Prisma.StringNullableFilter<"CatalogItem"> | string | null
|
partNumber?: Prisma.StringNullableFilter<"CatalogItem"> | string | null
|
||||||
@@ -411,15 +469,20 @@ export type CatalogItemWhereUniqueInput = Prisma.AtLeast<{
|
|||||||
updatedAt?: Prisma.DateTimeFilter<"CatalogItem"> | Date | string
|
updatedAt?: Prisma.DateTimeFilter<"CatalogItem"> | Date | string
|
||||||
linkedItems?: Prisma.CatalogItemListRelationFilter
|
linkedItems?: Prisma.CatalogItemListRelationFilter
|
||||||
linkedTo?: Prisma.CatalogItemListRelationFilter
|
linkedTo?: Prisma.CatalogItemListRelationFilter
|
||||||
}, "id" | "cwCatalogId">
|
}, "id" | "cwCatalogId" | "identifier">
|
||||||
|
|
||||||
export type CatalogItemOrderByWithAggregationInput = {
|
export type CatalogItemOrderByWithAggregationInput = {
|
||||||
id?: Prisma.SortOrder
|
id?: Prisma.SortOrder
|
||||||
cwCatalogId?: Prisma.SortOrder
|
cwCatalogId?: Prisma.SortOrder
|
||||||
|
identifier?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||||
name?: Prisma.SortOrder
|
name?: Prisma.SortOrder
|
||||||
description?: Prisma.SortOrderInput | Prisma.SortOrder
|
description?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||||
customerDescription?: Prisma.SortOrderInput | Prisma.SortOrder
|
customerDescription?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||||
internalNotes?: Prisma.SortOrderInput | Prisma.SortOrder
|
internalNotes?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||||
|
category?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||||
|
categoryCwId?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||||
|
subcategory?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||||
|
subcategoryCwId?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||||
manufacturer?: Prisma.SortOrderInput | Prisma.SortOrder
|
manufacturer?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||||
manufactureCwId?: Prisma.SortOrderInput | Prisma.SortOrder
|
manufactureCwId?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||||
partNumber?: Prisma.SortOrderInput | Prisma.SortOrder
|
partNumber?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||||
@@ -447,10 +510,15 @@ export type CatalogItemScalarWhereWithAggregatesInput = {
|
|||||||
NOT?: Prisma.CatalogItemScalarWhereWithAggregatesInput | Prisma.CatalogItemScalarWhereWithAggregatesInput[]
|
NOT?: Prisma.CatalogItemScalarWhereWithAggregatesInput | Prisma.CatalogItemScalarWhereWithAggregatesInput[]
|
||||||
id?: Prisma.StringWithAggregatesFilter<"CatalogItem"> | string
|
id?: Prisma.StringWithAggregatesFilter<"CatalogItem"> | string
|
||||||
cwCatalogId?: Prisma.IntWithAggregatesFilter<"CatalogItem"> | number
|
cwCatalogId?: Prisma.IntWithAggregatesFilter<"CatalogItem"> | number
|
||||||
|
identifier?: Prisma.StringNullableWithAggregatesFilter<"CatalogItem"> | string | null
|
||||||
name?: Prisma.StringWithAggregatesFilter<"CatalogItem"> | string
|
name?: Prisma.StringWithAggregatesFilter<"CatalogItem"> | string
|
||||||
description?: Prisma.StringNullableWithAggregatesFilter<"CatalogItem"> | string | null
|
description?: Prisma.StringNullableWithAggregatesFilter<"CatalogItem"> | string | null
|
||||||
customerDescription?: Prisma.StringNullableWithAggregatesFilter<"CatalogItem"> | string | null
|
customerDescription?: Prisma.StringNullableWithAggregatesFilter<"CatalogItem"> | string | null
|
||||||
internalNotes?: Prisma.StringNullableWithAggregatesFilter<"CatalogItem"> | string | null
|
internalNotes?: Prisma.StringNullableWithAggregatesFilter<"CatalogItem"> | string | null
|
||||||
|
category?: Prisma.StringNullableWithAggregatesFilter<"CatalogItem"> | string | null
|
||||||
|
categoryCwId?: Prisma.IntNullableWithAggregatesFilter<"CatalogItem"> | number | null
|
||||||
|
subcategory?: Prisma.StringNullableWithAggregatesFilter<"CatalogItem"> | string | null
|
||||||
|
subcategoryCwId?: Prisma.IntNullableWithAggregatesFilter<"CatalogItem"> | number | null
|
||||||
manufacturer?: Prisma.StringNullableWithAggregatesFilter<"CatalogItem"> | string | null
|
manufacturer?: Prisma.StringNullableWithAggregatesFilter<"CatalogItem"> | string | null
|
||||||
manufactureCwId?: Prisma.IntNullableWithAggregatesFilter<"CatalogItem"> | number | null
|
manufactureCwId?: Prisma.IntNullableWithAggregatesFilter<"CatalogItem"> | number | null
|
||||||
partNumber?: Prisma.StringNullableWithAggregatesFilter<"CatalogItem"> | string | null
|
partNumber?: Prisma.StringNullableWithAggregatesFilter<"CatalogItem"> | string | null
|
||||||
@@ -470,10 +538,15 @@ export type CatalogItemScalarWhereWithAggregatesInput = {
|
|||||||
export type CatalogItemCreateInput = {
|
export type CatalogItemCreateInput = {
|
||||||
id?: string
|
id?: string
|
||||||
cwCatalogId: number
|
cwCatalogId: number
|
||||||
|
identifier?: string | null
|
||||||
name: string
|
name: string
|
||||||
description?: string | null
|
description?: string | null
|
||||||
customerDescription?: string | null
|
customerDescription?: string | null
|
||||||
internalNotes?: string | null
|
internalNotes?: string | null
|
||||||
|
category?: string | null
|
||||||
|
categoryCwId?: number | null
|
||||||
|
subcategory?: string | null
|
||||||
|
subcategoryCwId?: number | null
|
||||||
manufacturer?: string | null
|
manufacturer?: string | null
|
||||||
manufactureCwId?: number | null
|
manufactureCwId?: number | null
|
||||||
partNumber?: string | null
|
partNumber?: string | null
|
||||||
@@ -495,10 +568,15 @@ export type CatalogItemCreateInput = {
|
|||||||
export type CatalogItemUncheckedCreateInput = {
|
export type CatalogItemUncheckedCreateInput = {
|
||||||
id?: string
|
id?: string
|
||||||
cwCatalogId: number
|
cwCatalogId: number
|
||||||
|
identifier?: string | null
|
||||||
name: string
|
name: string
|
||||||
description?: string | null
|
description?: string | null
|
||||||
customerDescription?: string | null
|
customerDescription?: string | null
|
||||||
internalNotes?: string | null
|
internalNotes?: string | null
|
||||||
|
category?: string | null
|
||||||
|
categoryCwId?: number | null
|
||||||
|
subcategory?: string | null
|
||||||
|
subcategoryCwId?: number | null
|
||||||
manufacturer?: string | null
|
manufacturer?: string | null
|
||||||
manufactureCwId?: number | null
|
manufactureCwId?: number | null
|
||||||
partNumber?: string | null
|
partNumber?: string | null
|
||||||
@@ -520,10 +598,15 @@ export type CatalogItemUncheckedCreateInput = {
|
|||||||
export type CatalogItemUpdateInput = {
|
export type CatalogItemUpdateInput = {
|
||||||
id?: Prisma.StringFieldUpdateOperationsInput | string
|
id?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
cwCatalogId?: Prisma.IntFieldUpdateOperationsInput | number
|
cwCatalogId?: Prisma.IntFieldUpdateOperationsInput | number
|
||||||
|
identifier?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
customerDescription?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
customerDescription?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
internalNotes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
internalNotes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
category?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
categoryCwId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
|
subcategory?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
subcategoryCwId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
manufacturer?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
manufacturer?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
manufactureCwId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
manufactureCwId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
partNumber?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
partNumber?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
@@ -545,10 +628,15 @@ export type CatalogItemUpdateInput = {
|
|||||||
export type CatalogItemUncheckedUpdateInput = {
|
export type CatalogItemUncheckedUpdateInput = {
|
||||||
id?: Prisma.StringFieldUpdateOperationsInput | string
|
id?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
cwCatalogId?: Prisma.IntFieldUpdateOperationsInput | number
|
cwCatalogId?: Prisma.IntFieldUpdateOperationsInput | number
|
||||||
|
identifier?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
customerDescription?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
customerDescription?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
internalNotes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
internalNotes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
category?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
categoryCwId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
|
subcategory?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
subcategoryCwId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
manufacturer?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
manufacturer?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
manufactureCwId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
manufactureCwId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
partNumber?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
partNumber?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
@@ -570,10 +658,15 @@ export type CatalogItemUncheckedUpdateInput = {
|
|||||||
export type CatalogItemCreateManyInput = {
|
export type CatalogItemCreateManyInput = {
|
||||||
id?: string
|
id?: string
|
||||||
cwCatalogId: number
|
cwCatalogId: number
|
||||||
|
identifier?: string | null
|
||||||
name: string
|
name: string
|
||||||
description?: string | null
|
description?: string | null
|
||||||
customerDescription?: string | null
|
customerDescription?: string | null
|
||||||
internalNotes?: string | null
|
internalNotes?: string | null
|
||||||
|
category?: string | null
|
||||||
|
categoryCwId?: number | null
|
||||||
|
subcategory?: string | null
|
||||||
|
subcategoryCwId?: number | null
|
||||||
manufacturer?: string | null
|
manufacturer?: string | null
|
||||||
manufactureCwId?: number | null
|
manufactureCwId?: number | null
|
||||||
partNumber?: string | null
|
partNumber?: string | null
|
||||||
@@ -593,10 +686,15 @@ export type CatalogItemCreateManyInput = {
|
|||||||
export type CatalogItemUpdateManyMutationInput = {
|
export type CatalogItemUpdateManyMutationInput = {
|
||||||
id?: Prisma.StringFieldUpdateOperationsInput | string
|
id?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
cwCatalogId?: Prisma.IntFieldUpdateOperationsInput | number
|
cwCatalogId?: Prisma.IntFieldUpdateOperationsInput | number
|
||||||
|
identifier?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
customerDescription?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
customerDescription?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
internalNotes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
internalNotes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
category?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
categoryCwId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
|
subcategory?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
subcategoryCwId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
manufacturer?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
manufacturer?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
manufactureCwId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
manufactureCwId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
partNumber?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
partNumber?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
@@ -616,10 +714,15 @@ export type CatalogItemUpdateManyMutationInput = {
|
|||||||
export type CatalogItemUncheckedUpdateManyInput = {
|
export type CatalogItemUncheckedUpdateManyInput = {
|
||||||
id?: Prisma.StringFieldUpdateOperationsInput | string
|
id?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
cwCatalogId?: Prisma.IntFieldUpdateOperationsInput | number
|
cwCatalogId?: Prisma.IntFieldUpdateOperationsInput | number
|
||||||
|
identifier?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
customerDescription?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
customerDescription?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
internalNotes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
internalNotes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
category?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
categoryCwId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
|
subcategory?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
subcategoryCwId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
manufacturer?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
manufacturer?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
manufactureCwId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
manufactureCwId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
partNumber?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
partNumber?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
@@ -649,10 +752,15 @@ export type CatalogItemOrderByRelationAggregateInput = {
|
|||||||
export type CatalogItemCountOrderByAggregateInput = {
|
export type CatalogItemCountOrderByAggregateInput = {
|
||||||
id?: Prisma.SortOrder
|
id?: Prisma.SortOrder
|
||||||
cwCatalogId?: Prisma.SortOrder
|
cwCatalogId?: Prisma.SortOrder
|
||||||
|
identifier?: Prisma.SortOrder
|
||||||
name?: Prisma.SortOrder
|
name?: Prisma.SortOrder
|
||||||
description?: Prisma.SortOrder
|
description?: Prisma.SortOrder
|
||||||
customerDescription?: Prisma.SortOrder
|
customerDescription?: Prisma.SortOrder
|
||||||
internalNotes?: Prisma.SortOrder
|
internalNotes?: Prisma.SortOrder
|
||||||
|
category?: Prisma.SortOrder
|
||||||
|
categoryCwId?: Prisma.SortOrder
|
||||||
|
subcategory?: Prisma.SortOrder
|
||||||
|
subcategoryCwId?: Prisma.SortOrder
|
||||||
manufacturer?: Prisma.SortOrder
|
manufacturer?: Prisma.SortOrder
|
||||||
manufactureCwId?: Prisma.SortOrder
|
manufactureCwId?: Prisma.SortOrder
|
||||||
partNumber?: Prisma.SortOrder
|
partNumber?: Prisma.SortOrder
|
||||||
@@ -671,6 +779,8 @@ export type CatalogItemCountOrderByAggregateInput = {
|
|||||||
|
|
||||||
export type CatalogItemAvgOrderByAggregateInput = {
|
export type CatalogItemAvgOrderByAggregateInput = {
|
||||||
cwCatalogId?: Prisma.SortOrder
|
cwCatalogId?: Prisma.SortOrder
|
||||||
|
categoryCwId?: Prisma.SortOrder
|
||||||
|
subcategoryCwId?: Prisma.SortOrder
|
||||||
manufactureCwId?: Prisma.SortOrder
|
manufactureCwId?: Prisma.SortOrder
|
||||||
vendorCwId?: Prisma.SortOrder
|
vendorCwId?: Prisma.SortOrder
|
||||||
price?: Prisma.SortOrder
|
price?: Prisma.SortOrder
|
||||||
@@ -681,10 +791,15 @@ export type CatalogItemAvgOrderByAggregateInput = {
|
|||||||
export type CatalogItemMaxOrderByAggregateInput = {
|
export type CatalogItemMaxOrderByAggregateInput = {
|
||||||
id?: Prisma.SortOrder
|
id?: Prisma.SortOrder
|
||||||
cwCatalogId?: Prisma.SortOrder
|
cwCatalogId?: Prisma.SortOrder
|
||||||
|
identifier?: Prisma.SortOrder
|
||||||
name?: Prisma.SortOrder
|
name?: Prisma.SortOrder
|
||||||
description?: Prisma.SortOrder
|
description?: Prisma.SortOrder
|
||||||
customerDescription?: Prisma.SortOrder
|
customerDescription?: Prisma.SortOrder
|
||||||
internalNotes?: Prisma.SortOrder
|
internalNotes?: Prisma.SortOrder
|
||||||
|
category?: Prisma.SortOrder
|
||||||
|
categoryCwId?: Prisma.SortOrder
|
||||||
|
subcategory?: Prisma.SortOrder
|
||||||
|
subcategoryCwId?: Prisma.SortOrder
|
||||||
manufacturer?: Prisma.SortOrder
|
manufacturer?: Prisma.SortOrder
|
||||||
manufactureCwId?: Prisma.SortOrder
|
manufactureCwId?: Prisma.SortOrder
|
||||||
partNumber?: Prisma.SortOrder
|
partNumber?: Prisma.SortOrder
|
||||||
@@ -704,10 +819,15 @@ export type CatalogItemMaxOrderByAggregateInput = {
|
|||||||
export type CatalogItemMinOrderByAggregateInput = {
|
export type CatalogItemMinOrderByAggregateInput = {
|
||||||
id?: Prisma.SortOrder
|
id?: Prisma.SortOrder
|
||||||
cwCatalogId?: Prisma.SortOrder
|
cwCatalogId?: Prisma.SortOrder
|
||||||
|
identifier?: Prisma.SortOrder
|
||||||
name?: Prisma.SortOrder
|
name?: Prisma.SortOrder
|
||||||
description?: Prisma.SortOrder
|
description?: Prisma.SortOrder
|
||||||
customerDescription?: Prisma.SortOrder
|
customerDescription?: Prisma.SortOrder
|
||||||
internalNotes?: Prisma.SortOrder
|
internalNotes?: Prisma.SortOrder
|
||||||
|
category?: Prisma.SortOrder
|
||||||
|
categoryCwId?: Prisma.SortOrder
|
||||||
|
subcategory?: Prisma.SortOrder
|
||||||
|
subcategoryCwId?: Prisma.SortOrder
|
||||||
manufacturer?: Prisma.SortOrder
|
manufacturer?: Prisma.SortOrder
|
||||||
manufactureCwId?: Prisma.SortOrder
|
manufactureCwId?: Prisma.SortOrder
|
||||||
partNumber?: Prisma.SortOrder
|
partNumber?: Prisma.SortOrder
|
||||||
@@ -726,6 +846,8 @@ export type CatalogItemMinOrderByAggregateInput = {
|
|||||||
|
|
||||||
export type CatalogItemSumOrderByAggregateInput = {
|
export type CatalogItemSumOrderByAggregateInput = {
|
||||||
cwCatalogId?: Prisma.SortOrder
|
cwCatalogId?: Prisma.SortOrder
|
||||||
|
categoryCwId?: Prisma.SortOrder
|
||||||
|
subcategoryCwId?: Prisma.SortOrder
|
||||||
manufactureCwId?: Prisma.SortOrder
|
manufactureCwId?: Prisma.SortOrder
|
||||||
vendorCwId?: Prisma.SortOrder
|
vendorCwId?: Prisma.SortOrder
|
||||||
price?: Prisma.SortOrder
|
price?: Prisma.SortOrder
|
||||||
@@ -828,10 +950,15 @@ export type CatalogItemUncheckedUpdateManyWithoutLinkedItemsNestedInput = {
|
|||||||
export type CatalogItemCreateWithoutLinkedToInput = {
|
export type CatalogItemCreateWithoutLinkedToInput = {
|
||||||
id?: string
|
id?: string
|
||||||
cwCatalogId: number
|
cwCatalogId: number
|
||||||
|
identifier?: string | null
|
||||||
name: string
|
name: string
|
||||||
description?: string | null
|
description?: string | null
|
||||||
customerDescription?: string | null
|
customerDescription?: string | null
|
||||||
internalNotes?: string | null
|
internalNotes?: string | null
|
||||||
|
category?: string | null
|
||||||
|
categoryCwId?: number | null
|
||||||
|
subcategory?: string | null
|
||||||
|
subcategoryCwId?: number | null
|
||||||
manufacturer?: string | null
|
manufacturer?: string | null
|
||||||
manufactureCwId?: number | null
|
manufactureCwId?: number | null
|
||||||
partNumber?: string | null
|
partNumber?: string | null
|
||||||
@@ -852,10 +979,15 @@ export type CatalogItemCreateWithoutLinkedToInput = {
|
|||||||
export type CatalogItemUncheckedCreateWithoutLinkedToInput = {
|
export type CatalogItemUncheckedCreateWithoutLinkedToInput = {
|
||||||
id?: string
|
id?: string
|
||||||
cwCatalogId: number
|
cwCatalogId: number
|
||||||
|
identifier?: string | null
|
||||||
name: string
|
name: string
|
||||||
description?: string | null
|
description?: string | null
|
||||||
customerDescription?: string | null
|
customerDescription?: string | null
|
||||||
internalNotes?: string | null
|
internalNotes?: string | null
|
||||||
|
category?: string | null
|
||||||
|
categoryCwId?: number | null
|
||||||
|
subcategory?: string | null
|
||||||
|
subcategoryCwId?: number | null
|
||||||
manufacturer?: string | null
|
manufacturer?: string | null
|
||||||
manufactureCwId?: number | null
|
manufactureCwId?: number | null
|
||||||
partNumber?: string | null
|
partNumber?: string | null
|
||||||
@@ -881,10 +1013,15 @@ export type CatalogItemCreateOrConnectWithoutLinkedToInput = {
|
|||||||
export type CatalogItemCreateWithoutLinkedItemsInput = {
|
export type CatalogItemCreateWithoutLinkedItemsInput = {
|
||||||
id?: string
|
id?: string
|
||||||
cwCatalogId: number
|
cwCatalogId: number
|
||||||
|
identifier?: string | null
|
||||||
name: string
|
name: string
|
||||||
description?: string | null
|
description?: string | null
|
||||||
customerDescription?: string | null
|
customerDescription?: string | null
|
||||||
internalNotes?: string | null
|
internalNotes?: string | null
|
||||||
|
category?: string | null
|
||||||
|
categoryCwId?: number | null
|
||||||
|
subcategory?: string | null
|
||||||
|
subcategoryCwId?: number | null
|
||||||
manufacturer?: string | null
|
manufacturer?: string | null
|
||||||
manufactureCwId?: number | null
|
manufactureCwId?: number | null
|
||||||
partNumber?: string | null
|
partNumber?: string | null
|
||||||
@@ -905,10 +1042,15 @@ export type CatalogItemCreateWithoutLinkedItemsInput = {
|
|||||||
export type CatalogItemUncheckedCreateWithoutLinkedItemsInput = {
|
export type CatalogItemUncheckedCreateWithoutLinkedItemsInput = {
|
||||||
id?: string
|
id?: string
|
||||||
cwCatalogId: number
|
cwCatalogId: number
|
||||||
|
identifier?: string | null
|
||||||
name: string
|
name: string
|
||||||
description?: string | null
|
description?: string | null
|
||||||
customerDescription?: string | null
|
customerDescription?: string | null
|
||||||
internalNotes?: string | null
|
internalNotes?: string | null
|
||||||
|
category?: string | null
|
||||||
|
categoryCwId?: number | null
|
||||||
|
subcategory?: string | null
|
||||||
|
subcategoryCwId?: number | null
|
||||||
manufacturer?: string | null
|
manufacturer?: string | null
|
||||||
manufactureCwId?: number | null
|
manufactureCwId?: number | null
|
||||||
partNumber?: string | null
|
partNumber?: string | null
|
||||||
@@ -953,10 +1095,15 @@ export type CatalogItemScalarWhereInput = {
|
|||||||
NOT?: Prisma.CatalogItemScalarWhereInput | Prisma.CatalogItemScalarWhereInput[]
|
NOT?: Prisma.CatalogItemScalarWhereInput | Prisma.CatalogItemScalarWhereInput[]
|
||||||
id?: Prisma.StringFilter<"CatalogItem"> | string
|
id?: Prisma.StringFilter<"CatalogItem"> | string
|
||||||
cwCatalogId?: Prisma.IntFilter<"CatalogItem"> | number
|
cwCatalogId?: Prisma.IntFilter<"CatalogItem"> | number
|
||||||
|
identifier?: Prisma.StringNullableFilter<"CatalogItem"> | string | null
|
||||||
name?: Prisma.StringFilter<"CatalogItem"> | string
|
name?: Prisma.StringFilter<"CatalogItem"> | string
|
||||||
description?: Prisma.StringNullableFilter<"CatalogItem"> | string | null
|
description?: Prisma.StringNullableFilter<"CatalogItem"> | string | null
|
||||||
customerDescription?: Prisma.StringNullableFilter<"CatalogItem"> | string | null
|
customerDescription?: Prisma.StringNullableFilter<"CatalogItem"> | string | null
|
||||||
internalNotes?: Prisma.StringNullableFilter<"CatalogItem"> | string | null
|
internalNotes?: Prisma.StringNullableFilter<"CatalogItem"> | string | null
|
||||||
|
category?: Prisma.StringNullableFilter<"CatalogItem"> | string | null
|
||||||
|
categoryCwId?: Prisma.IntNullableFilter<"CatalogItem"> | number | null
|
||||||
|
subcategory?: Prisma.StringNullableFilter<"CatalogItem"> | string | null
|
||||||
|
subcategoryCwId?: Prisma.IntNullableFilter<"CatalogItem"> | number | null
|
||||||
manufacturer?: Prisma.StringNullableFilter<"CatalogItem"> | string | null
|
manufacturer?: Prisma.StringNullableFilter<"CatalogItem"> | string | null
|
||||||
manufactureCwId?: Prisma.IntNullableFilter<"CatalogItem"> | number | null
|
manufactureCwId?: Prisma.IntNullableFilter<"CatalogItem"> | number | null
|
||||||
partNumber?: Prisma.StringNullableFilter<"CatalogItem"> | string | null
|
partNumber?: Prisma.StringNullableFilter<"CatalogItem"> | string | null
|
||||||
@@ -992,10 +1139,15 @@ export type CatalogItemUpdateManyWithWhereWithoutLinkedItemsInput = {
|
|||||||
export type CatalogItemUpdateWithoutLinkedToInput = {
|
export type CatalogItemUpdateWithoutLinkedToInput = {
|
||||||
id?: Prisma.StringFieldUpdateOperationsInput | string
|
id?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
cwCatalogId?: Prisma.IntFieldUpdateOperationsInput | number
|
cwCatalogId?: Prisma.IntFieldUpdateOperationsInput | number
|
||||||
|
identifier?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
customerDescription?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
customerDescription?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
internalNotes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
internalNotes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
category?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
categoryCwId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
|
subcategory?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
subcategoryCwId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
manufacturer?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
manufacturer?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
manufactureCwId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
manufactureCwId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
partNumber?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
partNumber?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
@@ -1016,10 +1168,15 @@ export type CatalogItemUpdateWithoutLinkedToInput = {
|
|||||||
export type CatalogItemUncheckedUpdateWithoutLinkedToInput = {
|
export type CatalogItemUncheckedUpdateWithoutLinkedToInput = {
|
||||||
id?: Prisma.StringFieldUpdateOperationsInput | string
|
id?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
cwCatalogId?: Prisma.IntFieldUpdateOperationsInput | number
|
cwCatalogId?: Prisma.IntFieldUpdateOperationsInput | number
|
||||||
|
identifier?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
customerDescription?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
customerDescription?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
internalNotes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
internalNotes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
category?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
categoryCwId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
|
subcategory?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
subcategoryCwId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
manufacturer?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
manufacturer?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
manufactureCwId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
manufactureCwId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
partNumber?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
partNumber?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
@@ -1040,10 +1197,15 @@ export type CatalogItemUncheckedUpdateWithoutLinkedToInput = {
|
|||||||
export type CatalogItemUncheckedUpdateManyWithoutLinkedToInput = {
|
export type CatalogItemUncheckedUpdateManyWithoutLinkedToInput = {
|
||||||
id?: Prisma.StringFieldUpdateOperationsInput | string
|
id?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
cwCatalogId?: Prisma.IntFieldUpdateOperationsInput | number
|
cwCatalogId?: Prisma.IntFieldUpdateOperationsInput | number
|
||||||
|
identifier?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
customerDescription?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
customerDescription?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
internalNotes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
internalNotes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
category?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
categoryCwId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
|
subcategory?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
subcategoryCwId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
manufacturer?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
manufacturer?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
manufactureCwId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
manufactureCwId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
partNumber?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
partNumber?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
@@ -1063,10 +1225,15 @@ export type CatalogItemUncheckedUpdateManyWithoutLinkedToInput = {
|
|||||||
export type CatalogItemUpdateWithoutLinkedItemsInput = {
|
export type CatalogItemUpdateWithoutLinkedItemsInput = {
|
||||||
id?: Prisma.StringFieldUpdateOperationsInput | string
|
id?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
cwCatalogId?: Prisma.IntFieldUpdateOperationsInput | number
|
cwCatalogId?: Prisma.IntFieldUpdateOperationsInput | number
|
||||||
|
identifier?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
customerDescription?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
customerDescription?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
internalNotes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
internalNotes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
category?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
categoryCwId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
|
subcategory?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
subcategoryCwId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
manufacturer?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
manufacturer?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
manufactureCwId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
manufactureCwId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
partNumber?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
partNumber?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
@@ -1087,10 +1254,15 @@ export type CatalogItemUpdateWithoutLinkedItemsInput = {
|
|||||||
export type CatalogItemUncheckedUpdateWithoutLinkedItemsInput = {
|
export type CatalogItemUncheckedUpdateWithoutLinkedItemsInput = {
|
||||||
id?: Prisma.StringFieldUpdateOperationsInput | string
|
id?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
cwCatalogId?: Prisma.IntFieldUpdateOperationsInput | number
|
cwCatalogId?: Prisma.IntFieldUpdateOperationsInput | number
|
||||||
|
identifier?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
customerDescription?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
customerDescription?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
internalNotes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
internalNotes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
category?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
categoryCwId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
|
subcategory?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
subcategoryCwId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
manufacturer?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
manufacturer?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
manufactureCwId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
manufactureCwId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
partNumber?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
partNumber?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
@@ -1111,10 +1283,15 @@ export type CatalogItemUncheckedUpdateWithoutLinkedItemsInput = {
|
|||||||
export type CatalogItemUncheckedUpdateManyWithoutLinkedItemsInput = {
|
export type CatalogItemUncheckedUpdateManyWithoutLinkedItemsInput = {
|
||||||
id?: Prisma.StringFieldUpdateOperationsInput | string
|
id?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
cwCatalogId?: Prisma.IntFieldUpdateOperationsInput | number
|
cwCatalogId?: Prisma.IntFieldUpdateOperationsInput | number
|
||||||
|
identifier?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
customerDescription?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
customerDescription?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
internalNotes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
internalNotes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
category?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
categoryCwId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
|
subcategory?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
subcategoryCwId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
manufacturer?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
manufacturer?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
manufactureCwId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
manufactureCwId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
partNumber?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
partNumber?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
@@ -1174,10 +1351,15 @@ export type CatalogItemCountOutputTypeCountLinkedToArgs<ExtArgs extends runtime.
|
|||||||
export type CatalogItemSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
|
export type CatalogItemSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
|
||||||
id?: boolean
|
id?: boolean
|
||||||
cwCatalogId?: boolean
|
cwCatalogId?: boolean
|
||||||
|
identifier?: boolean
|
||||||
name?: boolean
|
name?: boolean
|
||||||
description?: boolean
|
description?: boolean
|
||||||
customerDescription?: boolean
|
customerDescription?: boolean
|
||||||
internalNotes?: boolean
|
internalNotes?: boolean
|
||||||
|
category?: boolean
|
||||||
|
categoryCwId?: boolean
|
||||||
|
subcategory?: boolean
|
||||||
|
subcategoryCwId?: boolean
|
||||||
manufacturer?: boolean
|
manufacturer?: boolean
|
||||||
manufactureCwId?: boolean
|
manufactureCwId?: boolean
|
||||||
partNumber?: boolean
|
partNumber?: boolean
|
||||||
@@ -1200,10 +1382,15 @@ export type CatalogItemSelect<ExtArgs extends runtime.Types.Extensions.InternalA
|
|||||||
export type CatalogItemSelectCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
|
export type CatalogItemSelectCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
|
||||||
id?: boolean
|
id?: boolean
|
||||||
cwCatalogId?: boolean
|
cwCatalogId?: boolean
|
||||||
|
identifier?: boolean
|
||||||
name?: boolean
|
name?: boolean
|
||||||
description?: boolean
|
description?: boolean
|
||||||
customerDescription?: boolean
|
customerDescription?: boolean
|
||||||
internalNotes?: boolean
|
internalNotes?: boolean
|
||||||
|
category?: boolean
|
||||||
|
categoryCwId?: boolean
|
||||||
|
subcategory?: boolean
|
||||||
|
subcategoryCwId?: boolean
|
||||||
manufacturer?: boolean
|
manufacturer?: boolean
|
||||||
manufactureCwId?: boolean
|
manufactureCwId?: boolean
|
||||||
partNumber?: boolean
|
partNumber?: boolean
|
||||||
@@ -1223,10 +1410,15 @@ export type CatalogItemSelectCreateManyAndReturn<ExtArgs extends runtime.Types.E
|
|||||||
export type CatalogItemSelectUpdateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
|
export type CatalogItemSelectUpdateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
|
||||||
id?: boolean
|
id?: boolean
|
||||||
cwCatalogId?: boolean
|
cwCatalogId?: boolean
|
||||||
|
identifier?: boolean
|
||||||
name?: boolean
|
name?: boolean
|
||||||
description?: boolean
|
description?: boolean
|
||||||
customerDescription?: boolean
|
customerDescription?: boolean
|
||||||
internalNotes?: boolean
|
internalNotes?: boolean
|
||||||
|
category?: boolean
|
||||||
|
categoryCwId?: boolean
|
||||||
|
subcategory?: boolean
|
||||||
|
subcategoryCwId?: boolean
|
||||||
manufacturer?: boolean
|
manufacturer?: boolean
|
||||||
manufactureCwId?: boolean
|
manufactureCwId?: boolean
|
||||||
partNumber?: boolean
|
partNumber?: boolean
|
||||||
@@ -1246,10 +1438,15 @@ export type CatalogItemSelectUpdateManyAndReturn<ExtArgs extends runtime.Types.E
|
|||||||
export type CatalogItemSelectScalar = {
|
export type CatalogItemSelectScalar = {
|
||||||
id?: boolean
|
id?: boolean
|
||||||
cwCatalogId?: boolean
|
cwCatalogId?: boolean
|
||||||
|
identifier?: boolean
|
||||||
name?: boolean
|
name?: boolean
|
||||||
description?: boolean
|
description?: boolean
|
||||||
customerDescription?: boolean
|
customerDescription?: boolean
|
||||||
internalNotes?: boolean
|
internalNotes?: boolean
|
||||||
|
category?: boolean
|
||||||
|
categoryCwId?: boolean
|
||||||
|
subcategory?: boolean
|
||||||
|
subcategoryCwId?: boolean
|
||||||
manufacturer?: boolean
|
manufacturer?: boolean
|
||||||
manufactureCwId?: boolean
|
manufactureCwId?: boolean
|
||||||
partNumber?: boolean
|
partNumber?: boolean
|
||||||
@@ -1266,7 +1463,7 @@ export type CatalogItemSelectScalar = {
|
|||||||
updatedAt?: boolean
|
updatedAt?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CatalogItemOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "cwCatalogId" | "name" | "description" | "customerDescription" | "internalNotes" | "manufacturer" | "manufactureCwId" | "partNumber" | "vendorName" | "vendorSku" | "vendorCwId" | "price" | "cost" | "inactive" | "salesTaxable" | "onHand" | "cwLastUpdated" | "createdAt" | "updatedAt", ExtArgs["result"]["catalogItem"]>
|
export type CatalogItemOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "cwCatalogId" | "identifier" | "name" | "description" | "customerDescription" | "internalNotes" | "category" | "categoryCwId" | "subcategory" | "subcategoryCwId" | "manufacturer" | "manufactureCwId" | "partNumber" | "vendorName" | "vendorSku" | "vendorCwId" | "price" | "cost" | "inactive" | "salesTaxable" | "onHand" | "cwLastUpdated" | "createdAt" | "updatedAt", ExtArgs["result"]["catalogItem"]>
|
||||||
export type CatalogItemInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
export type CatalogItemInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||||
linkedItems?: boolean | Prisma.CatalogItem$linkedItemsArgs<ExtArgs>
|
linkedItems?: boolean | Prisma.CatalogItem$linkedItemsArgs<ExtArgs>
|
||||||
linkedTo?: boolean | Prisma.CatalogItem$linkedToArgs<ExtArgs>
|
linkedTo?: boolean | Prisma.CatalogItem$linkedToArgs<ExtArgs>
|
||||||
@@ -1284,10 +1481,15 @@ export type $CatalogItemPayload<ExtArgs extends runtime.Types.Extensions.Interna
|
|||||||
scalars: runtime.Types.Extensions.GetPayloadResult<{
|
scalars: runtime.Types.Extensions.GetPayloadResult<{
|
||||||
id: string
|
id: string
|
||||||
cwCatalogId: number
|
cwCatalogId: number
|
||||||
|
identifier: string | null
|
||||||
name: string
|
name: string
|
||||||
description: string | null
|
description: string | null
|
||||||
customerDescription: string | null
|
customerDescription: string | null
|
||||||
internalNotes: string | null
|
internalNotes: string | null
|
||||||
|
category: string | null
|
||||||
|
categoryCwId: number | null
|
||||||
|
subcategory: string | null
|
||||||
|
subcategoryCwId: number | null
|
||||||
manufacturer: string | null
|
manufacturer: string | null
|
||||||
manufactureCwId: number | null
|
manufactureCwId: number | null
|
||||||
partNumber: string | null
|
partNumber: string | null
|
||||||
@@ -1729,10 +1931,15 @@ export interface Prisma__CatalogItemClient<T, Null = never, ExtArgs extends runt
|
|||||||
export interface CatalogItemFieldRefs {
|
export interface CatalogItemFieldRefs {
|
||||||
readonly id: Prisma.FieldRef<"CatalogItem", 'String'>
|
readonly id: Prisma.FieldRef<"CatalogItem", 'String'>
|
||||||
readonly cwCatalogId: Prisma.FieldRef<"CatalogItem", 'Int'>
|
readonly cwCatalogId: Prisma.FieldRef<"CatalogItem", 'Int'>
|
||||||
|
readonly identifier: Prisma.FieldRef<"CatalogItem", 'String'>
|
||||||
readonly name: Prisma.FieldRef<"CatalogItem", 'String'>
|
readonly name: Prisma.FieldRef<"CatalogItem", 'String'>
|
||||||
readonly description: Prisma.FieldRef<"CatalogItem", 'String'>
|
readonly description: Prisma.FieldRef<"CatalogItem", 'String'>
|
||||||
readonly customerDescription: Prisma.FieldRef<"CatalogItem", 'String'>
|
readonly customerDescription: Prisma.FieldRef<"CatalogItem", 'String'>
|
||||||
readonly internalNotes: Prisma.FieldRef<"CatalogItem", 'String'>
|
readonly internalNotes: Prisma.FieldRef<"CatalogItem", 'String'>
|
||||||
|
readonly category: Prisma.FieldRef<"CatalogItem", 'String'>
|
||||||
|
readonly categoryCwId: Prisma.FieldRef<"CatalogItem", 'Int'>
|
||||||
|
readonly subcategory: Prisma.FieldRef<"CatalogItem", 'String'>
|
||||||
|
readonly subcategoryCwId: Prisma.FieldRef<"CatalogItem", 'Int'>
|
||||||
readonly manufacturer: Prisma.FieldRef<"CatalogItem", 'String'>
|
readonly manufacturer: Prisma.FieldRef<"CatalogItem", 'String'>
|
||||||
readonly manufactureCwId: Prisma.FieldRef<"CatalogItem", 'Int'>
|
readonly manufactureCwId: Prisma.FieldRef<"CatalogItem", 'Int'>
|
||||||
readonly partNumber: Prisma.FieldRef<"CatalogItem", 'String'>
|
readonly partNumber: Prisma.FieldRef<"CatalogItem", 'String'>
|
||||||
|
|||||||
@@ -226,6 +226,7 @@ export type CompanyWhereInput = {
|
|||||||
updatedAt?: Prisma.DateTimeFilter<"Company"> | Date | string
|
updatedAt?: Prisma.DateTimeFilter<"Company"> | Date | string
|
||||||
credentials?: Prisma.CredentialListRelationFilter
|
credentials?: Prisma.CredentialListRelationFilter
|
||||||
unifiSites?: Prisma.UnifiSiteListRelationFilter
|
unifiSites?: Prisma.UnifiSiteListRelationFilter
|
||||||
|
opportunities?: Prisma.OpportunityListRelationFilter
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CompanyOrderByWithRelationInput = {
|
export type CompanyOrderByWithRelationInput = {
|
||||||
@@ -237,6 +238,7 @@ export type CompanyOrderByWithRelationInput = {
|
|||||||
updatedAt?: Prisma.SortOrder
|
updatedAt?: Prisma.SortOrder
|
||||||
credentials?: Prisma.CredentialOrderByRelationAggregateInput
|
credentials?: Prisma.CredentialOrderByRelationAggregateInput
|
||||||
unifiSites?: Prisma.UnifiSiteOrderByRelationAggregateInput
|
unifiSites?: Prisma.UnifiSiteOrderByRelationAggregateInput
|
||||||
|
opportunities?: Prisma.OpportunityOrderByRelationAggregateInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CompanyWhereUniqueInput = Prisma.AtLeast<{
|
export type CompanyWhereUniqueInput = Prisma.AtLeast<{
|
||||||
@@ -251,6 +253,7 @@ export type CompanyWhereUniqueInput = Prisma.AtLeast<{
|
|||||||
updatedAt?: Prisma.DateTimeFilter<"Company"> | Date | string
|
updatedAt?: Prisma.DateTimeFilter<"Company"> | Date | string
|
||||||
credentials?: Prisma.CredentialListRelationFilter
|
credentials?: Prisma.CredentialListRelationFilter
|
||||||
unifiSites?: Prisma.UnifiSiteListRelationFilter
|
unifiSites?: Prisma.UnifiSiteListRelationFilter
|
||||||
|
opportunities?: Prisma.OpportunityListRelationFilter
|
||||||
}, "id" | "cw_CompanyId" | "cw_Identifier">
|
}, "id" | "cw_CompanyId" | "cw_Identifier">
|
||||||
|
|
||||||
export type CompanyOrderByWithAggregationInput = {
|
export type CompanyOrderByWithAggregationInput = {
|
||||||
@@ -288,6 +291,7 @@ export type CompanyCreateInput = {
|
|||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
credentials?: Prisma.CredentialCreateNestedManyWithoutCompanyInput
|
credentials?: Prisma.CredentialCreateNestedManyWithoutCompanyInput
|
||||||
unifiSites?: Prisma.UnifiSiteCreateNestedManyWithoutCompanyInput
|
unifiSites?: Prisma.UnifiSiteCreateNestedManyWithoutCompanyInput
|
||||||
|
opportunities?: Prisma.OpportunityCreateNestedManyWithoutCompanyInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CompanyUncheckedCreateInput = {
|
export type CompanyUncheckedCreateInput = {
|
||||||
@@ -299,6 +303,7 @@ export type CompanyUncheckedCreateInput = {
|
|||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
credentials?: Prisma.CredentialUncheckedCreateNestedManyWithoutCompanyInput
|
credentials?: Prisma.CredentialUncheckedCreateNestedManyWithoutCompanyInput
|
||||||
unifiSites?: Prisma.UnifiSiteUncheckedCreateNestedManyWithoutCompanyInput
|
unifiSites?: Prisma.UnifiSiteUncheckedCreateNestedManyWithoutCompanyInput
|
||||||
|
opportunities?: Prisma.OpportunityUncheckedCreateNestedManyWithoutCompanyInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CompanyUpdateInput = {
|
export type CompanyUpdateInput = {
|
||||||
@@ -310,6 +315,7 @@ export type CompanyUpdateInput = {
|
|||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
credentials?: Prisma.CredentialUpdateManyWithoutCompanyNestedInput
|
credentials?: Prisma.CredentialUpdateManyWithoutCompanyNestedInput
|
||||||
unifiSites?: Prisma.UnifiSiteUpdateManyWithoutCompanyNestedInput
|
unifiSites?: Prisma.UnifiSiteUpdateManyWithoutCompanyNestedInput
|
||||||
|
opportunities?: Prisma.OpportunityUpdateManyWithoutCompanyNestedInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CompanyUncheckedUpdateInput = {
|
export type CompanyUncheckedUpdateInput = {
|
||||||
@@ -321,6 +327,7 @@ export type CompanyUncheckedUpdateInput = {
|
|||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
credentials?: Prisma.CredentialUncheckedUpdateManyWithoutCompanyNestedInput
|
credentials?: Prisma.CredentialUncheckedUpdateManyWithoutCompanyNestedInput
|
||||||
unifiSites?: Prisma.UnifiSiteUncheckedUpdateManyWithoutCompanyNestedInput
|
unifiSites?: Prisma.UnifiSiteUncheckedUpdateManyWithoutCompanyNestedInput
|
||||||
|
opportunities?: Prisma.OpportunityUncheckedUpdateManyWithoutCompanyNestedInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CompanyCreateManyInput = {
|
export type CompanyCreateManyInput = {
|
||||||
@@ -419,6 +426,22 @@ export type IntFieldUpdateOperationsInput = {
|
|||||||
divide?: number
|
divide?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type CompanyCreateNestedOneWithoutOpportunitiesInput = {
|
||||||
|
create?: Prisma.XOR<Prisma.CompanyCreateWithoutOpportunitiesInput, Prisma.CompanyUncheckedCreateWithoutOpportunitiesInput>
|
||||||
|
connectOrCreate?: Prisma.CompanyCreateOrConnectWithoutOpportunitiesInput
|
||||||
|
connect?: Prisma.CompanyWhereUniqueInput
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CompanyUpdateOneWithoutOpportunitiesNestedInput = {
|
||||||
|
create?: Prisma.XOR<Prisma.CompanyCreateWithoutOpportunitiesInput, Prisma.CompanyUncheckedCreateWithoutOpportunitiesInput>
|
||||||
|
connectOrCreate?: Prisma.CompanyCreateOrConnectWithoutOpportunitiesInput
|
||||||
|
upsert?: Prisma.CompanyUpsertWithoutOpportunitiesInput
|
||||||
|
disconnect?: Prisma.CompanyWhereInput | boolean
|
||||||
|
delete?: Prisma.CompanyWhereInput | boolean
|
||||||
|
connect?: Prisma.CompanyWhereUniqueInput
|
||||||
|
update?: Prisma.XOR<Prisma.XOR<Prisma.CompanyUpdateToOneWithWhereWithoutOpportunitiesInput, Prisma.CompanyUpdateWithoutOpportunitiesInput>, Prisma.CompanyUncheckedUpdateWithoutOpportunitiesInput>
|
||||||
|
}
|
||||||
|
|
||||||
export type CompanyCreateNestedOneWithoutCredentialsInput = {
|
export type CompanyCreateNestedOneWithoutCredentialsInput = {
|
||||||
create?: Prisma.XOR<Prisma.CompanyCreateWithoutCredentialsInput, Prisma.CompanyUncheckedCreateWithoutCredentialsInput>
|
create?: Prisma.XOR<Prisma.CompanyCreateWithoutCredentialsInput, Prisma.CompanyUncheckedCreateWithoutCredentialsInput>
|
||||||
connectOrCreate?: Prisma.CompanyCreateOrConnectWithoutCredentialsInput
|
connectOrCreate?: Prisma.CompanyCreateOrConnectWithoutCredentialsInput
|
||||||
@@ -441,6 +464,7 @@ export type CompanyCreateWithoutUnifiSitesInput = {
|
|||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
credentials?: Prisma.CredentialCreateNestedManyWithoutCompanyInput
|
credentials?: Prisma.CredentialCreateNestedManyWithoutCompanyInput
|
||||||
|
opportunities?: Prisma.OpportunityCreateNestedManyWithoutCompanyInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CompanyUncheckedCreateWithoutUnifiSitesInput = {
|
export type CompanyUncheckedCreateWithoutUnifiSitesInput = {
|
||||||
@@ -451,6 +475,7 @@ export type CompanyUncheckedCreateWithoutUnifiSitesInput = {
|
|||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
credentials?: Prisma.CredentialUncheckedCreateNestedManyWithoutCompanyInput
|
credentials?: Prisma.CredentialUncheckedCreateNestedManyWithoutCompanyInput
|
||||||
|
opportunities?: Prisma.OpportunityUncheckedCreateNestedManyWithoutCompanyInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CompanyCreateOrConnectWithoutUnifiSitesInput = {
|
export type CompanyCreateOrConnectWithoutUnifiSitesInput = {
|
||||||
@@ -477,6 +502,7 @@ export type CompanyUpdateWithoutUnifiSitesInput = {
|
|||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
credentials?: Prisma.CredentialUpdateManyWithoutCompanyNestedInput
|
credentials?: Prisma.CredentialUpdateManyWithoutCompanyNestedInput
|
||||||
|
opportunities?: Prisma.OpportunityUpdateManyWithoutCompanyNestedInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CompanyUncheckedUpdateWithoutUnifiSitesInput = {
|
export type CompanyUncheckedUpdateWithoutUnifiSitesInput = {
|
||||||
@@ -487,6 +513,67 @@ export type CompanyUncheckedUpdateWithoutUnifiSitesInput = {
|
|||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
credentials?: Prisma.CredentialUncheckedUpdateManyWithoutCompanyNestedInput
|
credentials?: Prisma.CredentialUncheckedUpdateManyWithoutCompanyNestedInput
|
||||||
|
opportunities?: Prisma.OpportunityUncheckedUpdateManyWithoutCompanyNestedInput
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CompanyCreateWithoutOpportunitiesInput = {
|
||||||
|
id?: string
|
||||||
|
name: string
|
||||||
|
cw_CompanyId: number
|
||||||
|
cw_Identifier: string
|
||||||
|
createdAt?: Date | string
|
||||||
|
updatedAt?: Date | string
|
||||||
|
credentials?: Prisma.CredentialCreateNestedManyWithoutCompanyInput
|
||||||
|
unifiSites?: Prisma.UnifiSiteCreateNestedManyWithoutCompanyInput
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CompanyUncheckedCreateWithoutOpportunitiesInput = {
|
||||||
|
id?: string
|
||||||
|
name: string
|
||||||
|
cw_CompanyId: number
|
||||||
|
cw_Identifier: string
|
||||||
|
createdAt?: Date | string
|
||||||
|
updatedAt?: Date | string
|
||||||
|
credentials?: Prisma.CredentialUncheckedCreateNestedManyWithoutCompanyInput
|
||||||
|
unifiSites?: Prisma.UnifiSiteUncheckedCreateNestedManyWithoutCompanyInput
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CompanyCreateOrConnectWithoutOpportunitiesInput = {
|
||||||
|
where: Prisma.CompanyWhereUniqueInput
|
||||||
|
create: Prisma.XOR<Prisma.CompanyCreateWithoutOpportunitiesInput, Prisma.CompanyUncheckedCreateWithoutOpportunitiesInput>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CompanyUpsertWithoutOpportunitiesInput = {
|
||||||
|
update: Prisma.XOR<Prisma.CompanyUpdateWithoutOpportunitiesInput, Prisma.CompanyUncheckedUpdateWithoutOpportunitiesInput>
|
||||||
|
create: Prisma.XOR<Prisma.CompanyCreateWithoutOpportunitiesInput, Prisma.CompanyUncheckedCreateWithoutOpportunitiesInput>
|
||||||
|
where?: Prisma.CompanyWhereInput
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CompanyUpdateToOneWithWhereWithoutOpportunitiesInput = {
|
||||||
|
where?: Prisma.CompanyWhereInput
|
||||||
|
data: Prisma.XOR<Prisma.CompanyUpdateWithoutOpportunitiesInput, Prisma.CompanyUncheckedUpdateWithoutOpportunitiesInput>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CompanyUpdateWithoutOpportunitiesInput = {
|
||||||
|
id?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
cw_CompanyId?: Prisma.IntFieldUpdateOperationsInput | number
|
||||||
|
cw_Identifier?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
|
credentials?: Prisma.CredentialUpdateManyWithoutCompanyNestedInput
|
||||||
|
unifiSites?: Prisma.UnifiSiteUpdateManyWithoutCompanyNestedInput
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CompanyUncheckedUpdateWithoutOpportunitiesInput = {
|
||||||
|
id?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
cw_CompanyId?: Prisma.IntFieldUpdateOperationsInput | number
|
||||||
|
cw_Identifier?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
|
credentials?: Prisma.CredentialUncheckedUpdateManyWithoutCompanyNestedInput
|
||||||
|
unifiSites?: Prisma.UnifiSiteUncheckedUpdateManyWithoutCompanyNestedInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CompanyCreateWithoutCredentialsInput = {
|
export type CompanyCreateWithoutCredentialsInput = {
|
||||||
@@ -497,6 +584,7 @@ export type CompanyCreateWithoutCredentialsInput = {
|
|||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
unifiSites?: Prisma.UnifiSiteCreateNestedManyWithoutCompanyInput
|
unifiSites?: Prisma.UnifiSiteCreateNestedManyWithoutCompanyInput
|
||||||
|
opportunities?: Prisma.OpportunityCreateNestedManyWithoutCompanyInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CompanyUncheckedCreateWithoutCredentialsInput = {
|
export type CompanyUncheckedCreateWithoutCredentialsInput = {
|
||||||
@@ -507,6 +595,7 @@ export type CompanyUncheckedCreateWithoutCredentialsInput = {
|
|||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
unifiSites?: Prisma.UnifiSiteUncheckedCreateNestedManyWithoutCompanyInput
|
unifiSites?: Prisma.UnifiSiteUncheckedCreateNestedManyWithoutCompanyInput
|
||||||
|
opportunities?: Prisma.OpportunityUncheckedCreateNestedManyWithoutCompanyInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CompanyCreateOrConnectWithoutCredentialsInput = {
|
export type CompanyCreateOrConnectWithoutCredentialsInput = {
|
||||||
@@ -533,6 +622,7 @@ export type CompanyUpdateWithoutCredentialsInput = {
|
|||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
unifiSites?: Prisma.UnifiSiteUpdateManyWithoutCompanyNestedInput
|
unifiSites?: Prisma.UnifiSiteUpdateManyWithoutCompanyNestedInput
|
||||||
|
opportunities?: Prisma.OpportunityUpdateManyWithoutCompanyNestedInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CompanyUncheckedUpdateWithoutCredentialsInput = {
|
export type CompanyUncheckedUpdateWithoutCredentialsInput = {
|
||||||
@@ -543,6 +633,7 @@ export type CompanyUncheckedUpdateWithoutCredentialsInput = {
|
|||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
unifiSites?: Prisma.UnifiSiteUncheckedUpdateManyWithoutCompanyNestedInput
|
unifiSites?: Prisma.UnifiSiteUncheckedUpdateManyWithoutCompanyNestedInput
|
||||||
|
opportunities?: Prisma.OpportunityUncheckedUpdateManyWithoutCompanyNestedInput
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -553,11 +644,13 @@ export type CompanyUncheckedUpdateWithoutCredentialsInput = {
|
|||||||
export type CompanyCountOutputType = {
|
export type CompanyCountOutputType = {
|
||||||
credentials: number
|
credentials: number
|
||||||
unifiSites: number
|
unifiSites: number
|
||||||
|
opportunities: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CompanyCountOutputTypeSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
export type CompanyCountOutputTypeSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||||
credentials?: boolean | CompanyCountOutputTypeCountCredentialsArgs
|
credentials?: boolean | CompanyCountOutputTypeCountCredentialsArgs
|
||||||
unifiSites?: boolean | CompanyCountOutputTypeCountUnifiSitesArgs
|
unifiSites?: boolean | CompanyCountOutputTypeCountUnifiSitesArgs
|
||||||
|
opportunities?: boolean | CompanyCountOutputTypeCountOpportunitiesArgs
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -584,6 +677,13 @@ export type CompanyCountOutputTypeCountUnifiSitesArgs<ExtArgs extends runtime.Ty
|
|||||||
where?: Prisma.UnifiSiteWhereInput
|
where?: Prisma.UnifiSiteWhereInput
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CompanyCountOutputType without action
|
||||||
|
*/
|
||||||
|
export type CompanyCountOutputTypeCountOpportunitiesArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||||
|
where?: Prisma.OpportunityWhereInput
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
export type CompanySelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
|
export type CompanySelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
|
||||||
id?: boolean
|
id?: boolean
|
||||||
@@ -594,6 +694,7 @@ export type CompanySelect<ExtArgs extends runtime.Types.Extensions.InternalArgs
|
|||||||
updatedAt?: boolean
|
updatedAt?: boolean
|
||||||
credentials?: boolean | Prisma.Company$credentialsArgs<ExtArgs>
|
credentials?: boolean | Prisma.Company$credentialsArgs<ExtArgs>
|
||||||
unifiSites?: boolean | Prisma.Company$unifiSitesArgs<ExtArgs>
|
unifiSites?: boolean | Prisma.Company$unifiSitesArgs<ExtArgs>
|
||||||
|
opportunities?: boolean | Prisma.Company$opportunitiesArgs<ExtArgs>
|
||||||
_count?: boolean | Prisma.CompanyCountOutputTypeDefaultArgs<ExtArgs>
|
_count?: boolean | Prisma.CompanyCountOutputTypeDefaultArgs<ExtArgs>
|
||||||
}, ExtArgs["result"]["company"]>
|
}, ExtArgs["result"]["company"]>
|
||||||
|
|
||||||
@@ -628,6 +729,7 @@ export type CompanyOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs =
|
|||||||
export type CompanyInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
export type CompanyInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||||
credentials?: boolean | Prisma.Company$credentialsArgs<ExtArgs>
|
credentials?: boolean | Prisma.Company$credentialsArgs<ExtArgs>
|
||||||
unifiSites?: boolean | Prisma.Company$unifiSitesArgs<ExtArgs>
|
unifiSites?: boolean | Prisma.Company$unifiSitesArgs<ExtArgs>
|
||||||
|
opportunities?: boolean | Prisma.Company$opportunitiesArgs<ExtArgs>
|
||||||
_count?: boolean | Prisma.CompanyCountOutputTypeDefaultArgs<ExtArgs>
|
_count?: boolean | Prisma.CompanyCountOutputTypeDefaultArgs<ExtArgs>
|
||||||
}
|
}
|
||||||
export type CompanyIncludeCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {}
|
export type CompanyIncludeCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {}
|
||||||
@@ -638,6 +740,7 @@ export type $CompanyPayload<ExtArgs extends runtime.Types.Extensions.InternalArg
|
|||||||
objects: {
|
objects: {
|
||||||
credentials: Prisma.$CredentialPayload<ExtArgs>[]
|
credentials: Prisma.$CredentialPayload<ExtArgs>[]
|
||||||
unifiSites: Prisma.$UnifiSitePayload<ExtArgs>[]
|
unifiSites: Prisma.$UnifiSitePayload<ExtArgs>[]
|
||||||
|
opportunities: Prisma.$OpportunityPayload<ExtArgs>[]
|
||||||
}
|
}
|
||||||
scalars: runtime.Types.Extensions.GetPayloadResult<{
|
scalars: runtime.Types.Extensions.GetPayloadResult<{
|
||||||
id: string
|
id: string
|
||||||
@@ -1042,6 +1145,7 @@ export interface Prisma__CompanyClient<T, Null = never, ExtArgs extends runtime.
|
|||||||
readonly [Symbol.toStringTag]: "PrismaPromise"
|
readonly [Symbol.toStringTag]: "PrismaPromise"
|
||||||
credentials<T extends Prisma.Company$credentialsArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Company$credentialsArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$CredentialPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
|
credentials<T extends Prisma.Company$credentialsArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Company$credentialsArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$CredentialPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
|
||||||
unifiSites<T extends Prisma.Company$unifiSitesArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Company$unifiSitesArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$UnifiSitePayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
|
unifiSites<T extends Prisma.Company$unifiSitesArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Company$unifiSitesArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$UnifiSitePayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
|
||||||
|
opportunities<T extends Prisma.Company$opportunitiesArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Company$opportunitiesArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$OpportunityPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
|
||||||
/**
|
/**
|
||||||
* Attaches callbacks for the resolution and/or rejection of the Promise.
|
* Attaches callbacks for the resolution and/or rejection of the Promise.
|
||||||
* @param onfulfilled The callback to execute when the Promise is resolved.
|
* @param onfulfilled The callback to execute when the Promise is resolved.
|
||||||
@@ -1512,6 +1616,30 @@ export type Company$unifiSitesArgs<ExtArgs extends runtime.Types.Extensions.Inte
|
|||||||
distinct?: Prisma.UnifiSiteScalarFieldEnum | Prisma.UnifiSiteScalarFieldEnum[]
|
distinct?: Prisma.UnifiSiteScalarFieldEnum | Prisma.UnifiSiteScalarFieldEnum[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Company.opportunities
|
||||||
|
*/
|
||||||
|
export type Company$opportunitiesArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||||
|
/**
|
||||||
|
* Select specific fields to fetch from the Opportunity
|
||||||
|
*/
|
||||||
|
select?: Prisma.OpportunitySelect<ExtArgs> | null
|
||||||
|
/**
|
||||||
|
* Omit specific fields from the Opportunity
|
||||||
|
*/
|
||||||
|
omit?: Prisma.OpportunityOmit<ExtArgs> | null
|
||||||
|
/**
|
||||||
|
* Choose, which related nodes to fetch as well
|
||||||
|
*/
|
||||||
|
include?: Prisma.OpportunityInclude<ExtArgs> | null
|
||||||
|
where?: Prisma.OpportunityWhereInput
|
||||||
|
orderBy?: Prisma.OpportunityOrderByWithRelationInput | Prisma.OpportunityOrderByWithRelationInput[]
|
||||||
|
cursor?: Prisma.OpportunityWhereUniqueInput
|
||||||
|
take?: number
|
||||||
|
skip?: number
|
||||||
|
distinct?: Prisma.OpportunityScalarFieldEnum | Prisma.OpportunityScalarFieldEnum[]
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Company without action
|
* Company without action
|
||||||
*/
|
*/
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -32,6 +32,7 @@ export type UserMinAggregateOutputType = {
|
|||||||
email: string | null
|
email: string | null
|
||||||
emailVerified: Date | null
|
emailVerified: Date | null
|
||||||
image: string | null
|
image: string | null
|
||||||
|
cwIdentifier: string | null
|
||||||
userId: string | null
|
userId: string | null
|
||||||
token: string | null
|
token: string | null
|
||||||
createdAt: Date | null
|
createdAt: Date | null
|
||||||
@@ -46,6 +47,7 @@ export type UserMaxAggregateOutputType = {
|
|||||||
email: string | null
|
email: string | null
|
||||||
emailVerified: Date | null
|
emailVerified: Date | null
|
||||||
image: string | null
|
image: string | null
|
||||||
|
cwIdentifier: string | null
|
||||||
userId: string | null
|
userId: string | null
|
||||||
token: string | null
|
token: string | null
|
||||||
createdAt: Date | null
|
createdAt: Date | null
|
||||||
@@ -60,6 +62,7 @@ export type UserCountAggregateOutputType = {
|
|||||||
email: number
|
email: number
|
||||||
emailVerified: number
|
emailVerified: number
|
||||||
image: number
|
image: number
|
||||||
|
cwIdentifier: number
|
||||||
userId: number
|
userId: number
|
||||||
token: number
|
token: number
|
||||||
createdAt: number
|
createdAt: number
|
||||||
@@ -76,6 +79,7 @@ export type UserMinAggregateInputType = {
|
|||||||
email?: true
|
email?: true
|
||||||
emailVerified?: true
|
emailVerified?: true
|
||||||
image?: true
|
image?: true
|
||||||
|
cwIdentifier?: true
|
||||||
userId?: true
|
userId?: true
|
||||||
token?: true
|
token?: true
|
||||||
createdAt?: true
|
createdAt?: true
|
||||||
@@ -90,6 +94,7 @@ export type UserMaxAggregateInputType = {
|
|||||||
email?: true
|
email?: true
|
||||||
emailVerified?: true
|
emailVerified?: true
|
||||||
image?: true
|
image?: true
|
||||||
|
cwIdentifier?: true
|
||||||
userId?: true
|
userId?: true
|
||||||
token?: true
|
token?: true
|
||||||
createdAt?: true
|
createdAt?: true
|
||||||
@@ -104,6 +109,7 @@ export type UserCountAggregateInputType = {
|
|||||||
email?: true
|
email?: true
|
||||||
emailVerified?: true
|
emailVerified?: true
|
||||||
image?: true
|
image?: true
|
||||||
|
cwIdentifier?: true
|
||||||
userId?: true
|
userId?: true
|
||||||
token?: true
|
token?: true
|
||||||
createdAt?: true
|
createdAt?: true
|
||||||
@@ -191,6 +197,7 @@ export type UserGroupByOutputType = {
|
|||||||
email: string
|
email: string
|
||||||
emailVerified: Date | null
|
emailVerified: Date | null
|
||||||
image: string | null
|
image: string | null
|
||||||
|
cwIdentifier: string | null
|
||||||
userId: string
|
userId: string
|
||||||
token: string | null
|
token: string | null
|
||||||
createdAt: Date
|
createdAt: Date
|
||||||
@@ -226,6 +233,7 @@ export type UserWhereInput = {
|
|||||||
email?: Prisma.StringFilter<"User"> | string
|
email?: Prisma.StringFilter<"User"> | string
|
||||||
emailVerified?: Prisma.DateTimeNullableFilter<"User"> | Date | string | null
|
emailVerified?: Prisma.DateTimeNullableFilter<"User"> | Date | string | null
|
||||||
image?: Prisma.StringNullableFilter<"User"> | string | null
|
image?: Prisma.StringNullableFilter<"User"> | string | null
|
||||||
|
cwIdentifier?: Prisma.StringNullableFilter<"User"> | string | null
|
||||||
userId?: Prisma.StringFilter<"User"> | string
|
userId?: Prisma.StringFilter<"User"> | string
|
||||||
token?: Prisma.StringNullableFilter<"User"> | string | null
|
token?: Prisma.StringNullableFilter<"User"> | string | null
|
||||||
createdAt?: Prisma.DateTimeFilter<"User"> | Date | string
|
createdAt?: Prisma.DateTimeFilter<"User"> | Date | string
|
||||||
@@ -242,6 +250,7 @@ export type UserOrderByWithRelationInput = {
|
|||||||
email?: Prisma.SortOrder
|
email?: Prisma.SortOrder
|
||||||
emailVerified?: Prisma.SortOrderInput | Prisma.SortOrder
|
emailVerified?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||||
image?: Prisma.SortOrderInput | Prisma.SortOrder
|
image?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||||
|
cwIdentifier?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||||
userId?: Prisma.SortOrder
|
userId?: Prisma.SortOrder
|
||||||
token?: Prisma.SortOrderInput | Prisma.SortOrder
|
token?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||||
createdAt?: Prisma.SortOrder
|
createdAt?: Prisma.SortOrder
|
||||||
@@ -262,6 +271,7 @@ export type UserWhereUniqueInput = Prisma.AtLeast<{
|
|||||||
name?: Prisma.StringNullableFilter<"User"> | string | null
|
name?: Prisma.StringNullableFilter<"User"> | string | null
|
||||||
emailVerified?: Prisma.DateTimeNullableFilter<"User"> | Date | string | null
|
emailVerified?: Prisma.DateTimeNullableFilter<"User"> | Date | string | null
|
||||||
image?: Prisma.StringNullableFilter<"User"> | string | null
|
image?: Prisma.StringNullableFilter<"User"> | string | null
|
||||||
|
cwIdentifier?: Prisma.StringNullableFilter<"User"> | string | null
|
||||||
token?: Prisma.StringNullableFilter<"User"> | string | null
|
token?: Prisma.StringNullableFilter<"User"> | string | null
|
||||||
createdAt?: Prisma.DateTimeFilter<"User"> | Date | string
|
createdAt?: Prisma.DateTimeFilter<"User"> | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFilter<"User"> | Date | string
|
updatedAt?: Prisma.DateTimeFilter<"User"> | Date | string
|
||||||
@@ -277,6 +287,7 @@ export type UserOrderByWithAggregationInput = {
|
|||||||
email?: Prisma.SortOrder
|
email?: Prisma.SortOrder
|
||||||
emailVerified?: Prisma.SortOrderInput | Prisma.SortOrder
|
emailVerified?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||||
image?: Prisma.SortOrderInput | Prisma.SortOrder
|
image?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||||
|
cwIdentifier?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||||
userId?: Prisma.SortOrder
|
userId?: Prisma.SortOrder
|
||||||
token?: Prisma.SortOrderInput | Prisma.SortOrder
|
token?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||||
createdAt?: Prisma.SortOrder
|
createdAt?: Prisma.SortOrder
|
||||||
@@ -297,6 +308,7 @@ export type UserScalarWhereWithAggregatesInput = {
|
|||||||
email?: Prisma.StringWithAggregatesFilter<"User"> | string
|
email?: Prisma.StringWithAggregatesFilter<"User"> | string
|
||||||
emailVerified?: Prisma.DateTimeNullableWithAggregatesFilter<"User"> | Date | string | null
|
emailVerified?: Prisma.DateTimeNullableWithAggregatesFilter<"User"> | Date | string | null
|
||||||
image?: Prisma.StringNullableWithAggregatesFilter<"User"> | string | null
|
image?: Prisma.StringNullableWithAggregatesFilter<"User"> | string | null
|
||||||
|
cwIdentifier?: Prisma.StringNullableWithAggregatesFilter<"User"> | string | null
|
||||||
userId?: Prisma.StringWithAggregatesFilter<"User"> | string
|
userId?: Prisma.StringWithAggregatesFilter<"User"> | string
|
||||||
token?: Prisma.StringNullableWithAggregatesFilter<"User"> | string | null
|
token?: Prisma.StringNullableWithAggregatesFilter<"User"> | string | null
|
||||||
createdAt?: Prisma.DateTimeWithAggregatesFilter<"User"> | Date | string
|
createdAt?: Prisma.DateTimeWithAggregatesFilter<"User"> | Date | string
|
||||||
@@ -311,6 +323,7 @@ export type UserCreateInput = {
|
|||||||
email: string
|
email: string
|
||||||
emailVerified?: Date | string | null
|
emailVerified?: Date | string | null
|
||||||
image?: string | null
|
image?: string | null
|
||||||
|
cwIdentifier?: string | null
|
||||||
userId: string
|
userId: string
|
||||||
token?: string | null
|
token?: string | null
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
@@ -327,6 +340,7 @@ export type UserUncheckedCreateInput = {
|
|||||||
email: string
|
email: string
|
||||||
emailVerified?: Date | string | null
|
emailVerified?: Date | string | null
|
||||||
image?: string | null
|
image?: string | null
|
||||||
|
cwIdentifier?: string | null
|
||||||
userId: string
|
userId: string
|
||||||
token?: string | null
|
token?: string | null
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
@@ -343,6 +357,7 @@ export type UserUpdateInput = {
|
|||||||
email?: Prisma.StringFieldUpdateOperationsInput | string
|
email?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
emailVerified?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
emailVerified?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
cwIdentifier?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
userId?: Prisma.StringFieldUpdateOperationsInput | string
|
userId?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
token?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
token?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
@@ -359,6 +374,7 @@ export type UserUncheckedUpdateInput = {
|
|||||||
email?: Prisma.StringFieldUpdateOperationsInput | string
|
email?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
emailVerified?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
emailVerified?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
cwIdentifier?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
userId?: Prisma.StringFieldUpdateOperationsInput | string
|
userId?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
token?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
token?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
@@ -375,6 +391,7 @@ export type UserCreateManyInput = {
|
|||||||
email: string
|
email: string
|
||||||
emailVerified?: Date | string | null
|
emailVerified?: Date | string | null
|
||||||
image?: string | null
|
image?: string | null
|
||||||
|
cwIdentifier?: string | null
|
||||||
userId: string
|
userId: string
|
||||||
token?: string | null
|
token?: string | null
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
@@ -389,6 +406,7 @@ export type UserUpdateManyMutationInput = {
|
|||||||
email?: Prisma.StringFieldUpdateOperationsInput | string
|
email?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
emailVerified?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
emailVerified?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
cwIdentifier?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
userId?: Prisma.StringFieldUpdateOperationsInput | string
|
userId?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
token?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
token?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
@@ -403,6 +421,7 @@ export type UserUncheckedUpdateManyInput = {
|
|||||||
email?: Prisma.StringFieldUpdateOperationsInput | string
|
email?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
emailVerified?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
emailVerified?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
cwIdentifier?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
userId?: Prisma.StringFieldUpdateOperationsInput | string
|
userId?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
token?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
token?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
@@ -422,6 +441,7 @@ export type UserCountOrderByAggregateInput = {
|
|||||||
email?: Prisma.SortOrder
|
email?: Prisma.SortOrder
|
||||||
emailVerified?: Prisma.SortOrder
|
emailVerified?: Prisma.SortOrder
|
||||||
image?: Prisma.SortOrder
|
image?: Prisma.SortOrder
|
||||||
|
cwIdentifier?: Prisma.SortOrder
|
||||||
userId?: Prisma.SortOrder
|
userId?: Prisma.SortOrder
|
||||||
token?: Prisma.SortOrder
|
token?: Prisma.SortOrder
|
||||||
createdAt?: Prisma.SortOrder
|
createdAt?: Prisma.SortOrder
|
||||||
@@ -436,6 +456,7 @@ export type UserMaxOrderByAggregateInput = {
|
|||||||
email?: Prisma.SortOrder
|
email?: Prisma.SortOrder
|
||||||
emailVerified?: Prisma.SortOrder
|
emailVerified?: Prisma.SortOrder
|
||||||
image?: Prisma.SortOrder
|
image?: Prisma.SortOrder
|
||||||
|
cwIdentifier?: Prisma.SortOrder
|
||||||
userId?: Prisma.SortOrder
|
userId?: Prisma.SortOrder
|
||||||
token?: Prisma.SortOrder
|
token?: Prisma.SortOrder
|
||||||
createdAt?: Prisma.SortOrder
|
createdAt?: Prisma.SortOrder
|
||||||
@@ -450,6 +471,7 @@ export type UserMinOrderByAggregateInput = {
|
|||||||
email?: Prisma.SortOrder
|
email?: Prisma.SortOrder
|
||||||
emailVerified?: Prisma.SortOrder
|
emailVerified?: Prisma.SortOrder
|
||||||
image?: Prisma.SortOrder
|
image?: Prisma.SortOrder
|
||||||
|
cwIdentifier?: Prisma.SortOrder
|
||||||
userId?: Prisma.SortOrder
|
userId?: Prisma.SortOrder
|
||||||
token?: Prisma.SortOrder
|
token?: Prisma.SortOrder
|
||||||
createdAt?: Prisma.SortOrder
|
createdAt?: Prisma.SortOrder
|
||||||
@@ -530,6 +552,7 @@ export type UserCreateWithoutSessionsInput = {
|
|||||||
email: string
|
email: string
|
||||||
emailVerified?: Date | string | null
|
emailVerified?: Date | string | null
|
||||||
image?: string | null
|
image?: string | null
|
||||||
|
cwIdentifier?: string | null
|
||||||
userId: string
|
userId: string
|
||||||
token?: string | null
|
token?: string | null
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
@@ -545,6 +568,7 @@ export type UserUncheckedCreateWithoutSessionsInput = {
|
|||||||
email: string
|
email: string
|
||||||
emailVerified?: Date | string | null
|
emailVerified?: Date | string | null
|
||||||
image?: string | null
|
image?: string | null
|
||||||
|
cwIdentifier?: string | null
|
||||||
userId: string
|
userId: string
|
||||||
token?: string | null
|
token?: string | null
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
@@ -576,6 +600,7 @@ export type UserUpdateWithoutSessionsInput = {
|
|||||||
email?: Prisma.StringFieldUpdateOperationsInput | string
|
email?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
emailVerified?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
emailVerified?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
cwIdentifier?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
userId?: Prisma.StringFieldUpdateOperationsInput | string
|
userId?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
token?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
token?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
@@ -591,6 +616,7 @@ export type UserUncheckedUpdateWithoutSessionsInput = {
|
|||||||
email?: Prisma.StringFieldUpdateOperationsInput | string
|
email?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
emailVerified?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
emailVerified?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
cwIdentifier?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
userId?: Prisma.StringFieldUpdateOperationsInput | string
|
userId?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
token?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
token?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
@@ -606,6 +632,7 @@ export type UserCreateWithoutRolesInput = {
|
|||||||
email: string
|
email: string
|
||||||
emailVerified?: Date | string | null
|
emailVerified?: Date | string | null
|
||||||
image?: string | null
|
image?: string | null
|
||||||
|
cwIdentifier?: string | null
|
||||||
userId: string
|
userId: string
|
||||||
token?: string | null
|
token?: string | null
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
@@ -621,6 +648,7 @@ export type UserUncheckedCreateWithoutRolesInput = {
|
|||||||
email: string
|
email: string
|
||||||
emailVerified?: Date | string | null
|
emailVerified?: Date | string | null
|
||||||
image?: string | null
|
image?: string | null
|
||||||
|
cwIdentifier?: string | null
|
||||||
userId: string
|
userId: string
|
||||||
token?: string | null
|
token?: string | null
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
@@ -660,6 +688,7 @@ export type UserScalarWhereInput = {
|
|||||||
email?: Prisma.StringFilter<"User"> | string
|
email?: Prisma.StringFilter<"User"> | string
|
||||||
emailVerified?: Prisma.DateTimeNullableFilter<"User"> | Date | string | null
|
emailVerified?: Prisma.DateTimeNullableFilter<"User"> | Date | string | null
|
||||||
image?: Prisma.StringNullableFilter<"User"> | string | null
|
image?: Prisma.StringNullableFilter<"User"> | string | null
|
||||||
|
cwIdentifier?: Prisma.StringNullableFilter<"User"> | string | null
|
||||||
userId?: Prisma.StringFilter<"User"> | string
|
userId?: Prisma.StringFilter<"User"> | string
|
||||||
token?: Prisma.StringNullableFilter<"User"> | string | null
|
token?: Prisma.StringNullableFilter<"User"> | string | null
|
||||||
createdAt?: Prisma.DateTimeFilter<"User"> | Date | string
|
createdAt?: Prisma.DateTimeFilter<"User"> | Date | string
|
||||||
@@ -674,6 +703,7 @@ export type UserUpdateWithoutRolesInput = {
|
|||||||
email?: Prisma.StringFieldUpdateOperationsInput | string
|
email?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
emailVerified?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
emailVerified?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
cwIdentifier?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
userId?: Prisma.StringFieldUpdateOperationsInput | string
|
userId?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
token?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
token?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
@@ -689,6 +719,7 @@ export type UserUncheckedUpdateWithoutRolesInput = {
|
|||||||
email?: Prisma.StringFieldUpdateOperationsInput | string
|
email?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
emailVerified?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
emailVerified?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
cwIdentifier?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
userId?: Prisma.StringFieldUpdateOperationsInput | string
|
userId?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
token?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
token?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
@@ -704,6 +735,7 @@ export type UserUncheckedUpdateManyWithoutRolesInput = {
|
|||||||
email?: Prisma.StringFieldUpdateOperationsInput | string
|
email?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
emailVerified?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
emailVerified?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
cwIdentifier?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
userId?: Prisma.StringFieldUpdateOperationsInput | string
|
userId?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
token?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
token?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
@@ -758,6 +790,7 @@ export type UserSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = r
|
|||||||
email?: boolean
|
email?: boolean
|
||||||
emailVerified?: boolean
|
emailVerified?: boolean
|
||||||
image?: boolean
|
image?: boolean
|
||||||
|
cwIdentifier?: boolean
|
||||||
userId?: boolean
|
userId?: boolean
|
||||||
token?: boolean
|
token?: boolean
|
||||||
createdAt?: boolean
|
createdAt?: boolean
|
||||||
@@ -775,6 +808,7 @@ export type UserSelectCreateManyAndReturn<ExtArgs extends runtime.Types.Extensio
|
|||||||
email?: boolean
|
email?: boolean
|
||||||
emailVerified?: boolean
|
emailVerified?: boolean
|
||||||
image?: boolean
|
image?: boolean
|
||||||
|
cwIdentifier?: boolean
|
||||||
userId?: boolean
|
userId?: boolean
|
||||||
token?: boolean
|
token?: boolean
|
||||||
createdAt?: boolean
|
createdAt?: boolean
|
||||||
@@ -789,6 +823,7 @@ export type UserSelectUpdateManyAndReturn<ExtArgs extends runtime.Types.Extensio
|
|||||||
email?: boolean
|
email?: boolean
|
||||||
emailVerified?: boolean
|
emailVerified?: boolean
|
||||||
image?: boolean
|
image?: boolean
|
||||||
|
cwIdentifier?: boolean
|
||||||
userId?: boolean
|
userId?: boolean
|
||||||
token?: boolean
|
token?: boolean
|
||||||
createdAt?: boolean
|
createdAt?: boolean
|
||||||
@@ -803,13 +838,14 @@ export type UserSelectScalar = {
|
|||||||
email?: boolean
|
email?: boolean
|
||||||
emailVerified?: boolean
|
emailVerified?: boolean
|
||||||
image?: boolean
|
image?: boolean
|
||||||
|
cwIdentifier?: boolean
|
||||||
userId?: boolean
|
userId?: boolean
|
||||||
token?: boolean
|
token?: boolean
|
||||||
createdAt?: boolean
|
createdAt?: boolean
|
||||||
updatedAt?: boolean
|
updatedAt?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UserOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "permissions" | "login" | "name" | "email" | "emailVerified" | "image" | "userId" | "token" | "createdAt" | "updatedAt", ExtArgs["result"]["user"]>
|
export type UserOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "permissions" | "login" | "name" | "email" | "emailVerified" | "image" | "cwIdentifier" | "userId" | "token" | "createdAt" | "updatedAt", ExtArgs["result"]["user"]>
|
||||||
export type UserInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
export type UserInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||||
roles?: boolean | Prisma.User$rolesArgs<ExtArgs>
|
roles?: boolean | Prisma.User$rolesArgs<ExtArgs>
|
||||||
sessions?: boolean | Prisma.User$sessionsArgs<ExtArgs>
|
sessions?: boolean | Prisma.User$sessionsArgs<ExtArgs>
|
||||||
@@ -832,6 +868,7 @@ export type $UserPayload<ExtArgs extends runtime.Types.Extensions.InternalArgs =
|
|||||||
email: string
|
email: string
|
||||||
emailVerified: Date | null
|
emailVerified: Date | null
|
||||||
image: string | null
|
image: string | null
|
||||||
|
cwIdentifier: string | null
|
||||||
userId: string
|
userId: string
|
||||||
token: string | null
|
token: string | null
|
||||||
createdAt: Date
|
createdAt: Date
|
||||||
@@ -1268,6 +1305,7 @@ export interface UserFieldRefs {
|
|||||||
readonly email: Prisma.FieldRef<"User", 'String'>
|
readonly email: Prisma.FieldRef<"User", 'String'>
|
||||||
readonly emailVerified: Prisma.FieldRef<"User", 'DateTime'>
|
readonly emailVerified: Prisma.FieldRef<"User", 'DateTime'>
|
||||||
readonly image: Prisma.FieldRef<"User", 'String'>
|
readonly image: Prisma.FieldRef<"User", 'String'>
|
||||||
|
readonly cwIdentifier: Prisma.FieldRef<"User", 'String'>
|
||||||
readonly userId: Prisma.FieldRef<"User", 'String'>
|
readonly userId: Prisma.FieldRef<"User", 'String'>
|
||||||
readonly token: Prisma.FieldRef<"User", 'String'>
|
readonly token: Prisma.FieldRef<"User", 'String'>
|
||||||
readonly createdAt: Prisma.FieldRef<"User", 'DateTime'>
|
readonly createdAt: Prisma.FieldRef<"User", 'DateTime'>
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "NODE_ENV=development bun --watch src/index.ts",
|
"dev": "NODE_ENV=development bun --watch src/index.ts",
|
||||||
|
"test": "bun test --preload ./tests/setup.ts",
|
||||||
"db:gen": "prisma generate",
|
"db:gen": "prisma generate",
|
||||||
"db:push": "prisma migrate dev --skip-generate",
|
"db:push": "prisma migrate dev --skip-generate",
|
||||||
"db:deploy": "prisma migrate deploy",
|
"db:deploy": "prisma migrate deploy",
|
||||||
@@ -40,6 +41,7 @@
|
|||||||
"cors": "^2.8.6",
|
"cors": "^2.8.6",
|
||||||
"cuid": "^3.0.0",
|
"cuid": "^3.0.0",
|
||||||
"hono": "^4.11.5",
|
"hono": "^4.11.5",
|
||||||
|
"ioredis": "^5.10.0",
|
||||||
"jsonwebtoken": "^9.0.3",
|
"jsonwebtoken": "^9.0.3",
|
||||||
"keypair": "^1.0.4",
|
"keypair": "^1.0.4",
|
||||||
"prisma": "^7.3.0",
|
"prisma": "^7.3.0",
|
||||||
|
|||||||
Executable
+23
@@ -0,0 +1,23 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 1. Resolve any previously failed migrations so deploy can proceed.
|
||||||
|
# Only migrations explicitly marked as "Failed" in the status output are
|
||||||
|
# resolved. We grep for lines containing "Failed" and extract the name.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
echo "[migrate] Checking for failed migrations..."
|
||||||
|
STATUS_OUTPUT=$(bunx prisma migrate status 2>&1 || true)
|
||||||
|
echo "$STATUS_OUTPUT"
|
||||||
|
|
||||||
|
# Only resolve migrations whose status line explicitly says "Failed"
|
||||||
|
echo "$STATUS_OUTPUT" | grep -i "failed" | grep -oE '[0-9]{14}_[a-zA-Z_]+' | while read -r MIGRATION; do
|
||||||
|
echo "[migrate] Resolving failed migration: $MIGRATION"
|
||||||
|
bunx prisma migrate resolve --rolled-back "$MIGRATION" || true
|
||||||
|
done
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 2. Deploy all pending migrations from the migrations directory.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
echo "[migrate] Running prisma migrate deploy..."
|
||||||
|
bunx prisma migrate deploy
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Opportunity" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"cwOpportunityId" INTEGER NOT NULL,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"notes" TEXT,
|
||||||
|
"typeName" TEXT,
|
||||||
|
"typeCwId" INTEGER,
|
||||||
|
"stageName" TEXT,
|
||||||
|
"stageCwId" INTEGER,
|
||||||
|
"statusName" TEXT,
|
||||||
|
"statusCwId" INTEGER,
|
||||||
|
"priorityName" TEXT,
|
||||||
|
"priorityCwId" INTEGER,
|
||||||
|
"ratingName" TEXT,
|
||||||
|
"ratingCwId" INTEGER,
|
||||||
|
"source" TEXT,
|
||||||
|
"campaignName" TEXT,
|
||||||
|
"campaignCwId" INTEGER,
|
||||||
|
"primarySalesRepName" TEXT,
|
||||||
|
"primarySalesRepIdentifier" TEXT,
|
||||||
|
"primarySalesRepCwId" INTEGER,
|
||||||
|
"secondarySalesRepName" TEXT,
|
||||||
|
"secondarySalesRepIdentifier" TEXT,
|
||||||
|
"secondarySalesRepCwId" INTEGER,
|
||||||
|
"companyCwId" INTEGER,
|
||||||
|
"companyName" TEXT,
|
||||||
|
"contactCwId" INTEGER,
|
||||||
|
"contactName" TEXT,
|
||||||
|
"siteCwId" INTEGER,
|
||||||
|
"siteName" TEXT,
|
||||||
|
"customerPO" TEXT,
|
||||||
|
"totalSalesTax" DOUBLE PRECISION NOT NULL DEFAULT 0,
|
||||||
|
"locationName" TEXT,
|
||||||
|
"locationCwId" INTEGER,
|
||||||
|
"departmentName" TEXT,
|
||||||
|
"departmentCwId" INTEGER,
|
||||||
|
"expectedCloseDate" TIMESTAMP(3),
|
||||||
|
"pipelineChangeDate" TIMESTAMP(3),
|
||||||
|
"dateBecameLead" TIMESTAMP(3),
|
||||||
|
"closedDate" TIMESTAMP(3),
|
||||||
|
"closedFlag" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"closedByName" TEXT,
|
||||||
|
"closedByCwId" INTEGER,
|
||||||
|
"companyId" TEXT,
|
||||||
|
"cwLastUpdated" TIMESTAMP(3),
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "Opportunity_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "Opportunity_cwOpportunityId_key" ON "Opportunity"("cwOpportunityId");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Opportunity" ADD CONSTRAINT "Opportunity_companyId_fkey" FOREIGN KEY ("companyId") REFERENCES "Company"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "CatalogItem" ADD COLUMN "identifier" TEXT;
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "CatalogItem_identifier_key" ON "CatalogItem"("identifier");
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
-- AlterTable: User
|
||||||
|
ALTER TABLE "User" ADD COLUMN "cwIdentifier" TEXT;
|
||||||
|
|
||||||
|
-- AlterTable: CatalogItem
|
||||||
|
ALTER TABLE "CatalogItem" ADD COLUMN "category" TEXT;
|
||||||
|
ALTER TABLE "CatalogItem" ADD COLUMN "categoryCwId" INTEGER;
|
||||||
|
ALTER TABLE "CatalogItem" ADD COLUMN "subcategory" TEXT;
|
||||||
|
ALTER TABLE "CatalogItem" ADD COLUMN "subcategoryCwId" INTEGER;
|
||||||
|
|
||||||
|
-- AlterTable: Opportunity
|
||||||
|
ALTER TABLE "Opportunity" ADD COLUMN "productSequence" INTEGER[] DEFAULT ARRAY[]::INTEGER[];
|
||||||
@@ -34,6 +34,8 @@ model User {
|
|||||||
emailVerified DateTime?
|
emailVerified DateTime?
|
||||||
image String?
|
image String?
|
||||||
|
|
||||||
|
cwIdentifier String?
|
||||||
|
|
||||||
userId String @unique
|
userId String @unique
|
||||||
token String?
|
token String?
|
||||||
|
|
||||||
@@ -77,6 +79,7 @@ model Company {
|
|||||||
|
|
||||||
credentials Credential[]
|
credentials Credential[]
|
||||||
unifiSites UnifiSite[]
|
unifiSites UnifiSite[]
|
||||||
|
opportunities Opportunity[]
|
||||||
|
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
@@ -85,6 +88,7 @@ model Company {
|
|||||||
model CatalogItem {
|
model CatalogItem {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
cwCatalogId Int @unique
|
cwCatalogId Int @unique
|
||||||
|
identifier String? @unique
|
||||||
name String
|
name String
|
||||||
description String?
|
description String?
|
||||||
customerDescription String?
|
customerDescription String?
|
||||||
@@ -93,6 +97,11 @@ model CatalogItem {
|
|||||||
linkedItems CatalogItem[] @relation("LinkedItems")
|
linkedItems CatalogItem[] @relation("LinkedItems")
|
||||||
linkedTo CatalogItem[] @relation("LinkedItems")
|
linkedTo CatalogItem[] @relation("LinkedItems")
|
||||||
|
|
||||||
|
category String?
|
||||||
|
categoryCwId Int?
|
||||||
|
subcategory String?
|
||||||
|
subcategoryCwId Int?
|
||||||
|
|
||||||
manufacturer String?
|
manufacturer String?
|
||||||
manufactureCwId Int?
|
manufactureCwId Int?
|
||||||
|
|
||||||
@@ -115,6 +124,77 @@ model CatalogItem {
|
|||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model Opportunity {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
cwOpportunityId Int @unique
|
||||||
|
name String
|
||||||
|
notes String?
|
||||||
|
|
||||||
|
// Stage / status / priority / type / rating stored as JSON references
|
||||||
|
// so we don't need separate lookup tables for CW enums
|
||||||
|
typeName String?
|
||||||
|
typeCwId Int?
|
||||||
|
stageName String?
|
||||||
|
stageCwId Int?
|
||||||
|
statusName String?
|
||||||
|
statusCwId Int?
|
||||||
|
priorityName String?
|
||||||
|
priorityCwId Int?
|
||||||
|
ratingName String?
|
||||||
|
ratingCwId Int?
|
||||||
|
source String?
|
||||||
|
campaignName String?
|
||||||
|
campaignCwId Int?
|
||||||
|
|
||||||
|
// Sales rep references
|
||||||
|
primarySalesRepName String?
|
||||||
|
primarySalesRepIdentifier String?
|
||||||
|
primarySalesRepCwId Int?
|
||||||
|
secondarySalesRepName String?
|
||||||
|
secondarySalesRepIdentifier String?
|
||||||
|
secondarySalesRepCwId Int?
|
||||||
|
|
||||||
|
// Company / contact / site
|
||||||
|
companyCwId Int?
|
||||||
|
companyName String?
|
||||||
|
contactCwId Int?
|
||||||
|
contactName String?
|
||||||
|
siteCwId Int?
|
||||||
|
siteName String?
|
||||||
|
customerPO String?
|
||||||
|
|
||||||
|
// Financials
|
||||||
|
totalSalesTax Float @default(0)
|
||||||
|
|
||||||
|
// Location / department
|
||||||
|
locationName String?
|
||||||
|
locationCwId Int?
|
||||||
|
departmentName String?
|
||||||
|
departmentCwId Int?
|
||||||
|
|
||||||
|
// Dates
|
||||||
|
expectedCloseDate DateTime?
|
||||||
|
pipelineChangeDate DateTime?
|
||||||
|
dateBecameLead DateTime?
|
||||||
|
closedDate DateTime?
|
||||||
|
closedFlag Boolean @default(false)
|
||||||
|
closedByName String?
|
||||||
|
closedByCwId Int?
|
||||||
|
|
||||||
|
// Internal relation to Company (optional, linked by cwCompanyId)
|
||||||
|
companyId String?
|
||||||
|
company Company? @relation(fields: [companyId], references: [id])
|
||||||
|
|
||||||
|
// Local product sequence — array of CW forecast item IDs in display order.
|
||||||
|
// When present, fetchProducts() uses this order instead of CW sequenceNumber.
|
||||||
|
productSequence Int[] @default([])
|
||||||
|
|
||||||
|
cwLastUpdated DateTime?
|
||||||
|
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
}
|
||||||
|
|
||||||
model CredentialType {
|
model CredentialType {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
name String @unique
|
name String @unique
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { apiResponse } from "../../../modules/api-utils/apiResponse";
|
|||||||
import { ContentfulStatusCode } from "hono/utils/http-status";
|
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||||
import { authMiddleware } from "../../middleware/authorization";
|
import { authMiddleware } from "../../middleware/authorization";
|
||||||
import GenericError from "../../../Errors/GenericError";
|
import GenericError from "../../../Errors/GenericError";
|
||||||
|
import { processObjectValuePerms } from "../../../modules/permission-utils/processObjectPermissions";
|
||||||
|
|
||||||
/* /v1/company/companies/[id] */
|
/* /v1/company/companies/[id] */
|
||||||
export default createRoute(
|
export default createRoute(
|
||||||
@@ -42,13 +43,20 @@ export default createRoute(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = apiResponse.successful(
|
const companyData = company.toJson({
|
||||||
"Company Fetched Successfully!",
|
|
||||||
company.toJson({
|
|
||||||
includeAddress,
|
includeAddress,
|
||||||
includePrimaryContact,
|
includePrimaryContact,
|
||||||
includeAllContacts,
|
includeAllContacts,
|
||||||
}),
|
});
|
||||||
|
const gatedData = await processObjectValuePerms(
|
||||||
|
companyData,
|
||||||
|
"obj.company",
|
||||||
|
c.get("user"),
|
||||||
|
);
|
||||||
|
|
||||||
|
const response = apiResponse.successful(
|
||||||
|
"Company Fetched Successfully!",
|
||||||
|
gatedData,
|
||||||
);
|
);
|
||||||
return c.json(response, response.status as ContentfulStatusCode);
|
return c.json(response, response.status as ContentfulStatusCode);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { companies } from "../../../managers/companies";
|
|||||||
import { apiResponse } from "../../../modules/api-utils/apiResponse";
|
import { apiResponse } from "../../../modules/api-utils/apiResponse";
|
||||||
import { ContentfulStatusCode } from "hono/utils/http-status";
|
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||||
import { authMiddleware } from "../../middleware/authorization";
|
import { authMiddleware } from "../../middleware/authorization";
|
||||||
|
import { processObjectValuePerms } from "../../../modules/permission-utils/processObjectPermissions";
|
||||||
|
|
||||||
/* GET /v1/company/companies/:identifier/unifi/sites */
|
/* GET /v1/company/companies/:identifier/unifi/sites */
|
||||||
export default createRoute(
|
export default createRoute(
|
||||||
@@ -12,9 +13,16 @@ export default createRoute(
|
|||||||
async (c) => {
|
async (c) => {
|
||||||
const company = await companies.fetch(c.req.param("identifier"));
|
const company = await companies.fetch(c.req.param("identifier"));
|
||||||
const sites = await unifiSites.fetchByCompany(company.id);
|
const sites = await unifiSites.fetchByCompany(company.id);
|
||||||
|
|
||||||
|
const gatedData = await Promise.all(
|
||||||
|
sites.map((site) =>
|
||||||
|
processObjectValuePerms(site, "obj.unifiSite", c.get("user")),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
const response = apiResponse.successful(
|
const response = apiResponse.successful(
|
||||||
"Company UniFi Sites Fetched Successfully!",
|
"Company UniFi Sites Fetched Successfully!",
|
||||||
sites,
|
gatedData,
|
||||||
);
|
);
|
||||||
return c.json(response, response.status as ContentfulStatusCode);
|
return c.json(response, response.status as ContentfulStatusCode);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { companies } from "../../managers/companies";
|
|||||||
import { apiResponse } from "../../modules/api-utils/apiResponse";
|
import { apiResponse } from "../../modules/api-utils/apiResponse";
|
||||||
import { ContentfulStatusCode } from "hono/utils/http-status";
|
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||||
import { authMiddleware } from "../middleware/authorization";
|
import { authMiddleware } from "../middleware/authorization";
|
||||||
|
import { processObjectValuePerms } from "../../modules/permission-utils/processObjectPermissions";
|
||||||
|
|
||||||
/* /v1/company/companies */
|
/* /v1/company/companies */
|
||||||
export default createRoute(
|
export default createRoute(
|
||||||
@@ -22,9 +23,15 @@ export default createRoute(
|
|||||||
? (await companies.search(search, 1, 999999)).length
|
? (await companies.search(search, 1, 999999)).length
|
||||||
: await companies.count();
|
: await companies.count();
|
||||||
|
|
||||||
|
const gatedData = await Promise.all(
|
||||||
|
data.map((item) =>
|
||||||
|
processObjectValuePerms(item, "obj.company", c.get("user")),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
let response = apiResponse.successful(
|
let response = apiResponse.successful(
|
||||||
"Companies Fetched Successfully!",
|
"Companies Fetched Successfully!",
|
||||||
data,
|
gatedData,
|
||||||
{
|
{
|
||||||
pagination: {
|
pagination: {
|
||||||
previousPage: page == 1 ? null : page - 1, // Previous Page
|
previousPage: page == 1 ? null : page - 1, // Previous Page
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { credentialTypes } from "../../managers/credentialTypes";
|
|||||||
import { apiResponse } from "../../modules/api-utils/apiResponse";
|
import { apiResponse } from "../../modules/api-utils/apiResponse";
|
||||||
import { ContentfulStatusCode } from "hono/utils/http-status";
|
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||||
import { authMiddleware } from "../middleware/authorization";
|
import { authMiddleware } from "../middleware/authorization";
|
||||||
|
import { processObjectValuePerms } from "../../modules/permission-utils/processObjectPermissions";
|
||||||
|
|
||||||
/* /v1/credential-type/:identifier */
|
/* /v1/credential-type/:identifier */
|
||||||
export default createRoute(
|
export default createRoute(
|
||||||
@@ -15,9 +16,15 @@ export default createRoute(
|
|||||||
c.req.param("identifier"),
|
c.req.param("identifier"),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const gatedData = await processObjectValuePerms(
|
||||||
|
credentialType.toJson({ includeCredentialCount: true }),
|
||||||
|
"obj.credentialType",
|
||||||
|
c.get("user"),
|
||||||
|
);
|
||||||
|
|
||||||
const response = apiResponse.successful(
|
const response = apiResponse.successful(
|
||||||
"Credential Type Fetched Successfully!",
|
"Credential Type Fetched Successfully!",
|
||||||
credentialType.toJson({ includeCredentialCount: true }),
|
gatedData,
|
||||||
);
|
);
|
||||||
return c.json(response, response.status as ContentfulStatusCode);
|
return c.json(response, response.status as ContentfulStatusCode);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { credentialTypes } from "../../managers/credentialTypes";
|
|||||||
import { apiResponse } from "../../modules/api-utils/apiResponse";
|
import { apiResponse } from "../../modules/api-utils/apiResponse";
|
||||||
import { ContentfulStatusCode } from "hono/utils/http-status";
|
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||||
import { authMiddleware } from "../middleware/authorization";
|
import { authMiddleware } from "../middleware/authorization";
|
||||||
|
import { processObjectValuePerms } from "../../modules/permission-utils/processObjectPermissions";
|
||||||
|
|
||||||
/* /v1/credential-type */
|
/* /v1/credential-type */
|
||||||
export default createRoute(
|
export default createRoute(
|
||||||
@@ -13,11 +14,19 @@ export default createRoute(
|
|||||||
async (c) => {
|
async (c) => {
|
||||||
const allCredentialTypes = await credentialTypes.fetchAll();
|
const allCredentialTypes = await credentialTypes.fetchAll();
|
||||||
|
|
||||||
|
const gatedData = await Promise.all(
|
||||||
|
allCredentialTypes.map((ct) =>
|
||||||
|
processObjectValuePerms(
|
||||||
|
ct.toJson({ includeCredentialCount: true }),
|
||||||
|
"obj.credentialType",
|
||||||
|
c.get("user"),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
const response = apiResponse.successful(
|
const response = apiResponse.successful(
|
||||||
"Credential Types Fetched Successfully!",
|
"Credential Types Fetched Successfully!",
|
||||||
allCredentialTypes.map((ct) =>
|
gatedData,
|
||||||
ct.toJson({ includeCredentialCount: true }),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
return c.json(response, response.status as ContentfulStatusCode);
|
return c.json(response, response.status as ContentfulStatusCode);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { credentialTypes } from "../../managers/credentialTypes";
|
|||||||
import { apiResponse } from "../../modules/api-utils/apiResponse";
|
import { apiResponse } from "../../modules/api-utils/apiResponse";
|
||||||
import { ContentfulStatusCode } from "hono/utils/http-status";
|
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||||
import { authMiddleware } from "../middleware/authorization";
|
import { authMiddleware } from "../middleware/authorization";
|
||||||
|
import { processObjectValuePerms } from "../../modules/permission-utils/processObjectPermissions";
|
||||||
|
|
||||||
/* /v1/credential-type/:id/credentials */
|
/* /v1/credential-type/:id/credentials */
|
||||||
export default createRoute(
|
export default createRoute(
|
||||||
@@ -14,9 +15,15 @@ export default createRoute(
|
|||||||
const credentialType = await credentialTypes.fetch(c.req.param("id"));
|
const credentialType = await credentialTypes.fetch(c.req.param("id"));
|
||||||
const credentials = await credentialType.fetchCredentials();
|
const credentials = await credentialType.fetchCredentials();
|
||||||
|
|
||||||
|
const gatedData = await Promise.all(
|
||||||
|
credentials.map((cred) =>
|
||||||
|
processObjectValuePerms(cred.toJson(), "obj.credential", c.get("user")),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
const response = apiResponse.successful(
|
const response = apiResponse.successful(
|
||||||
"Credentials Fetched Successfully!",
|
"Credentials Fetched Successfully!",
|
||||||
credentials.map((cred) => cred.toJson()),
|
gatedData,
|
||||||
);
|
);
|
||||||
return c.json(response, response.status as ContentfulStatusCode);
|
return c.json(response, response.status as ContentfulStatusCode);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { credentials } from "../../managers/credentials";
|
|||||||
import { apiResponse } from "../../modules/api-utils/apiResponse";
|
import { apiResponse } from "../../modules/api-utils/apiResponse";
|
||||||
import { ContentfulStatusCode } from "hono/utils/http-status";
|
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||||
import { authMiddleware } from "../middleware/authorization";
|
import { authMiddleware } from "../middleware/authorization";
|
||||||
|
import { processObjectValuePerms } from "../../modules/permission-utils/processObjectPermissions";
|
||||||
|
|
||||||
/* /v1/credential/:id */
|
/* /v1/credential/:id */
|
||||||
export default createRoute(
|
export default createRoute(
|
||||||
@@ -12,10 +13,15 @@ export default createRoute(
|
|||||||
|
|
||||||
async (c) => {
|
async (c) => {
|
||||||
const credential = await credentials.fetch(c.req.param("id"));
|
const credential = await credentials.fetch(c.req.param("id"));
|
||||||
|
const gatedData = await processObjectValuePerms(
|
||||||
|
credential.toJson(),
|
||||||
|
"obj.credential",
|
||||||
|
c.get("user"),
|
||||||
|
);
|
||||||
|
|
||||||
const response = apiResponse.successful(
|
const response = apiResponse.successful(
|
||||||
"Credential Fetched Successfully!",
|
"Credential Fetched Successfully!",
|
||||||
credential.toJson(),
|
gatedData,
|
||||||
);
|
);
|
||||||
return c.json(response, response.status as ContentfulStatusCode);
|
return c.json(response, response.status as ContentfulStatusCode);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { credentials } from "../../managers/credentials";
|
|||||||
import { apiResponse } from "../../modules/api-utils/apiResponse";
|
import { apiResponse } from "../../modules/api-utils/apiResponse";
|
||||||
import { ContentfulStatusCode } from "hono/utils/http-status";
|
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||||
import { authMiddleware } from "../middleware/authorization";
|
import { authMiddleware } from "../middleware/authorization";
|
||||||
|
import { processObjectValuePerms } from "../../modules/permission-utils/processObjectPermissions";
|
||||||
|
|
||||||
/* /v1/credential/company/:companyId */
|
/* /v1/credential/company/:companyId */
|
||||||
export default createRoute(
|
export default createRoute(
|
||||||
@@ -15,9 +16,15 @@ export default createRoute(
|
|||||||
c.req.param("companyId"),
|
c.req.param("companyId"),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const gatedData = await Promise.all(
|
||||||
|
companyCredentials.map((cred) =>
|
||||||
|
processObjectValuePerms(cred.toJson(), "obj.credential", c.get("user")),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
const response = apiResponse.successful(
|
const response = apiResponse.successful(
|
||||||
"Company Credentials Fetched Successfully!",
|
"Company Credentials Fetched Successfully!",
|
||||||
companyCredentials.map((cred) => cred.toJson()),
|
gatedData,
|
||||||
);
|
);
|
||||||
return c.json(response, response.status as ContentfulStatusCode);
|
return c.json(response, response.status as ContentfulStatusCode);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { credentials } from "../../managers/credentials";
|
|||||||
import { apiResponse } from "../../modules/api-utils/apiResponse";
|
import { apiResponse } from "../../modules/api-utils/apiResponse";
|
||||||
import { ContentfulStatusCode } from "hono/utils/http-status";
|
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||||
import { authMiddleware } from "../middleware/authorization";
|
import { authMiddleware } from "../middleware/authorization";
|
||||||
|
import { processObjectValuePerms } from "../../modules/permission-utils/processObjectPermissions";
|
||||||
|
|
||||||
/* GET /v1/credential/credentials/:id/sub-credentials */
|
/* GET /v1/credential/credentials/:id/sub-credentials */
|
||||||
export default createRoute(
|
export default createRoute(
|
||||||
@@ -17,9 +18,15 @@ export default createRoute(
|
|||||||
|
|
||||||
const subCredentials = await credentials.fetchSubCredentials(parentId);
|
const subCredentials = await credentials.fetchSubCredentials(parentId);
|
||||||
|
|
||||||
|
const gatedData = await Promise.all(
|
||||||
|
subCredentials.map((sc) =>
|
||||||
|
processObjectValuePerms(sc.toJson(), "obj.credential", c.get("user")),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
const response = apiResponse.successful(
|
const response = apiResponse.successful(
|
||||||
"Sub-Credentials Fetched Successfully!",
|
"Sub-Credentials Fetched Successfully!",
|
||||||
subCredentials.map((sc) => sc.toJson()),
|
gatedData,
|
||||||
);
|
);
|
||||||
return c.json(response, response.status as ContentfulStatusCode);
|
return c.json(response, response.status as ContentfulStatusCode);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import { createRoute } from "../../../modules/api-utils/createRoute";
|
||||||
|
import { procurement } from "../../../managers/procurement";
|
||||||
|
import { apiResponse } from "../../../modules/api-utils/apiResponse";
|
||||||
|
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||||
|
import { authMiddleware } from "../../middleware/authorization";
|
||||||
|
import { processObjectValuePerms } from "../../../modules/permission-utils/processObjectPermissions";
|
||||||
|
|
||||||
|
/* /v1/procurement/items/:identifier */
|
||||||
|
export default createRoute(
|
||||||
|
"get",
|
||||||
|
["/items/:identifier"],
|
||||||
|
async (c) => {
|
||||||
|
const identifier = c.req.param("identifier");
|
||||||
|
const includeLinkedItems = c.req.query("includeLinkedItems") === "true";
|
||||||
|
|
||||||
|
const item = await procurement.fetchItem(identifier);
|
||||||
|
|
||||||
|
const gatedData = await processObjectValuePerms(
|
||||||
|
item.toJson({ includeLinkedItems }),
|
||||||
|
"obj.catalogItem",
|
||||||
|
c.get("user"),
|
||||||
|
);
|
||||||
|
|
||||||
|
const response = apiResponse.successful(
|
||||||
|
"Catalog item fetched successfully!",
|
||||||
|
gatedData,
|
||||||
|
);
|
||||||
|
|
||||||
|
return c.json(response, response.status as ContentfulStatusCode);
|
||||||
|
},
|
||||||
|
authMiddleware({ permissions: ["procurement.catalog.fetch"] }),
|
||||||
|
);
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import { createRoute } from "../../../modules/api-utils/createRoute";
|
||||||
|
import { procurement } from "../../../managers/procurement";
|
||||||
|
import { apiResponse } from "../../../modules/api-utils/apiResponse";
|
||||||
|
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||||
|
import { authMiddleware } from "../../middleware/authorization";
|
||||||
|
import { processObjectValuePerms } from "../../../modules/permission-utils/processObjectPermissions";
|
||||||
|
|
||||||
|
/* GET /v1/procurement/items/:identifier/linked */
|
||||||
|
export default createRoute(
|
||||||
|
"get",
|
||||||
|
["/items/:identifier/linked"],
|
||||||
|
async (c) => {
|
||||||
|
const identifier = c.req.param("identifier");
|
||||||
|
const item = await procurement.fetchItem(identifier);
|
||||||
|
|
||||||
|
const linkedItems = item.getLinkedItems().map((linked) => linked.toJson());
|
||||||
|
|
||||||
|
const gatedData = await Promise.all(
|
||||||
|
linkedItems.map((linked) =>
|
||||||
|
processObjectValuePerms(linked, "obj.catalogItem", c.get("user")),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const response = apiResponse.successful(
|
||||||
|
"Linked catalog items fetched successfully!",
|
||||||
|
gatedData,
|
||||||
|
);
|
||||||
|
|
||||||
|
return c.json(response, response.status as ContentfulStatusCode);
|
||||||
|
},
|
||||||
|
authMiddleware({ permissions: ["procurement.catalog.fetch"] }),
|
||||||
|
);
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { createRoute } from "../../../modules/api-utils/createRoute";
|
||||||
|
import { procurement } from "../../../managers/procurement";
|
||||||
|
import { apiResponse } from "../../../modules/api-utils/apiResponse";
|
||||||
|
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||||
|
import { authMiddleware } from "../../middleware/authorization";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
/* POST /v1/procurement/items/:identifier/link */
|
||||||
|
export default createRoute(
|
||||||
|
"post",
|
||||||
|
["/items/:identifier/link"],
|
||||||
|
async (c) => {
|
||||||
|
const identifier = c.req.param("identifier");
|
||||||
|
const body = await c.req.json();
|
||||||
|
const schema = z.object({ targetId: z.string() }).strict();
|
||||||
|
const { targetId } = schema.parse(body);
|
||||||
|
|
||||||
|
const item = await procurement.linkItems(identifier, targetId);
|
||||||
|
|
||||||
|
const response = apiResponse.successful(
|
||||||
|
"Catalog item linked successfully!",
|
||||||
|
item.toJson({ includeLinkedItems: true }),
|
||||||
|
);
|
||||||
|
|
||||||
|
return c.json(response, response.status as ContentfulStatusCode);
|
||||||
|
},
|
||||||
|
authMiddleware({ permissions: ["procurement.catalog.link"] }),
|
||||||
|
);
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { createRoute } from "../../../modules/api-utils/createRoute";
|
||||||
|
import { procurement } from "../../../managers/procurement";
|
||||||
|
import { apiResponse } from "../../../modules/api-utils/apiResponse";
|
||||||
|
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||||
|
import { authMiddleware } from "../../middleware/authorization";
|
||||||
|
|
||||||
|
/* /v1/procurement/items/:identifier/refresh-inventory */
|
||||||
|
export default createRoute(
|
||||||
|
"post",
|
||||||
|
["/items/:identifier/refresh-inventory"],
|
||||||
|
async (c) => {
|
||||||
|
const identifier = c.req.param("identifier");
|
||||||
|
const item = await procurement.fetchItem(identifier);
|
||||||
|
|
||||||
|
await item.refreshInventory();
|
||||||
|
|
||||||
|
const response = apiResponse.successful(
|
||||||
|
"Inventory refreshed successfully!",
|
||||||
|
item.toJson(),
|
||||||
|
);
|
||||||
|
|
||||||
|
return c.json(response, response.status as ContentfulStatusCode);
|
||||||
|
},
|
||||||
|
authMiddleware({ permissions: ["procurement.catalog.inventory.refresh"] }),
|
||||||
|
);
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { createRoute } from "../../../modules/api-utils/createRoute";
|
||||||
|
import { procurement } from "../../../managers/procurement";
|
||||||
|
import { apiResponse } from "../../../modules/api-utils/apiResponse";
|
||||||
|
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||||
|
import { authMiddleware } from "../../middleware/authorization";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
/* POST /v1/procurement/items/:identifier/unlink */
|
||||||
|
export default createRoute(
|
||||||
|
"post",
|
||||||
|
["/items/:identifier/unlink"],
|
||||||
|
async (c) => {
|
||||||
|
const identifier = c.req.param("identifier");
|
||||||
|
const body = await c.req.json();
|
||||||
|
const schema = z.object({ targetId: z.string() }).strict();
|
||||||
|
const { targetId } = schema.parse(body);
|
||||||
|
|
||||||
|
const item = await procurement.unlinkItems(identifier, targetId);
|
||||||
|
|
||||||
|
const response = apiResponse.successful(
|
||||||
|
"Catalog item unlinked successfully!",
|
||||||
|
item.toJson({ includeLinkedItems: true }),
|
||||||
|
);
|
||||||
|
|
||||||
|
return c.json(response, response.status as ContentfulStatusCode);
|
||||||
|
},
|
||||||
|
authMiddleware({ permissions: ["procurement.catalog.link"] }),
|
||||||
|
);
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { createRoute } from "../../modules/api-utils/createRoute";
|
||||||
|
import { apiResponse } from "../../modules/api-utils/apiResponse";
|
||||||
|
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||||
|
import { authMiddleware } from "../middleware/authorization";
|
||||||
|
import {
|
||||||
|
serializeCategoryTree,
|
||||||
|
serializeEcosystemTree,
|
||||||
|
} from "../../modules/catalog-categories/catalogCategories";
|
||||||
|
|
||||||
|
/* /v1/procurement/categories */
|
||||||
|
export default createRoute(
|
||||||
|
"get",
|
||||||
|
["/categories"],
|
||||||
|
async (c) => {
|
||||||
|
const categories = serializeCategoryTree();
|
||||||
|
const ecosystems = serializeEcosystemTree();
|
||||||
|
|
||||||
|
const response = apiResponse.successful(
|
||||||
|
"Category and ecosystem data fetched successfully!",
|
||||||
|
{ categories, ecosystems },
|
||||||
|
);
|
||||||
|
|
||||||
|
return c.json(response, response.status as ContentfulStatusCode);
|
||||||
|
},
|
||||||
|
authMiddleware({ permissions: ["procurement.catalog.fetch.many"] }),
|
||||||
|
);
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { createRoute } from "../../modules/api-utils/createRoute";
|
||||||
|
import { procurement } from "../../managers/procurement";
|
||||||
|
import { apiResponse } from "../../modules/api-utils/apiResponse";
|
||||||
|
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||||
|
import { authMiddleware } from "../middleware/authorization";
|
||||||
|
|
||||||
|
/* /v1/procurement/count */
|
||||||
|
export default createRoute(
|
||||||
|
"get",
|
||||||
|
["/count"],
|
||||||
|
async (c) => {
|
||||||
|
const activeOnly = c.req.query("activeOnly") === "true";
|
||||||
|
|
||||||
|
const count = await procurement.count({ activeOnly });
|
||||||
|
|
||||||
|
const response = apiResponse.successful(
|
||||||
|
"Catalog item count fetched successfully!",
|
||||||
|
{ count },
|
||||||
|
);
|
||||||
|
|
||||||
|
return c.json(response, response.status as ContentfulStatusCode);
|
||||||
|
},
|
||||||
|
authMiddleware({ permissions: ["procurement.catalog.fetch.many"] }),
|
||||||
|
);
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
import { createRoute } from "../../modules/api-utils/createRoute";
|
||||||
|
import { procurement, CatalogFilterOpts } from "../../managers/procurement";
|
||||||
|
import { apiResponse } from "../../modules/api-utils/apiResponse";
|
||||||
|
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||||
|
import { authMiddleware } from "../middleware/authorization";
|
||||||
|
import { processObjectValuePerms } from "../../modules/permission-utils/processObjectPermissions";
|
||||||
|
|
||||||
|
/* /v1/procurement/items */
|
||||||
|
export default createRoute(
|
||||||
|
"get",
|
||||||
|
["/items"],
|
||||||
|
async (c) => {
|
||||||
|
const page = Number(c.req.query("page") ?? 1);
|
||||||
|
const rpp = Number(c.req.query("rpp") ?? 30);
|
||||||
|
const search = c.req.query("search") as string;
|
||||||
|
const includeInactive = c.req.query("includeInactive") === "true";
|
||||||
|
|
||||||
|
// Category / filter params
|
||||||
|
const category = c.req.query("category") as string | undefined;
|
||||||
|
const subcategory = c.req.query("subcategory") as string | undefined;
|
||||||
|
const group = c.req.query("group") as string | undefined;
|
||||||
|
const manufacturer = c.req.query("manufacturer") as string | undefined;
|
||||||
|
const ecosystem = c.req.query("ecosystem") as string | undefined;
|
||||||
|
const inStock = c.req.query("inStock") === "true" ? true : undefined;
|
||||||
|
const minPrice = c.req.query("minPrice")
|
||||||
|
? Number(c.req.query("minPrice"))
|
||||||
|
: undefined;
|
||||||
|
const maxPrice = c.req.query("maxPrice")
|
||||||
|
? Number(c.req.query("maxPrice"))
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const filterOpts: CatalogFilterOpts = {
|
||||||
|
includeInactive,
|
||||||
|
category,
|
||||||
|
subcategory,
|
||||||
|
group,
|
||||||
|
manufacturer,
|
||||||
|
ecosystem,
|
||||||
|
inStock,
|
||||||
|
minPrice,
|
||||||
|
maxPrice,
|
||||||
|
};
|
||||||
|
|
||||||
|
const data = search
|
||||||
|
? await procurement.search(search, page, rpp, filterOpts)
|
||||||
|
: await procurement.fetchPages(page, rpp, filterOpts);
|
||||||
|
|
||||||
|
const totalRecords = search
|
||||||
|
? await procurement.countSearch(search, filterOpts)
|
||||||
|
: await procurement.count(filterOpts);
|
||||||
|
|
||||||
|
const gatedData = await Promise.all(
|
||||||
|
data.map((item) =>
|
||||||
|
processObjectValuePerms(
|
||||||
|
item.toJson(),
|
||||||
|
"obj.catalogItem",
|
||||||
|
c.get("user"),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const response = apiResponse.successful(
|
||||||
|
"Catalog items fetched successfully!",
|
||||||
|
gatedData,
|
||||||
|
{
|
||||||
|
pagination: {
|
||||||
|
previousPage: page <= 1 ? null : page - 1,
|
||||||
|
currentPage: page,
|
||||||
|
nextPage: page >= totalRecords / rpp ? null : page + 1,
|
||||||
|
totalPages: Math.ceil(totalRecords / rpp),
|
||||||
|
totalRecords,
|
||||||
|
listedRecords: rpp,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return c.json(response, response.status as ContentfulStatusCode);
|
||||||
|
},
|
||||||
|
authMiddleware({ permissions: ["procurement.catalog.fetch.many"] }),
|
||||||
|
);
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import { createRoute } from "../../modules/api-utils/createRoute";
|
||||||
|
import { procurement } from "../../managers/procurement";
|
||||||
|
import { apiResponse } from "../../modules/api-utils/apiResponse";
|
||||||
|
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||||
|
import { authMiddleware } from "../middleware/authorization";
|
||||||
|
|
||||||
|
/* /v1/procurement/filters */
|
||||||
|
export default createRoute(
|
||||||
|
"get",
|
||||||
|
["/filters"],
|
||||||
|
async (c) => {
|
||||||
|
const category = c.req.query("category") as string | undefined;
|
||||||
|
const subcategory = c.req.query("subcategory") as string | undefined;
|
||||||
|
const includeInactive = c.req.query("includeInactive") === "true";
|
||||||
|
|
||||||
|
const filterOpts = { category, subcategory, includeInactive };
|
||||||
|
|
||||||
|
const [categories, subcategories, manufacturers] = await Promise.all([
|
||||||
|
procurement.fetchDistinctValues("category", filterOpts),
|
||||||
|
procurement.fetchDistinctValues("subcategory", filterOpts),
|
||||||
|
procurement.fetchDistinctValues("manufacturer", filterOpts),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const response = apiResponse.successful(
|
||||||
|
"Available filter values fetched successfully!",
|
||||||
|
{ categories, subcategories, manufacturers },
|
||||||
|
);
|
||||||
|
|
||||||
|
return c.json(response, response.status as ContentfulStatusCode);
|
||||||
|
},
|
||||||
|
authMiddleware({ permissions: ["procurement.catalog.fetch.many"] }),
|
||||||
|
);
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { default as fetchAll } from "./fetchAll";
|
||||||
|
import { default as fetch } from "./[id]/fetch";
|
||||||
|
import { default as refreshInventory } from "./[id]/refreshInventory";
|
||||||
|
import { default as link } from "./[id]/link";
|
||||||
|
import { default as unlink } from "./[id]/unlink";
|
||||||
|
import { default as fetchLinked } from "./[id]/fetchLinked";
|
||||||
|
import { default as count } from "./count";
|
||||||
|
import { default as categories } from "./categories";
|
||||||
|
import { default as filters } from "./filters";
|
||||||
|
|
||||||
|
export {
|
||||||
|
categories,
|
||||||
|
count,
|
||||||
|
fetch,
|
||||||
|
fetchAll,
|
||||||
|
fetchLinked,
|
||||||
|
filters,
|
||||||
|
link,
|
||||||
|
refreshInventory,
|
||||||
|
unlink,
|
||||||
|
};
|
||||||
@@ -4,6 +4,7 @@ import { roles } from "../../managers/roles";
|
|||||||
import { apiResponse } from "../../modules/api-utils/apiResponse";
|
import { apiResponse } from "../../modules/api-utils/apiResponse";
|
||||||
import { ContentfulStatusCode } from "hono/utils/http-status";
|
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||||
import { authMiddleware } from "../middleware/authorization";
|
import { authMiddleware } from "../middleware/authorization";
|
||||||
|
import { processObjectValuePerms } from "../../modules/permission-utils/processObjectPermissions";
|
||||||
|
|
||||||
/* GET /v1/role/:identifier */
|
/* GET /v1/role/:identifier */
|
||||||
export default createRoute(
|
export default createRoute(
|
||||||
@@ -15,9 +16,15 @@ export default createRoute(
|
|||||||
|
|
||||||
const role = await roles.fetch(identifier);
|
const role = await roles.fetch(identifier);
|
||||||
|
|
||||||
|
const gatedData = await processObjectValuePerms(
|
||||||
|
role.toJson({ viewPermissions: true }),
|
||||||
|
"obj.role",
|
||||||
|
c.get("user"),
|
||||||
|
);
|
||||||
|
|
||||||
const response = apiResponse.successful(
|
const response = apiResponse.successful(
|
||||||
"Role Fetched Successfully!",
|
"Role Fetched Successfully!",
|
||||||
role.toJson({ viewPermissions: true }),
|
gatedData,
|
||||||
);
|
);
|
||||||
return c.json(response, response.status as ContentfulStatusCode);
|
return c.json(response, response.status as ContentfulStatusCode);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { roles } from "../../managers/roles";
|
|||||||
import { apiResponse } from "../../modules/api-utils/apiResponse";
|
import { apiResponse } from "../../modules/api-utils/apiResponse";
|
||||||
import { ContentfulStatusCode } from "hono/utils/http-status";
|
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||||
import { authMiddleware } from "../middleware/authorization";
|
import { authMiddleware } from "../middleware/authorization";
|
||||||
|
import { processObjectValuePerms } from "../../modules/permission-utils/processObjectPermissions";
|
||||||
|
|
||||||
/* GET /v1/role */
|
/* GET /v1/role */
|
||||||
export default createRoute(
|
export default createRoute(
|
||||||
@@ -13,13 +14,19 @@ export default createRoute(
|
|||||||
async (c) => {
|
async (c) => {
|
||||||
const allRoles = await roles.fetchAllRoles();
|
const allRoles = await roles.fetchAllRoles();
|
||||||
|
|
||||||
const rolesArray = allRoles.map((role) =>
|
const gatedData = await Promise.all(
|
||||||
|
allRoles.map((role) =>
|
||||||
|
processObjectValuePerms(
|
||||||
role.toJson({ viewPermissions: true }),
|
role.toJson({ viewPermissions: true }),
|
||||||
|
"obj.role",
|
||||||
|
c.get("user"),
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
const response = apiResponse.successful(
|
const response = apiResponse.successful(
|
||||||
"Roles Fetched Successfully!",
|
"Roles Fetched Successfully!",
|
||||||
rolesArray,
|
gatedData,
|
||||||
);
|
);
|
||||||
return c.json(response, response.status as ContentfulStatusCode);
|
return c.json(response, response.status as ContentfulStatusCode);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { roles } from "../../managers/roles";
|
|||||||
import { apiResponse } from "../../modules/api-utils/apiResponse";
|
import { apiResponse } from "../../modules/api-utils/apiResponse";
|
||||||
import { ContentfulStatusCode } from "hono/utils/http-status";
|
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||||
import { authMiddleware } from "../middleware/authorization";
|
import { authMiddleware } from "../middleware/authorization";
|
||||||
|
import { processObjectValuePerms } from "../../modules/permission-utils/processObjectPermissions";
|
||||||
|
|
||||||
/* GET /v1/role/:identifier/users */
|
/* GET /v1/role/:identifier/users */
|
||||||
export default createRoute(
|
export default createRoute(
|
||||||
@@ -16,11 +17,15 @@ export default createRoute(
|
|||||||
const role = await roles.fetch(identifier);
|
const role = await roles.fetch(identifier);
|
||||||
const users = role.getUsers();
|
const users = role.getUsers();
|
||||||
|
|
||||||
const usersArray = users.map((user) => user.toJson());
|
const gatedData = await Promise.all(
|
||||||
|
users.map((user) =>
|
||||||
|
processObjectValuePerms(user.toJson(), "obj.user", c.get("user")),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
const response = apiResponse.successful(
|
const response = apiResponse.successful(
|
||||||
"Users Fetched Successfully!",
|
"Users Fetched Successfully!",
|
||||||
usersArray,
|
gatedData,
|
||||||
);
|
);
|
||||||
return c.json(response, response.status as ContentfulStatusCode);
|
return c.json(response, response.status as ContentfulStatusCode);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { Hono } from "hono";
|
||||||
|
import * as procurementRoutes from "../procurement";
|
||||||
|
|
||||||
|
const procurementRouter = new Hono();
|
||||||
|
Object.values(procurementRoutes).map((r) => procurementRouter.route("/", r));
|
||||||
|
|
||||||
|
export default procurementRouter;
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { Hono } from "hono";
|
||||||
|
import * as salesRoutes from "../sales";
|
||||||
|
|
||||||
|
const salesRouter = new Hono();
|
||||||
|
Object.values(salesRoutes).map((r) => salesRouter.route("/", r));
|
||||||
|
|
||||||
|
export default salesRouter;
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import { createRoute } from "../../../modules/api-utils/createRoute";
|
||||||
|
import { opportunities } from "../../../managers/opportunities";
|
||||||
|
import { apiResponse } from "../../../modules/api-utils/apiResponse";
|
||||||
|
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||||
|
import { authMiddleware } from "../../middleware/authorization";
|
||||||
|
import { processObjectValuePerms } from "../../../modules/permission-utils/processObjectPermissions";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
const productItemSchema = z
|
||||||
|
.object({
|
||||||
|
catalogItem: z.object({ id: z.number().int().positive() }).optional(),
|
||||||
|
forecastDescription: z.string().optional(),
|
||||||
|
productDescription: z.string().optional(),
|
||||||
|
quantity: z.number().positive().optional(),
|
||||||
|
status: z.object({ id: z.number().int().positive() }).optional(),
|
||||||
|
productClass: z.string().optional(),
|
||||||
|
forecastType: z.string().optional(),
|
||||||
|
revenue: z.number().optional(),
|
||||||
|
cost: z.number().optional(),
|
||||||
|
includeFlag: z.boolean().optional(),
|
||||||
|
linkFlag: z.boolean().optional(),
|
||||||
|
recurringFlag: z.boolean().optional(),
|
||||||
|
taxableFlag: z.boolean().optional(),
|
||||||
|
recurringRevenue: z.number().optional(),
|
||||||
|
recurringCost: z.number().optional(),
|
||||||
|
cycles: z.number().int().min(0).optional(),
|
||||||
|
sequenceNumber: z.number().int().min(0).optional(),
|
||||||
|
})
|
||||||
|
.strict();
|
||||||
|
|
||||||
|
const addProductSchema = z.union([
|
||||||
|
productItemSchema,
|
||||||
|
z.array(productItemSchema).min(1, "At least one product is required"),
|
||||||
|
]);
|
||||||
|
|
||||||
|
/* POST /v1/sales/opportunities/:identifier/products */
|
||||||
|
export default createRoute(
|
||||||
|
"post",
|
||||||
|
["/opportunities/:identifier/products"],
|
||||||
|
async (c) => {
|
||||||
|
const identifier = c.req.param("identifier");
|
||||||
|
const body = await c.req.json();
|
||||||
|
|
||||||
|
const validated = addProductSchema.parse(body);
|
||||||
|
const inputItems = Array.isArray(validated) ? validated : [validated];
|
||||||
|
|
||||||
|
// Gate each submitted field against user permissions.
|
||||||
|
// Only fields the user has permission for are forwarded to ConnectWise.
|
||||||
|
const user = c.get("user");
|
||||||
|
const gatedItems = await Promise.all(
|
||||||
|
inputItems.map((item) =>
|
||||||
|
processObjectValuePerms(item, "sales.opportunity.product.field", user),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const item = await opportunities.fetchItem(identifier);
|
||||||
|
const created = await item.addProducts(gatedItems);
|
||||||
|
|
||||||
|
const isBatch = Array.isArray(body);
|
||||||
|
const response = apiResponse.created(
|
||||||
|
isBatch
|
||||||
|
? `${created.length} product(s) added to opportunity successfully!`
|
||||||
|
: "Product added to opportunity successfully!",
|
||||||
|
isBatch ? created.map((p) => p.toJson()) : created[0]!.toJson(),
|
||||||
|
);
|
||||||
|
return c.json(response, response.status as ContentfulStatusCode);
|
||||||
|
},
|
||||||
|
authMiddleware({ permissions: ["sales.opportunity.product.add"] }),
|
||||||
|
);
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { createRoute } from "../../../modules/api-utils/createRoute";
|
||||||
|
import { opportunities } from "../../../managers/opportunities";
|
||||||
|
import { apiResponse } from "../../../modules/api-utils/apiResponse";
|
||||||
|
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||||
|
import { authMiddleware } from "../../middleware/authorization";
|
||||||
|
|
||||||
|
/* GET /v1/sales/opportunities/:identifier/contacts */
|
||||||
|
export default createRoute(
|
||||||
|
"get",
|
||||||
|
["/opportunities/:identifier/contacts"],
|
||||||
|
async (c) => {
|
||||||
|
const identifier = c.req.param("identifier");
|
||||||
|
const item = await opportunities.fetchItem(identifier);
|
||||||
|
|
||||||
|
const data = await item.fetchContacts();
|
||||||
|
|
||||||
|
const response = apiResponse.successful(
|
||||||
|
"Opportunity contacts fetched successfully!",
|
||||||
|
data,
|
||||||
|
);
|
||||||
|
|
||||||
|
return c.json(response, response.status as ContentfulStatusCode);
|
||||||
|
},
|
||||||
|
authMiddleware({ permissions: ["sales.opportunity.fetch"] }),
|
||||||
|
);
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import { createRoute } from "../../../modules/api-utils/createRoute";
|
||||||
|
import { opportunities } from "../../../managers/opportunities";
|
||||||
|
import { apiResponse } from "../../../modules/api-utils/apiResponse";
|
||||||
|
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||||
|
import { authMiddleware } from "../../middleware/authorization";
|
||||||
|
import { resolveMember } from "../../../modules/cw-utils/members/memberCache";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
/* POST /v1/sales/opportunities/:identifier/notes */
|
||||||
|
export default createRoute(
|
||||||
|
"post",
|
||||||
|
["/opportunities/:identifier/notes"],
|
||||||
|
async (c) => {
|
||||||
|
const identifier = c.req.param("identifier");
|
||||||
|
const body = await c.req.json();
|
||||||
|
|
||||||
|
const schema = z.object({
|
||||||
|
text: z.string().min(1, "Note text is required"),
|
||||||
|
flagged: z.boolean().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = schema.parse(body);
|
||||||
|
|
||||||
|
const item = await opportunities.fetchItem(identifier);
|
||||||
|
const user = c.get("user");
|
||||||
|
|
||||||
|
const created = await item.addNote(data.text, user.login, {
|
||||||
|
flagged: data.flagged,
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = apiResponse.created(
|
||||||
|
"Opportunity note created successfully!",
|
||||||
|
{
|
||||||
|
id: created.id,
|
||||||
|
text: created.text,
|
||||||
|
type: created.type
|
||||||
|
? { id: created.type.id, name: created.type.name }
|
||||||
|
: null,
|
||||||
|
flagged: created.flagged,
|
||||||
|
enteredBy: await resolveMember(created.enteredBy),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return c.json(response, response.status as ContentfulStatusCode);
|
||||||
|
},
|
||||||
|
authMiddleware({ permissions: ["sales.opportunity.note.create"] }),
|
||||||
|
);
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import { createRoute } from "../../../modules/api-utils/createRoute";
|
||||||
|
import { opportunities } from "../../../managers/opportunities";
|
||||||
|
import { apiResponse } from "../../../modules/api-utils/apiResponse";
|
||||||
|
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||||
|
import { authMiddleware } from "../../middleware/authorization";
|
||||||
|
import GenericError from "../../../Errors/GenericError";
|
||||||
|
|
||||||
|
/* DELETE /v1/sales/opportunities/:identifier/notes/:noteId */
|
||||||
|
export default createRoute(
|
||||||
|
"delete",
|
||||||
|
["/opportunities/:identifier/notes/:noteId"],
|
||||||
|
async (c) => {
|
||||||
|
const identifier = c.req.param("identifier");
|
||||||
|
const noteId = Number(c.req.param("noteId"));
|
||||||
|
|
||||||
|
if (isNaN(noteId))
|
||||||
|
throw new GenericError({
|
||||||
|
status: 400,
|
||||||
|
name: "InvalidNoteId",
|
||||||
|
message: "Note ID must be a number",
|
||||||
|
});
|
||||||
|
|
||||||
|
const item = await opportunities.fetchItem(identifier);
|
||||||
|
await item.deleteNote(noteId);
|
||||||
|
|
||||||
|
const response = apiResponse.successful(
|
||||||
|
"Opportunity note deleted successfully!",
|
||||||
|
);
|
||||||
|
|
||||||
|
return c.json(response, response.status as ContentfulStatusCode);
|
||||||
|
},
|
||||||
|
authMiddleware({ permissions: ["sales.opportunity.note.delete"] }),
|
||||||
|
);
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { createRoute } from "../../../modules/api-utils/createRoute";
|
||||||
|
import { opportunities } from "../../../managers/opportunities";
|
||||||
|
import { apiResponse } from "../../../modules/api-utils/apiResponse";
|
||||||
|
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||||
|
import { authMiddleware } from "../../middleware/authorization";
|
||||||
|
import { processObjectValuePerms } from "../../../modules/permission-utils/processObjectPermissions";
|
||||||
|
|
||||||
|
/* GET /v1/sales/opportunities/:identifier */
|
||||||
|
export default createRoute(
|
||||||
|
"get",
|
||||||
|
["/opportunities/:identifier"],
|
||||||
|
async (c) => {
|
||||||
|
const identifier = c.req.param("identifier");
|
||||||
|
|
||||||
|
const item = await opportunities.fetchItem(identifier);
|
||||||
|
|
||||||
|
// Eagerly load site data so toJson() includes full site info
|
||||||
|
await item.fetchSite();
|
||||||
|
|
||||||
|
const gatedData = await processObjectValuePerms(
|
||||||
|
item.toJson(),
|
||||||
|
"obj.opportunity",
|
||||||
|
c.get("user"),
|
||||||
|
);
|
||||||
|
|
||||||
|
const response = apiResponse.successful(
|
||||||
|
"Opportunity fetched successfully!",
|
||||||
|
gatedData,
|
||||||
|
);
|
||||||
|
|
||||||
|
return c.json(response, response.status as ContentfulStatusCode);
|
||||||
|
},
|
||||||
|
authMiddleware({ permissions: ["sales.opportunity.fetch"] }),
|
||||||
|
);
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { createRoute } from "../../../modules/api-utils/createRoute";
|
||||||
|
import { opportunities } from "../../../managers/opportunities";
|
||||||
|
import { apiResponse } from "../../../modules/api-utils/apiResponse";
|
||||||
|
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||||
|
import { authMiddleware } from "../../middleware/authorization";
|
||||||
|
import GenericError from "../../../Errors/GenericError";
|
||||||
|
|
||||||
|
/* GET /v1/sales/opportunities/:identifier/notes/:noteId */
|
||||||
|
export default createRoute(
|
||||||
|
"get",
|
||||||
|
["/opportunities/:identifier/notes/:noteId"],
|
||||||
|
async (c) => {
|
||||||
|
const identifier = c.req.param("identifier");
|
||||||
|
const noteId = Number(c.req.param("noteId"));
|
||||||
|
|
||||||
|
if (isNaN(noteId))
|
||||||
|
throw new GenericError({
|
||||||
|
status: 400,
|
||||||
|
name: "InvalidNoteId",
|
||||||
|
message: "Note ID must be a number",
|
||||||
|
});
|
||||||
|
|
||||||
|
const item = await opportunities.fetchItem(identifier);
|
||||||
|
const data = await item.fetchNote(noteId);
|
||||||
|
|
||||||
|
const response = apiResponse.successful(
|
||||||
|
"Opportunity note fetched successfully!",
|
||||||
|
data,
|
||||||
|
);
|
||||||
|
|
||||||
|
return c.json(response, response.status as ContentfulStatusCode);
|
||||||
|
},
|
||||||
|
authMiddleware({ permissions: ["sales.opportunity.fetch"] }),
|
||||||
|
);
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { createRoute } from "../../../modules/api-utils/createRoute";
|
||||||
|
import { opportunities } from "../../../managers/opportunities";
|
||||||
|
import { apiResponse } from "../../../modules/api-utils/apiResponse";
|
||||||
|
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||||
|
import { authMiddleware } from "../../middleware/authorization";
|
||||||
|
|
||||||
|
/* GET /v1/sales/opportunities/:identifier/notes */
|
||||||
|
export default createRoute(
|
||||||
|
"get",
|
||||||
|
["/opportunities/:identifier/notes"],
|
||||||
|
async (c) => {
|
||||||
|
const identifier = c.req.param("identifier");
|
||||||
|
const item = await opportunities.fetchItem(identifier);
|
||||||
|
|
||||||
|
const data = await item.fetchNotes();
|
||||||
|
|
||||||
|
const response = apiResponse.successful(
|
||||||
|
"Opportunity notes fetched successfully!",
|
||||||
|
data,
|
||||||
|
);
|
||||||
|
|
||||||
|
return c.json(response, response.status as ContentfulStatusCode);
|
||||||
|
},
|
||||||
|
authMiddleware({ permissions: ["sales.opportunity.fetch"] }),
|
||||||
|
);
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { createRoute } from "../../../modules/api-utils/createRoute";
|
||||||
|
import { opportunities } from "../../../managers/opportunities";
|
||||||
|
import { apiResponse } from "../../../modules/api-utils/apiResponse";
|
||||||
|
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||||
|
import { authMiddleware } from "../../middleware/authorization";
|
||||||
|
|
||||||
|
/* GET /v1/sales/opportunities/:identifier/products */
|
||||||
|
export default createRoute(
|
||||||
|
"get",
|
||||||
|
["/opportunities/:identifier/products"],
|
||||||
|
async (c) => {
|
||||||
|
const identifier = c.req.param("identifier");
|
||||||
|
const item = await opportunities.fetchItem(identifier);
|
||||||
|
|
||||||
|
const data = await item.fetchProducts();
|
||||||
|
|
||||||
|
const response = apiResponse.successful(
|
||||||
|
"Opportunity products fetched successfully!",
|
||||||
|
data.map((p) => p.toJson()),
|
||||||
|
);
|
||||||
|
|
||||||
|
return c.json(response, response.status as ContentfulStatusCode);
|
||||||
|
},
|
||||||
|
authMiddleware({ permissions: ["sales.opportunity.fetch"] }),
|
||||||
|
);
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { createRoute } from "../../../modules/api-utils/createRoute";
|
||||||
|
import { opportunities } from "../../../managers/opportunities";
|
||||||
|
import { apiResponse } from "../../../modules/api-utils/apiResponse";
|
||||||
|
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||||
|
import { authMiddleware } from "../../middleware/authorization";
|
||||||
|
|
||||||
|
/* POST /v1/sales/opportunities/:identifier/refresh */
|
||||||
|
export default createRoute(
|
||||||
|
"post",
|
||||||
|
["/opportunities/:identifier/refresh"],
|
||||||
|
async (c) => {
|
||||||
|
const identifier = c.req.param("identifier");
|
||||||
|
const item = await opportunities.fetchItem(identifier);
|
||||||
|
|
||||||
|
const refreshed = await item.refreshFromCW();
|
||||||
|
|
||||||
|
const response = apiResponse.successful(
|
||||||
|
"Opportunity refreshed from ConnectWise successfully!",
|
||||||
|
refreshed.toJson(),
|
||||||
|
);
|
||||||
|
|
||||||
|
return c.json(response, response.status as ContentfulStatusCode);
|
||||||
|
},
|
||||||
|
authMiddleware({ permissions: ["sales.opportunity.refresh"] }),
|
||||||
|
);
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import { createRoute } from "../../../modules/api-utils/createRoute";
|
||||||
|
import { opportunities } from "../../../managers/opportunities";
|
||||||
|
import { apiResponse } from "../../../modules/api-utils/apiResponse";
|
||||||
|
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||||
|
import { authMiddleware } from "../../middleware/authorization";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
/* PATCH /v1/sales/opportunities/:identifier/products/sequence */
|
||||||
|
export default createRoute(
|
||||||
|
"patch",
|
||||||
|
["/opportunities/:identifier/products/sequence"],
|
||||||
|
async (c) => {
|
||||||
|
const identifier = c.req.param("identifier");
|
||||||
|
const body = await c.req.json();
|
||||||
|
|
||||||
|
const schema = z.object({
|
||||||
|
orderedIds: z
|
||||||
|
.array(z.number().int().positive())
|
||||||
|
.min(1, "At least one forecast item ID is required"),
|
||||||
|
});
|
||||||
|
|
||||||
|
const { orderedIds } = schema.parse(body);
|
||||||
|
|
||||||
|
const item = await opportunities.fetchItem(identifier);
|
||||||
|
const updated = await item.resequenceProducts(orderedIds);
|
||||||
|
|
||||||
|
const response = apiResponse.successful(
|
||||||
|
"Product sequence updated successfully!",
|
||||||
|
{
|
||||||
|
products: updated.map((p) => p.toJson()),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return c.json(response, response.status as ContentfulStatusCode);
|
||||||
|
},
|
||||||
|
authMiddleware({ permissions: ["sales.opportunity.product.update"] }),
|
||||||
|
);
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import { createRoute } from "../../../modules/api-utils/createRoute";
|
||||||
|
import { opportunities } from "../../../managers/opportunities";
|
||||||
|
import { apiResponse } from "../../../modules/api-utils/apiResponse";
|
||||||
|
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||||
|
import { authMiddleware } from "../../middleware/authorization";
|
||||||
|
import GenericError from "../../../Errors/GenericError";
|
||||||
|
import { resolveMember } from "../../../modules/cw-utils/members/memberCache";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
/* PATCH /v1/sales/opportunities/:identifier/notes/:noteId */
|
||||||
|
export default createRoute(
|
||||||
|
"patch",
|
||||||
|
["/opportunities/:identifier/notes/:noteId"],
|
||||||
|
async (c) => {
|
||||||
|
const identifier = c.req.param("identifier");
|
||||||
|
const noteId = Number(c.req.param("noteId"));
|
||||||
|
|
||||||
|
if (isNaN(noteId))
|
||||||
|
throw new GenericError({
|
||||||
|
status: 400,
|
||||||
|
name: "InvalidNoteId",
|
||||||
|
message: "Note ID must be a number",
|
||||||
|
});
|
||||||
|
|
||||||
|
const body = await c.req.json();
|
||||||
|
|
||||||
|
const schema = z
|
||||||
|
.object({
|
||||||
|
text: z.string().min(1).optional(),
|
||||||
|
flagged: z.boolean().optional(),
|
||||||
|
})
|
||||||
|
.refine((d) => d.text !== undefined || d.flagged !== undefined, {
|
||||||
|
message: "At least one of 'text' or 'flagged' must be provided",
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = schema.parse(body);
|
||||||
|
|
||||||
|
const item = await opportunities.fetchItem(identifier);
|
||||||
|
const updated = await item.updateNote(noteId, data);
|
||||||
|
|
||||||
|
const response = apiResponse.successful(
|
||||||
|
"Opportunity note updated successfully!",
|
||||||
|
{
|
||||||
|
id: updated.id,
|
||||||
|
text: updated.text,
|
||||||
|
type: updated.type
|
||||||
|
? { id: updated.type.id, name: updated.type.name }
|
||||||
|
: null,
|
||||||
|
flagged: updated.flagged,
|
||||||
|
enteredBy: await resolveMember(updated.enteredBy),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return c.json(response, response.status as ContentfulStatusCode);
|
||||||
|
},
|
||||||
|
authMiddleware({ permissions: ["sales.opportunity.note.update"] }),
|
||||||
|
);
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { createRoute } from "../../modules/api-utils/createRoute";
|
||||||
|
import { opportunities } from "../../managers/opportunities";
|
||||||
|
import { apiResponse } from "../../modules/api-utils/apiResponse";
|
||||||
|
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||||
|
import { authMiddleware } from "../middleware/authorization";
|
||||||
|
|
||||||
|
/* GET /v1/sales/opportunities/count */
|
||||||
|
export default createRoute(
|
||||||
|
"get",
|
||||||
|
["/opportunities/count"],
|
||||||
|
async (c) => {
|
||||||
|
const openOnly = c.req.query("openOnly") === "true";
|
||||||
|
|
||||||
|
const count = await opportunities.count({ openOnly });
|
||||||
|
|
||||||
|
const response = apiResponse.successful(
|
||||||
|
"Opportunity count fetched successfully!",
|
||||||
|
{ count },
|
||||||
|
);
|
||||||
|
|
||||||
|
return c.json(response, response.status as ContentfulStatusCode);
|
||||||
|
},
|
||||||
|
authMiddleware({ permissions: ["sales.opportunity.fetch.many"] }),
|
||||||
|
);
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import { createRoute } from "../../modules/api-utils/createRoute";
|
||||||
|
import { opportunities } from "../../managers/opportunities";
|
||||||
|
import { apiResponse } from "../../modules/api-utils/apiResponse";
|
||||||
|
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||||
|
import { authMiddleware } from "../middleware/authorization";
|
||||||
|
import { processObjectValuePerms } from "../../modules/permission-utils/processObjectPermissions";
|
||||||
|
|
||||||
|
/* GET /v1/sales/opportunities */
|
||||||
|
export default createRoute(
|
||||||
|
"get",
|
||||||
|
["/opportunities"],
|
||||||
|
async (c) => {
|
||||||
|
const page = Number(c.req.query("page") ?? 1);
|
||||||
|
const rpp = Number(c.req.query("rpp") ?? 30);
|
||||||
|
const search = c.req.query("search") as string;
|
||||||
|
const includeClosed = c.req.query("includeClosed") === "true";
|
||||||
|
|
||||||
|
const data = search
|
||||||
|
? await opportunities.search(search, page, rpp, { includeClosed })
|
||||||
|
: await opportunities.fetchPages(page, rpp, { includeClosed });
|
||||||
|
|
||||||
|
const totalRecords = search
|
||||||
|
? await opportunities.searchCount(search, { includeClosed })
|
||||||
|
: await opportunities.count({ openOnly: !includeClosed });
|
||||||
|
|
||||||
|
const gatedData = await Promise.all(
|
||||||
|
data.map((item) =>
|
||||||
|
processObjectValuePerms(
|
||||||
|
item.toJson(),
|
||||||
|
"obj.opportunity",
|
||||||
|
c.get("user"),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const response = apiResponse.successful(
|
||||||
|
"Opportunities fetched successfully!",
|
||||||
|
gatedData,
|
||||||
|
{
|
||||||
|
pagination: {
|
||||||
|
previousPage: page <= 1 ? null : page - 1,
|
||||||
|
currentPage: page,
|
||||||
|
nextPage: page >= totalRecords / rpp ? null : page + 1,
|
||||||
|
totalPages: Math.ceil(totalRecords / rpp),
|
||||||
|
totalRecords,
|
||||||
|
listedRecords: rpp,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return c.json(response, response.status as ContentfulStatusCode);
|
||||||
|
},
|
||||||
|
authMiddleware({ permissions: ["sales.opportunity.fetch.many"] }),
|
||||||
|
);
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { createRoute } from "../../modules/api-utils/createRoute";
|
||||||
|
import { apiResponse } from "../../modules/api-utils/apiResponse";
|
||||||
|
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||||
|
import { authMiddleware } from "../middleware/authorization";
|
||||||
|
import { QUOTE_STATUSES } from "../../types/QuoteStatuses";
|
||||||
|
|
||||||
|
/* GET /v1/sales/opportunity-types */
|
||||||
|
export default createRoute(
|
||||||
|
"get",
|
||||||
|
["/opportunity-types"],
|
||||||
|
|
||||||
|
async (c) => {
|
||||||
|
const response = apiResponse.successful(
|
||||||
|
"Opportunity Types Fetched Successfully!",
|
||||||
|
QUOTE_STATUSES,
|
||||||
|
);
|
||||||
|
return c.json(response, response.status as ContentfulStatusCode);
|
||||||
|
},
|
||||||
|
authMiddleware({ permissions: ["sales.opportunity.fetch.many"] }),
|
||||||
|
);
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { default as fetchAll } from "./fetchAll";
|
||||||
|
import { default as fetchOpportunityTypes } from "./fetchOpportunityTypes";
|
||||||
|
import { default as count } from "./count";
|
||||||
|
import { default as fetch } from "./[id]/fetch";
|
||||||
|
import { default as refresh } from "./[id]/refresh";
|
||||||
|
import { default as products } from "./[id]/products";
|
||||||
|
import { default as addProduct } from "./[id]/addProduct";
|
||||||
|
import { default as resequenceProducts } from "./[id]/resequenceProducts";
|
||||||
|
import { default as notes } from "./[id]/notes";
|
||||||
|
import { default as fetchNote } from "./[id]/fetchNote";
|
||||||
|
import { default as createNote } from "./[id]/createNote";
|
||||||
|
import { default as updateNote } from "./[id]/updateNote";
|
||||||
|
import { default as deleteNote } from "./[id]/deleteNote";
|
||||||
|
import { default as contacts } from "./[id]/contacts";
|
||||||
|
|
||||||
|
export {
|
||||||
|
addProduct,
|
||||||
|
count,
|
||||||
|
fetch,
|
||||||
|
fetchAll,
|
||||||
|
fetchOpportunityTypes,
|
||||||
|
products,
|
||||||
|
resequenceProducts,
|
||||||
|
notes,
|
||||||
|
fetchNote,
|
||||||
|
createNote,
|
||||||
|
updateNote,
|
||||||
|
deleteNote,
|
||||||
|
contacts,
|
||||||
|
refresh,
|
||||||
|
};
|
||||||
@@ -55,6 +55,8 @@ v1.route("/credential-type", require("./routers/credentialTypeRouter").default);
|
|||||||
v1.route("/role", require("./routers/roleRouter").default);
|
v1.route("/role", require("./routers/roleRouter").default);
|
||||||
v1.route("/permissions", require("./routers/permissionRouter").default);
|
v1.route("/permissions", require("./routers/permissionRouter").default);
|
||||||
v1.route("/unifi", require("./routers/unifiRouter").default);
|
v1.route("/unifi", require("./routers/unifiRouter").default);
|
||||||
|
v1.route("/procurement", require("./routers/procurementRouter").default);
|
||||||
|
v1.route("/sales", require("./routers/salesRouter").default);
|
||||||
app.route("/v1", v1);
|
app.route("/v1", v1);
|
||||||
|
|
||||||
export default app;
|
export default app;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { unifiSites } from "../../../managers/unifiSites";
|
|||||||
import { apiResponse } from "../../../modules/api-utils/apiResponse";
|
import { apiResponse } from "../../../modules/api-utils/apiResponse";
|
||||||
import { ContentfulStatusCode } from "hono/utils/http-status";
|
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||||
import { authMiddleware } from "../../middleware/authorization";
|
import { authMiddleware } from "../../middleware/authorization";
|
||||||
|
import { processObjectValuePerms } from "../../../modules/permission-utils/processObjectPermissions";
|
||||||
|
|
||||||
/* GET /v1/unifi/site/:id */
|
/* GET /v1/unifi/site/:id */
|
||||||
export default createRoute(
|
export default createRoute(
|
||||||
@@ -10,9 +11,16 @@ export default createRoute(
|
|||||||
["/site/:id"],
|
["/site/:id"],
|
||||||
async (c) => {
|
async (c) => {
|
||||||
const site = await unifiSites.fetch(c.req.param("id"));
|
const site = await unifiSites.fetch(c.req.param("id"));
|
||||||
|
|
||||||
|
const gatedData = await processObjectValuePerms(
|
||||||
|
site,
|
||||||
|
"obj.unifiSite",
|
||||||
|
c.get("user"),
|
||||||
|
);
|
||||||
|
|
||||||
const response = apiResponse.successful(
|
const response = apiResponse.successful(
|
||||||
"UniFi Site Fetched Successfully!",
|
"UniFi Site Fetched Successfully!",
|
||||||
site,
|
gatedData,
|
||||||
);
|
);
|
||||||
return c.json(response, response.status as ContentfulStatusCode);
|
return c.json(response, response.status as ContentfulStatusCode);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { unifiSites } from "../../../managers/unifiSites";
|
|||||||
import { apiResponse } from "../../../modules/api-utils/apiResponse";
|
import { apiResponse } from "../../../modules/api-utils/apiResponse";
|
||||||
import { ContentfulStatusCode } from "hono/utils/http-status";
|
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||||
import { authMiddleware } from "../../middleware/authorization";
|
import { authMiddleware } from "../../middleware/authorization";
|
||||||
|
import { processObjectValuePerms } from "../../../modules/permission-utils/processObjectPermissions";
|
||||||
|
|
||||||
/* GET /v1/unifi/sites */
|
/* GET /v1/unifi/sites */
|
||||||
export default createRoute(
|
export default createRoute(
|
||||||
@@ -10,9 +11,16 @@ export default createRoute(
|
|||||||
["/sites"],
|
["/sites"],
|
||||||
async (c) => {
|
async (c) => {
|
||||||
const sites = await unifiSites.fetchAll();
|
const sites = await unifiSites.fetchAll();
|
||||||
|
|
||||||
|
const gatedData = await Promise.all(
|
||||||
|
sites.map((site) =>
|
||||||
|
processObjectValuePerms(site, "obj.unifiSite", c.get("user")),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
const response = apiResponse.successful(
|
const response = apiResponse.successful(
|
||||||
"UniFi Sites Fetched Successfully!",
|
"UniFi Sites Fetched Successfully!",
|
||||||
sites,
|
gatedData,
|
||||||
);
|
);
|
||||||
return c.json(response, response.status as ContentfulStatusCode);
|
return c.json(response, response.status as ContentfulStatusCode);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2,16 +2,20 @@ import { ContentfulStatusCode } from "hono/utils/http-status";
|
|||||||
import { apiResponse } from "../../../modules/api-utils/apiResponse";
|
import { apiResponse } from "../../../modules/api-utils/apiResponse";
|
||||||
import { createRoute } from "../../../modules/api-utils/createRoute";
|
import { createRoute } from "../../../modules/api-utils/createRoute";
|
||||||
import { authMiddleware } from "../../middleware/authorization";
|
import { authMiddleware } from "../../middleware/authorization";
|
||||||
|
import { processObjectValuePerms } from "../../../modules/permission-utils/processObjectPermissions";
|
||||||
|
|
||||||
// /v1/user/@me
|
// /v1/user/@me
|
||||||
export default createRoute(
|
export default createRoute(
|
||||||
"get",
|
"get",
|
||||||
["/@me"],
|
["/@me"],
|
||||||
(c) => {
|
async (c) => {
|
||||||
const response = apiResponse.successful(
|
const gatedData = await processObjectValuePerms(
|
||||||
"Fetched user.",
|
|
||||||
c.get("user")?.toJson(),
|
c.get("user")?.toJson(),
|
||||||
|
"obj.user",
|
||||||
|
c.get("user"),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const response = apiResponse.successful("Fetched user.", gatedData);
|
||||||
return c.json(response, response.status as ContentfulStatusCode);
|
return c.json(response, response.status as ContentfulStatusCode);
|
||||||
},
|
},
|
||||||
authMiddleware({ scopes: ["user.read"] }),
|
authMiddleware({ scopes: ["user.read"] }),
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { createRoute } from "../../modules/api-utils/createRoute";
|
|||||||
import { authMiddleware } from "../middleware/authorization";
|
import { authMiddleware } from "../middleware/authorization";
|
||||||
import { users } from "../../managers/users";
|
import { users } from "../../managers/users";
|
||||||
import GenericError from "../../Errors/GenericError";
|
import GenericError from "../../Errors/GenericError";
|
||||||
|
import { processObjectValuePerms } from "../../modules/permission-utils/processObjectPermissions";
|
||||||
|
|
||||||
/* GET /v1/user/users/:identifier */
|
/* GET /v1/user/users/:identifier */
|
||||||
export default createRoute(
|
export default createRoute(
|
||||||
@@ -21,9 +22,15 @@ export default createRoute(
|
|||||||
status: 404,
|
status: 404,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const gatedData = await processObjectValuePerms(
|
||||||
|
user.toJson(),
|
||||||
|
"obj.user",
|
||||||
|
c.get("user"),
|
||||||
|
);
|
||||||
|
|
||||||
const response = apiResponse.successful(
|
const response = apiResponse.successful(
|
||||||
"User Fetched Successfully!",
|
"User Fetched Successfully!",
|
||||||
user.toJson(),
|
gatedData,
|
||||||
);
|
);
|
||||||
return c.json(response, response.status as ContentfulStatusCode);
|
return c.json(response, response.status as ContentfulStatusCode);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { apiResponse } from "../../modules/api-utils/apiResponse";
|
|||||||
import { createRoute } from "../../modules/api-utils/createRoute";
|
import { createRoute } from "../../modules/api-utils/createRoute";
|
||||||
import { authMiddleware } from "../middleware/authorization";
|
import { authMiddleware } from "../middleware/authorization";
|
||||||
import { users } from "../../managers/users";
|
import { users } from "../../managers/users";
|
||||||
|
import { processObjectValuePerms } from "../../modules/permission-utils/processObjectPermissions";
|
||||||
|
|
||||||
/* GET /v1/user/users */
|
/* GET /v1/user/users */
|
||||||
export default createRoute(
|
export default createRoute(
|
||||||
@@ -11,11 +12,16 @@ export default createRoute(
|
|||||||
|
|
||||||
async (c) => {
|
async (c) => {
|
||||||
const allUsers = await users.fetchAllUsers();
|
const allUsers = await users.fetchAllUsers();
|
||||||
const usersArray = allUsers.map((u) => u.toJson());
|
|
||||||
|
const gatedData = await Promise.all(
|
||||||
|
allUsers.map((u) =>
|
||||||
|
processObjectValuePerms(u.toJson(), "obj.user", c.get("user")),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
const response = apiResponse.successful(
|
const response = apiResponse.successful(
|
||||||
"Users Fetched Successfully!",
|
"Users Fetched Successfully!",
|
||||||
usersArray,
|
gatedData,
|
||||||
);
|
);
|
||||||
return c.json(response, response.status as ContentfulStatusCode);
|
return c.json(response, response.status as ContentfulStatusCode);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { createRoute } from "../../modules/api-utils/createRoute";
|
|||||||
import { authMiddleware } from "../middleware/authorization";
|
import { authMiddleware } from "../middleware/authorization";
|
||||||
import { users } from "../../managers/users";
|
import { users } from "../../managers/users";
|
||||||
import GenericError from "../../Errors/GenericError";
|
import GenericError from "../../Errors/GenericError";
|
||||||
|
import { processObjectValuePerms } from "../../modules/permission-utils/processObjectPermissions";
|
||||||
|
|
||||||
/* GET /v1/user/users/:identifier/roles */
|
/* GET /v1/user/users/:identifier/roles */
|
||||||
export default createRoute(
|
export default createRoute(
|
||||||
@@ -22,11 +23,20 @@ export default createRoute(
|
|||||||
});
|
});
|
||||||
|
|
||||||
const roles = await user.fetchRoles();
|
const roles = await user.fetchRoles();
|
||||||
const rolesArray = roles.map((r) => r.toJson({ viewPermissions: true }));
|
|
||||||
|
const gatedData = await Promise.all(
|
||||||
|
roles.map((r) =>
|
||||||
|
processObjectValuePerms(
|
||||||
|
r.toJson({ viewPermissions: true }),
|
||||||
|
"obj.role",
|
||||||
|
c.get("user"),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
const response = apiResponse.successful(
|
const response = apiResponse.successful(
|
||||||
"User Roles Fetched Successfully!",
|
"User Roles Fetched Successfully!",
|
||||||
rolesArray,
|
gatedData,
|
||||||
);
|
);
|
||||||
return c.json(response, response.status as ContentfulStatusCode);
|
return c.json(response, response.status as ContentfulStatusCode);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { Server } from "socket.io";
|
|||||||
import { Server as Engine } from "@socket.io/bun-engine";
|
import { Server as Engine } from "@socket.io/bun-engine";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { UnifiClient } from "./modules/unifi-api/UnifiClient";
|
import { UnifiClient } from "./modules/unifi-api/UnifiClient";
|
||||||
|
import Redis from "ioredis";
|
||||||
|
|
||||||
const connectionString = `${process.env.DATABASE_URL}`;
|
const connectionString = `${process.env.DATABASE_URL}`;
|
||||||
const adapter = new PrismaPg({ connectionString });
|
const adapter = new PrismaPg({ connectionString });
|
||||||
@@ -22,6 +23,10 @@ export const API_BASE_URL =
|
|||||||
|
|
||||||
export const prisma = new PrismaClient({ adapter });
|
export const prisma = new PrismaClient({ adapter });
|
||||||
|
|
||||||
|
// Redis Client
|
||||||
|
|
||||||
|
export const redis = new Redis(process.env.REDIS_URL!);
|
||||||
|
|
||||||
export const sessionDuration = 30 * 24 * 60 * 60000;
|
export const sessionDuration = 30 * 24 * 60 * 60000;
|
||||||
export const accessTokenDuration = "10min";
|
export const accessTokenDuration = "10min";
|
||||||
export const refreshTokenDuration = "30d";
|
export const refreshTokenDuration = "30d";
|
||||||
|
|||||||
@@ -0,0 +1,252 @@
|
|||||||
|
import {
|
||||||
|
CWActivity,
|
||||||
|
CWActivityCustomField,
|
||||||
|
CWPatchOperation,
|
||||||
|
CWCreateActivity,
|
||||||
|
} from "../modules/cw-utils/activities/activity.types";
|
||||||
|
import { activityCw } from "../modules/cw-utils/activities/activities";
|
||||||
|
import { fetchActivity } from "../modules/cw-utils/activities/fetchActivity";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Activity Controller
|
||||||
|
*
|
||||||
|
* Domain model class that encapsulates a ConnectWise Activity entity.
|
||||||
|
* Activities are not persisted locally — all data is sourced directly
|
||||||
|
* from the ConnectWise API.
|
||||||
|
*/
|
||||||
|
export class ActivityController {
|
||||||
|
public readonly cwActivityId: number;
|
||||||
|
public name: string;
|
||||||
|
public notes: string | null;
|
||||||
|
|
||||||
|
public typeName: string | null;
|
||||||
|
public typeCwId: number | null;
|
||||||
|
public statusName: string | null;
|
||||||
|
public statusCwId: number | null;
|
||||||
|
|
||||||
|
public companyCwId: number | null;
|
||||||
|
public companyName: string | null;
|
||||||
|
public companyIdentifier: string | null;
|
||||||
|
public contactCwId: number | null;
|
||||||
|
public contactName: string | null;
|
||||||
|
|
||||||
|
public phoneNumber: string | null;
|
||||||
|
public email: string | null;
|
||||||
|
|
||||||
|
public opportunityCwId: number | null;
|
||||||
|
public opportunityName: string | null;
|
||||||
|
public ticketCwId: number | null;
|
||||||
|
public ticketName: string | null;
|
||||||
|
public agreementCwId: number | null;
|
||||||
|
public agreementName: string | null;
|
||||||
|
public campaignCwId: number | null;
|
||||||
|
public campaignName: string | null;
|
||||||
|
|
||||||
|
public assignToCwId: number | null;
|
||||||
|
public assignToName: string | null;
|
||||||
|
public assignToIdentifier: string | null;
|
||||||
|
|
||||||
|
public scheduleStatusCwId: number | null;
|
||||||
|
public scheduleStatusName: string | null;
|
||||||
|
public reminderCwId: number | null;
|
||||||
|
public reminderName: string | null;
|
||||||
|
public whereCwId: number | null;
|
||||||
|
public whereName: string | null;
|
||||||
|
|
||||||
|
public dateStart: Date | null;
|
||||||
|
public dateEnd: Date | null;
|
||||||
|
public notifyFlag: boolean;
|
||||||
|
|
||||||
|
public currencyCwId: number | null;
|
||||||
|
public currencyName: string | null;
|
||||||
|
|
||||||
|
public mobileGuid: string | null;
|
||||||
|
public customFields: CWActivityCustomField[];
|
||||||
|
|
||||||
|
public cwLastUpdated: Date | null;
|
||||||
|
public cwDateEntered: Date | null;
|
||||||
|
public cwEnteredBy: string | null;
|
||||||
|
public cwUpdatedBy: string | null;
|
||||||
|
|
||||||
|
constructor(data: CWActivity) {
|
||||||
|
this.cwActivityId = data.id;
|
||||||
|
this.name = data.name;
|
||||||
|
this.notes = data.notes ?? null;
|
||||||
|
|
||||||
|
this.typeName = data.type?.name ?? null;
|
||||||
|
this.typeCwId = data.type?.id ?? null;
|
||||||
|
this.statusName = data.status?.name ?? null;
|
||||||
|
this.statusCwId = data.status?.id ?? null;
|
||||||
|
|
||||||
|
this.companyCwId = data.company?.id ?? null;
|
||||||
|
this.companyName = data.company?.name ?? null;
|
||||||
|
this.companyIdentifier = data.company?.identifier ?? null;
|
||||||
|
this.contactCwId = data.contact?.id ?? null;
|
||||||
|
this.contactName = data.contact?.name ?? null;
|
||||||
|
|
||||||
|
this.phoneNumber = data.phoneNumber ?? null;
|
||||||
|
this.email = data.email ?? null;
|
||||||
|
|
||||||
|
this.opportunityCwId = data.opportunity?.id ?? null;
|
||||||
|
this.opportunityName = data.opportunity?.name ?? null;
|
||||||
|
this.ticketCwId = data.ticket?.id ?? null;
|
||||||
|
this.ticketName = data.ticket?.name ?? null;
|
||||||
|
this.agreementCwId = data.agreement?.id ?? null;
|
||||||
|
this.agreementName = data.agreement?.name ?? null;
|
||||||
|
this.campaignCwId = data.campaign?.id ?? null;
|
||||||
|
this.campaignName = data.campaign?.name ?? null;
|
||||||
|
|
||||||
|
this.assignToCwId = data.assignTo?.id ?? null;
|
||||||
|
this.assignToName = data.assignTo?.name ?? null;
|
||||||
|
this.assignToIdentifier = data.assignTo?.identifier ?? null;
|
||||||
|
|
||||||
|
this.scheduleStatusCwId = data.scheduleStatus?.id ?? null;
|
||||||
|
this.scheduleStatusName = data.scheduleStatus?.name ?? null;
|
||||||
|
this.reminderCwId = data.reminder?.id ?? null;
|
||||||
|
this.reminderName = data.reminder?.name ?? null;
|
||||||
|
this.whereCwId = data.where?.id ?? null;
|
||||||
|
this.whereName = data.where?.name ?? null;
|
||||||
|
|
||||||
|
this.dateStart = data.dateStart ? new Date(data.dateStart) : null;
|
||||||
|
this.dateEnd = data.dateEnd ? new Date(data.dateEnd) : null;
|
||||||
|
this.notifyFlag = data.notifyFlag ?? false;
|
||||||
|
|
||||||
|
this.currencyCwId = data.currency?.id ?? null;
|
||||||
|
this.currencyName = data.currency?.name ?? null;
|
||||||
|
|
||||||
|
this.mobileGuid = data.mobileGuid ?? null;
|
||||||
|
this.customFields = data.customFields ?? [];
|
||||||
|
|
||||||
|
this.cwLastUpdated = data._info?.lastUpdated
|
||||||
|
? new Date(data._info.lastUpdated)
|
||||||
|
: null;
|
||||||
|
this.cwDateEntered = data._info?.dateEntered
|
||||||
|
? new Date(data._info.dateEntered)
|
||||||
|
: null;
|
||||||
|
this.cwEnteredBy = data._info?.enteredBy ?? null;
|
||||||
|
this.cwUpdatedBy = data._info?.updatedBy ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refresh from ConnectWise
|
||||||
|
*
|
||||||
|
* Fetches the latest activity data from CW and returns
|
||||||
|
* a new ActivityController instance with updated state.
|
||||||
|
*/
|
||||||
|
public async refreshFromCW(): Promise<ActivityController> {
|
||||||
|
const cwData = await fetchActivity(this.cwActivityId);
|
||||||
|
return new ActivityController(cwData);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch raw CW data
|
||||||
|
*
|
||||||
|
* Returns the raw ConnectWise activity object.
|
||||||
|
*/
|
||||||
|
public async fetchCwData(): Promise<CWActivity> {
|
||||||
|
return fetchActivity(this.cwActivityId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update in ConnectWise
|
||||||
|
*
|
||||||
|
* Applies JSON Patch operations to this activity in ConnectWise
|
||||||
|
* and returns a new controller with the updated data.
|
||||||
|
*/
|
||||||
|
public async update(
|
||||||
|
operations: CWPatchOperation[],
|
||||||
|
): Promise<ActivityController> {
|
||||||
|
const updated = await activityCw.update(this.cwActivityId, operations);
|
||||||
|
return new ActivityController(updated);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete from ConnectWise
|
||||||
|
*
|
||||||
|
* Deletes this activity in ConnectWise.
|
||||||
|
*/
|
||||||
|
public async delete(): Promise<void> {
|
||||||
|
await activityCw.delete(this.cwActivityId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create Activity (static factory)
|
||||||
|
*
|
||||||
|
* Creates a new activity in ConnectWise and returns a controller instance.
|
||||||
|
*/
|
||||||
|
public static async create(
|
||||||
|
data: CWCreateActivity,
|
||||||
|
): Promise<ActivityController> {
|
||||||
|
const created = await activityCw.create(data);
|
||||||
|
return new ActivityController(created);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* To JSON
|
||||||
|
*
|
||||||
|
* Serializes the activity into a safe, API-friendly object.
|
||||||
|
*/
|
||||||
|
public toJson(): Record<string, any> {
|
||||||
|
return {
|
||||||
|
cwActivityId: this.cwActivityId,
|
||||||
|
name: this.name,
|
||||||
|
notes: this.notes,
|
||||||
|
type: this.typeCwId ? { id: this.typeCwId, name: this.typeName } : null,
|
||||||
|
status: this.statusCwId
|
||||||
|
? { id: this.statusCwId, name: this.statusName }
|
||||||
|
: null,
|
||||||
|
company: this.companyCwId
|
||||||
|
? {
|
||||||
|
id: this.companyCwId,
|
||||||
|
identifier: this.companyIdentifier,
|
||||||
|
name: this.companyName,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
contact: this.contactCwId
|
||||||
|
? { id: this.contactCwId, name: this.contactName }
|
||||||
|
: null,
|
||||||
|
phoneNumber: this.phoneNumber,
|
||||||
|
email: this.email,
|
||||||
|
opportunity: this.opportunityCwId
|
||||||
|
? { id: this.opportunityCwId, name: this.opportunityName }
|
||||||
|
: null,
|
||||||
|
ticket: this.ticketCwId
|
||||||
|
? { id: this.ticketCwId, name: this.ticketName }
|
||||||
|
: null,
|
||||||
|
agreement: this.agreementCwId
|
||||||
|
? { id: this.agreementCwId, name: this.agreementName }
|
||||||
|
: null,
|
||||||
|
campaign: this.campaignCwId
|
||||||
|
? { id: this.campaignCwId, name: this.campaignName }
|
||||||
|
: null,
|
||||||
|
assignTo: this.assignToCwId
|
||||||
|
? {
|
||||||
|
id: this.assignToCwId,
|
||||||
|
identifier: this.assignToIdentifier,
|
||||||
|
name: this.assignToName,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
scheduleStatus: this.scheduleStatusCwId
|
||||||
|
? { id: this.scheduleStatusCwId, name: this.scheduleStatusName }
|
||||||
|
: null,
|
||||||
|
reminder: this.reminderCwId
|
||||||
|
? { id: this.reminderCwId, name: this.reminderName }
|
||||||
|
: null,
|
||||||
|
where: this.whereCwId
|
||||||
|
? { id: this.whereCwId, name: this.whereName }
|
||||||
|
: null,
|
||||||
|
dateStart: this.dateStart,
|
||||||
|
dateEnd: this.dateEnd,
|
||||||
|
notifyFlag: this.notifyFlag,
|
||||||
|
currency: this.currencyCwId
|
||||||
|
? { id: this.currencyCwId, name: this.currencyName }
|
||||||
|
: null,
|
||||||
|
mobileGuid: this.mobileGuid,
|
||||||
|
customFields: this.customFields,
|
||||||
|
cwLastUpdated: this.cwLastUpdated,
|
||||||
|
cwDateEntered: this.cwDateEntered,
|
||||||
|
cwEnteredBy: this.cwEnteredBy,
|
||||||
|
cwUpdatedBy: this.cwUpdatedBy,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,231 @@
|
|||||||
|
import { CatalogItem } from "../../generated/prisma/client";
|
||||||
|
import { prisma } from "../constants";
|
||||||
|
import { catalogCw } from "../modules/cw-utils/procurement/catalog";
|
||||||
|
import { CatalogItem as CWCatalogItem } from "../modules/cw-utils/procurement/catalog.types";
|
||||||
|
import GenericError from "../Errors/GenericError";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Catalog Item Controller
|
||||||
|
*
|
||||||
|
* This class encapsulates a catalog item entity and provides domain methods
|
||||||
|
* for accessing, refreshing, and serializing catalog item data. It bridges
|
||||||
|
* the internal database representation with ConnectWise catalog data.
|
||||||
|
*/
|
||||||
|
export class CatalogItemController {
|
||||||
|
public readonly id: string;
|
||||||
|
public name: string;
|
||||||
|
public description: string | null;
|
||||||
|
public customerDescription: string | null;
|
||||||
|
public internalNotes: string | null;
|
||||||
|
|
||||||
|
public readonly cwCatalogId: number;
|
||||||
|
public readonly identifier: string | null;
|
||||||
|
|
||||||
|
public category: string | null;
|
||||||
|
public categoryCwId: number | null;
|
||||||
|
public subcategory: string | null;
|
||||||
|
public subcategoryCwId: number | null;
|
||||||
|
|
||||||
|
public manufacturer: string | null;
|
||||||
|
public manufactureCwId: number | null;
|
||||||
|
public partNumber: string | null;
|
||||||
|
|
||||||
|
public vendorName: string | null;
|
||||||
|
public vendorSku: string | null;
|
||||||
|
public vendorCwId: number | null;
|
||||||
|
|
||||||
|
public price: number;
|
||||||
|
public cost: number;
|
||||||
|
|
||||||
|
public inactive: boolean;
|
||||||
|
public salesTaxable: boolean;
|
||||||
|
|
||||||
|
public onHand: number;
|
||||||
|
public cwLastUpdated: Date | null;
|
||||||
|
|
||||||
|
private _linkedItems: CatalogItemController[];
|
||||||
|
|
||||||
|
public readonly createdAt: Date;
|
||||||
|
public updatedAt: Date;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
itemData: CatalogItem & {
|
||||||
|
linkedItems?: CatalogItem[];
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
this.id = itemData.id;
|
||||||
|
this.name = itemData.name;
|
||||||
|
this.description = itemData.description;
|
||||||
|
this.customerDescription = itemData.customerDescription;
|
||||||
|
this.internalNotes = itemData.internalNotes;
|
||||||
|
this.cwCatalogId = itemData.cwCatalogId;
|
||||||
|
this.identifier = itemData.identifier;
|
||||||
|
this.category = itemData.category;
|
||||||
|
this.categoryCwId = itemData.categoryCwId;
|
||||||
|
this.subcategory = itemData.subcategory;
|
||||||
|
this.subcategoryCwId = itemData.subcategoryCwId;
|
||||||
|
this.manufacturer = itemData.manufacturer;
|
||||||
|
this.manufactureCwId = itemData.manufactureCwId;
|
||||||
|
this.partNumber = itemData.partNumber;
|
||||||
|
this.vendorName = itemData.vendorName;
|
||||||
|
this.vendorSku = itemData.vendorSku;
|
||||||
|
this.vendorCwId = itemData.vendorCwId;
|
||||||
|
this.price = itemData.price;
|
||||||
|
this.cost = itemData.cost;
|
||||||
|
this.inactive = itemData.inactive;
|
||||||
|
this.salesTaxable = itemData.salesTaxable;
|
||||||
|
this.onHand = itemData.onHand;
|
||||||
|
this.cwLastUpdated = itemData.cwLastUpdated;
|
||||||
|
this.createdAt = itemData.createdAt;
|
||||||
|
this.updatedAt = itemData.updatedAt;
|
||||||
|
|
||||||
|
this._linkedItems = (itemData.linkedItems ?? []).map(
|
||||||
|
(linked) => new CatalogItemController(linked),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refresh Inventory
|
||||||
|
*
|
||||||
|
* Fetches the latest on-hand inventory count from ConnectWise
|
||||||
|
* and updates both the controller state and the database.
|
||||||
|
*
|
||||||
|
* @returns {Promise<CatalogItemController>} - The updated controller
|
||||||
|
*/
|
||||||
|
public async refreshInventory(): Promise<CatalogItemController> {
|
||||||
|
const onHand = await catalogCw.fetchInventoryOnHand(this.cwCatalogId);
|
||||||
|
|
||||||
|
if (onHand !== this.onHand) {
|
||||||
|
await prisma.catalogItem.update({
|
||||||
|
where: { id: this.id },
|
||||||
|
data: { onHand },
|
||||||
|
});
|
||||||
|
this.onHand = onHand;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch Linked Items
|
||||||
|
*
|
||||||
|
* Returns the linked catalog items as an array of controllers.
|
||||||
|
*
|
||||||
|
* @returns {CatalogItemController[]} - Array of linked item controllers
|
||||||
|
*/
|
||||||
|
public getLinkedItems(): CatalogItemController[] {
|
||||||
|
return this._linkedItems;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Link Item
|
||||||
|
*
|
||||||
|
* Links another catalog item to this item. The relationship is bidirectional
|
||||||
|
* via the Prisma implicit many-to-many.
|
||||||
|
*
|
||||||
|
* @param targetId - The internal ID of the catalog item to link
|
||||||
|
* @returns {Promise<CatalogItemController>} - The updated controller
|
||||||
|
*/
|
||||||
|
public async linkItem(targetId: string): Promise<CatalogItemController> {
|
||||||
|
if (targetId === this.id) {
|
||||||
|
throw new GenericError({
|
||||||
|
message: "Cannot link a catalog item to itself",
|
||||||
|
name: "InvalidLinkTarget",
|
||||||
|
cause: `Item '${this.id}' cannot be linked to itself`,
|
||||||
|
status: 400,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const target = await prisma.catalogItem.findFirst({
|
||||||
|
where: { id: targetId },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!target) {
|
||||||
|
throw new GenericError({
|
||||||
|
message: "Target catalog item not found",
|
||||||
|
name: "CatalogItemNotFound",
|
||||||
|
cause: `No catalog item exists with ID '${targetId}'`,
|
||||||
|
status: 404,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const updated = await prisma.catalogItem.update({
|
||||||
|
where: { id: this.id },
|
||||||
|
data: {
|
||||||
|
linkedItems: { connect: { id: targetId } },
|
||||||
|
},
|
||||||
|
include: { linkedItems: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
this._linkedItems = (updated.linkedItems ?? []).map(
|
||||||
|
(linked) => new CatalogItemController(linked),
|
||||||
|
);
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unlink Item
|
||||||
|
*
|
||||||
|
* Removes the link between this catalog item and another.
|
||||||
|
*
|
||||||
|
* @param targetId - The internal ID of the catalog item to unlink
|
||||||
|
* @returns {Promise<CatalogItemController>} - The updated controller
|
||||||
|
*/
|
||||||
|
public async unlinkItem(targetId: string): Promise<CatalogItemController> {
|
||||||
|
const updated = await prisma.catalogItem.update({
|
||||||
|
where: { id: this.id },
|
||||||
|
data: {
|
||||||
|
linkedItems: { disconnect: { id: targetId } },
|
||||||
|
},
|
||||||
|
include: { linkedItems: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
this._linkedItems = (updated.linkedItems ?? []).map(
|
||||||
|
(linked) => new CatalogItemController(linked),
|
||||||
|
);
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* To JSON
|
||||||
|
*
|
||||||
|
* Serializes the catalog item into a safe, API-friendly object.
|
||||||
|
*
|
||||||
|
* @param opts - Options to control output
|
||||||
|
* @returns - A JSON-safe representation of the catalog item
|
||||||
|
*/
|
||||||
|
public toJson(opts?: { includeLinkedItems?: boolean }): Record<string, any> {
|
||||||
|
return {
|
||||||
|
id: this.id,
|
||||||
|
cwCatalogId: this.cwCatalogId,
|
||||||
|
identifier: this.identifier,
|
||||||
|
name: this.name,
|
||||||
|
description: this.description,
|
||||||
|
customerDescription: this.customerDescription,
|
||||||
|
internalNotes: this.internalNotes,
|
||||||
|
category: this.category,
|
||||||
|
categoryCwId: this.categoryCwId,
|
||||||
|
subcategory: this.subcategory,
|
||||||
|
subcategoryCwId: this.subcategoryCwId,
|
||||||
|
manufacturer: this.manufacturer,
|
||||||
|
manufactureCwId: this.manufactureCwId,
|
||||||
|
partNumber: this.partNumber,
|
||||||
|
vendorName: this.vendorName,
|
||||||
|
vendorSku: this.vendorSku,
|
||||||
|
vendorCwId: this.vendorCwId,
|
||||||
|
price: this.price,
|
||||||
|
cost: this.cost,
|
||||||
|
inactive: this.inactive,
|
||||||
|
salesTaxable: this.salesTaxable,
|
||||||
|
onHand: this.onHand,
|
||||||
|
cwLastUpdated: this.cwLastUpdated,
|
||||||
|
linkedItems: opts?.includeLinkedItems
|
||||||
|
? this._linkedItems.map((item) => item.toJson())
|
||||||
|
: undefined,
|
||||||
|
createdAt: this.createdAt,
|
||||||
|
updatedAt: this.updatedAt,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,13 @@
|
|||||||
import { Company } from "../../generated/prisma/client";
|
import { Company } from "../../generated/prisma/client";
|
||||||
|
import { connectWiseApi } from "../constants";
|
||||||
import { fetchCwCompanyById } from "../modules/cw-utils/fetchCompany";
|
import { fetchCwCompanyById } from "../modules/cw-utils/fetchCompany";
|
||||||
import { fetchCompanyConfigurations } from "../modules/cw-utils/configurations/fetchCompanyConfigurations";
|
import { fetchCompanyConfigurations } from "../modules/cw-utils/configurations/fetchCompanyConfigurations";
|
||||||
import { updateCwInternalCompany } from "../modules/cw-utils/updateCompany";
|
import { updateCwInternalCompany } from "../modules/cw-utils/updateCompany";
|
||||||
|
import {
|
||||||
|
fetchCompanySites,
|
||||||
|
fetchCompanySite,
|
||||||
|
serializeCwSite,
|
||||||
|
} from "../modules/cw-utils/sites/companySites";
|
||||||
import { Company as CWCompany, Contact } from "../types/ConnectWiseTypes";
|
import { Company as CWCompany, Contact } from "../types/ConnectWiseTypes";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -16,9 +22,9 @@ export class CompanyController {
|
|||||||
public name: string;
|
public name: string;
|
||||||
public readonly cw_Identifier: string;
|
public readonly cw_Identifier: string;
|
||||||
public readonly cw_CompanyId: number;
|
public readonly cw_CompanyId: number;
|
||||||
public readonly cw_Data?: {
|
public cw_Data?: {
|
||||||
company: CWCompany;
|
company: CWCompany;
|
||||||
defaultContact: Contact;
|
defaultContact: Contact | null;
|
||||||
allContacts: Contact[];
|
allContacts: Contact[];
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -30,6 +36,38 @@ export class CompanyController {
|
|||||||
this.cw_Data = cwData;
|
this.cw_Data = cwData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hydrate CW Data
|
||||||
|
*
|
||||||
|
* Fetches and populates the full ConnectWise company data
|
||||||
|
* (company, default contact, all contacts) if not already loaded.
|
||||||
|
*
|
||||||
|
* @returns {ThisType}
|
||||||
|
*/
|
||||||
|
public async hydrateCwData() {
|
||||||
|
if (this.cw_Data) return this;
|
||||||
|
|
||||||
|
const cwCompany = await fetchCwCompanyById(this.cw_CompanyId);
|
||||||
|
if (!cwCompany) return this;
|
||||||
|
|
||||||
|
const contactHref = cwCompany.defaultContact?._info?.contact_href;
|
||||||
|
const defaultContactData = contactHref
|
||||||
|
? await connectWiseApi.get(contactHref)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const allContactsData = await connectWiseApi.get(
|
||||||
|
`${cwCompany._info.contacts_href}&pageSize=1000`,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.cw_Data = {
|
||||||
|
company: cwCompany,
|
||||||
|
defaultContact: defaultContactData?.data ?? null,
|
||||||
|
allContacts: allContactsData.data,
|
||||||
|
};
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Refresh Internal Company Data from ConnectWise
|
* Refresh Internal Company Data from ConnectWise
|
||||||
*
|
*
|
||||||
@@ -71,6 +109,30 @@ export class CompanyController {
|
|||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch Company Sites
|
||||||
|
*
|
||||||
|
* Retrieves all sites for this company from ConnectWise
|
||||||
|
* and returns them as serialized site objects.
|
||||||
|
*/
|
||||||
|
public async fetchSites() {
|
||||||
|
const sites = await fetchCompanySites(this.cw_CompanyId);
|
||||||
|
return sites.map(serializeCwSite);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch Company Site by ID
|
||||||
|
*
|
||||||
|
* Retrieves a single site by its ConnectWise site ID
|
||||||
|
* and returns a serialized site object.
|
||||||
|
*
|
||||||
|
* @param cwSiteId - The ConnectWise site ID
|
||||||
|
*/
|
||||||
|
public async fetchSite(cwSiteId: number) {
|
||||||
|
const site = await fetchCompanySite(this.cw_CompanyId, cwSiteId);
|
||||||
|
return serializeCwSite(site);
|
||||||
|
}
|
||||||
|
|
||||||
public toJson(opts?: {
|
public toJson(opts?: {
|
||||||
includeAddress: boolean;
|
includeAddress: boolean;
|
||||||
includePrimaryContact: boolean;
|
includePrimaryContact: boolean;
|
||||||
@@ -96,23 +158,25 @@ export class CompanyController {
|
|||||||
},
|
},
|
||||||
primaryContact: !opts?.includePrimaryContact
|
primaryContact: !opts?.includePrimaryContact
|
||||||
? undefined
|
? undefined
|
||||||
: {
|
: this.cw_Data?.defaultContact
|
||||||
firstName: this.cw_Data?.defaultContact.firstName,
|
? {
|
||||||
lastName: this.cw_Data?.defaultContact.lastName,
|
firstName: this.cw_Data.defaultContact.firstName,
|
||||||
cwId: this.cw_Data?.defaultContact.id,
|
lastName: this.cw_Data.defaultContact.lastName,
|
||||||
inactive: this.cw_Data?.defaultContact.inactiveFlag,
|
cwId: this.cw_Data.defaultContact.id,
|
||||||
title: this.cw_Data?.defaultContact.title,
|
inactive: this.cw_Data.defaultContact.inactiveFlag,
|
||||||
phone: this.cw_Data?.defaultContact.defaultPhoneNbr,
|
title: this.cw_Data.defaultContact.title,
|
||||||
|
phone: this.cw_Data.defaultContact.defaultPhoneNbr,
|
||||||
email: (() => {
|
email: (() => {
|
||||||
if (!this.cw_Data?.defaultContact.communicationItems)
|
if (!this.cw_Data?.defaultContact?.communicationItems)
|
||||||
return null;
|
return null;
|
||||||
return (
|
return (
|
||||||
this.cw_Data?.defaultContact.communicationItems.find(
|
this.cw_Data.defaultContact.communicationItems.find(
|
||||||
(v) => v.type.name === "Email",
|
(v) => v.type.name === "Email",
|
||||||
)?.value ?? null
|
)?.value ?? null
|
||||||
);
|
);
|
||||||
})(),
|
})(),
|
||||||
},
|
}
|
||||||
|
: null,
|
||||||
allContacts: !opts?.includeAllContacts
|
allContacts: !opts?.includeAllContacts
|
||||||
? undefined
|
? undefined
|
||||||
: this.cw_Data?.allContacts.map((contact) => ({
|
: this.cw_Data?.allContacts.map((contact) => ({
|
||||||
|
|||||||
@@ -0,0 +1,226 @@
|
|||||||
|
import { CWForecastItem } from "../modules/cw-utils/opportunities/opportunity.types";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Forecast Product Controller
|
||||||
|
*
|
||||||
|
* Domain model class that encapsulates a ConnectWise Forecast Item (product/
|
||||||
|
* revenue line item on an opportunity). Forecast products are not persisted
|
||||||
|
* locally — all data is sourced directly from the ConnectWise API.
|
||||||
|
*/
|
||||||
|
export class ForecastProductController {
|
||||||
|
public readonly cwForecastId: number;
|
||||||
|
public forecastDescription: string;
|
||||||
|
|
||||||
|
public opportunityCwId: number | null;
|
||||||
|
public opportunityName: string | null;
|
||||||
|
|
||||||
|
public quantity: number;
|
||||||
|
|
||||||
|
public statusCwId: number | null;
|
||||||
|
public statusName: string | null;
|
||||||
|
|
||||||
|
public catalogItemCwId: number | null;
|
||||||
|
public catalogItemIdentifier: string | null;
|
||||||
|
|
||||||
|
public productDescription: string;
|
||||||
|
public productClass: string;
|
||||||
|
public forecastType: string;
|
||||||
|
|
||||||
|
public revenue: number;
|
||||||
|
public cost: number;
|
||||||
|
public margin: number;
|
||||||
|
public percentage: number;
|
||||||
|
|
||||||
|
public includeFlag: boolean;
|
||||||
|
public linkFlag: boolean;
|
||||||
|
public recurringFlag: boolean;
|
||||||
|
public taxableFlag: boolean;
|
||||||
|
|
||||||
|
public recurringRevenue: number;
|
||||||
|
public recurringCost: number;
|
||||||
|
public cycles: number;
|
||||||
|
|
||||||
|
public sequenceNumber: number;
|
||||||
|
public subNumber: number;
|
||||||
|
public quoteWerksQuantity: number;
|
||||||
|
|
||||||
|
public cwLastUpdated: Date | null;
|
||||||
|
public cwUpdatedBy: string | null;
|
||||||
|
|
||||||
|
// Cancellation data (from procurement products endpoint)
|
||||||
|
public cancelledFlag: boolean;
|
||||||
|
public quantityCancelled: number;
|
||||||
|
public cancelledReason: string | null;
|
||||||
|
public cancelledBy: number | null;
|
||||||
|
public cancelledDate: Date | null;
|
||||||
|
|
||||||
|
// Internal inventory data (from local CatalogItem database)
|
||||||
|
public onHand: number | null;
|
||||||
|
public inStock: boolean | null;
|
||||||
|
|
||||||
|
constructor(data: CWForecastItem) {
|
||||||
|
this.cwForecastId = data.id;
|
||||||
|
this.forecastDescription = data.forecastDescription;
|
||||||
|
|
||||||
|
this.opportunityCwId = data.opportunity?.id ?? null;
|
||||||
|
this.opportunityName = data.opportunity?.name ?? null;
|
||||||
|
|
||||||
|
this.quantity = data.quantity;
|
||||||
|
|
||||||
|
this.statusCwId = data.status?.id ?? null;
|
||||||
|
this.statusName = data.status?.name ?? null;
|
||||||
|
|
||||||
|
this.catalogItemCwId = data.catalogItem?.id ?? null;
|
||||||
|
this.catalogItemIdentifier = data.catalogItem?.identifier ?? null;
|
||||||
|
|
||||||
|
this.productDescription = data.productDescription;
|
||||||
|
this.productClass = data.productClass;
|
||||||
|
this.forecastType = data.forecastType;
|
||||||
|
|
||||||
|
this.revenue = data.revenue;
|
||||||
|
this.cost = data.cost;
|
||||||
|
this.margin = data.margin;
|
||||||
|
this.percentage = data.percentage;
|
||||||
|
|
||||||
|
this.includeFlag = data.includeFlag ?? false;
|
||||||
|
this.linkFlag = data.linkFlag ?? false;
|
||||||
|
this.recurringFlag = data.recurringFlag ?? false;
|
||||||
|
this.taxableFlag = data.taxableFlag ?? false;
|
||||||
|
|
||||||
|
this.recurringRevenue = data.recurringRevenue ?? 0;
|
||||||
|
this.recurringCost = data.recurringCost ?? 0;
|
||||||
|
this.cycles = data.cycles ?? 0;
|
||||||
|
|
||||||
|
this.sequenceNumber = data.sequenceNumber ?? 0;
|
||||||
|
this.subNumber = data.subNumber ?? 0;
|
||||||
|
this.quoteWerksQuantity = data.quoteWerksQuantity ?? 0;
|
||||||
|
|
||||||
|
this.cwLastUpdated = data._info?.lastUpdated
|
||||||
|
? new Date(data._info.lastUpdated)
|
||||||
|
: null;
|
||||||
|
this.cwUpdatedBy = data._info?.updatedBy ?? null;
|
||||||
|
|
||||||
|
// Cancellation defaults — enriched later via applyCancellationData()
|
||||||
|
this.cancelledFlag = false;
|
||||||
|
this.quantityCancelled = 0;
|
||||||
|
this.cancelledReason = null;
|
||||||
|
this.cancelledBy = null;
|
||||||
|
this.cancelledDate = null;
|
||||||
|
|
||||||
|
// Inventory defaults — enriched later via applyInventoryData()
|
||||||
|
this.onHand = null;
|
||||||
|
this.inStock = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply Cancellation Data
|
||||||
|
*
|
||||||
|
* Enriches this forecast product with cancellation data from the
|
||||||
|
* procurement products endpoint.
|
||||||
|
*/
|
||||||
|
public applyCancellationData(data: {
|
||||||
|
cancelledFlag?: boolean;
|
||||||
|
quantityCancelled?: number;
|
||||||
|
cancelledReason?: string;
|
||||||
|
cancelledBy?: number;
|
||||||
|
cancelledDate?: string;
|
||||||
|
}): void {
|
||||||
|
this.cancelledFlag = data.cancelledFlag ?? false;
|
||||||
|
this.quantityCancelled = data.quantityCancelled ?? 0;
|
||||||
|
this.cancelledReason = data.cancelledReason ?? null;
|
||||||
|
this.cancelledBy = data.cancelledBy ?? null;
|
||||||
|
this.cancelledDate = data.cancelledDate
|
||||||
|
? new Date(data.cancelledDate)
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply Inventory Data
|
||||||
|
*
|
||||||
|
* Enriches this forecast product with internal inventory data from
|
||||||
|
* the local CatalogItem database.
|
||||||
|
*/
|
||||||
|
public applyInventoryData(data: { onHand: number }): void {
|
||||||
|
this.onHand = data.onHand;
|
||||||
|
this.inStock = data.onHand > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Profit
|
||||||
|
*
|
||||||
|
* Returns the calculated profit (revenue - cost).
|
||||||
|
*/
|
||||||
|
public get profit(): number {
|
||||||
|
return this.revenue - this.cost;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cancelled
|
||||||
|
*
|
||||||
|
* Returns true if the forecast item has been cancelled (fully or partially).
|
||||||
|
*/
|
||||||
|
public get cancelled(): boolean {
|
||||||
|
return this.cancelledFlag;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cancellation Type
|
||||||
|
*
|
||||||
|
* Returns the type of cancellation:
|
||||||
|
* - `"full"` — all units have been cancelled (`quantityCancelled >= quantity`)
|
||||||
|
* - `"partial"` — some units cancelled but not all
|
||||||
|
* - `null` — not cancelled
|
||||||
|
*/
|
||||||
|
public get cancellationType(): "full" | "partial" | null {
|
||||||
|
if (!this.cancelledFlag || this.quantityCancelled <= 0) return null;
|
||||||
|
return this.quantityCancelled >= this.quantity ? "full" : "partial";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* To JSON
|
||||||
|
*
|
||||||
|
* Serializes the forecast product into a safe, API-friendly object.
|
||||||
|
*/
|
||||||
|
public toJson(): Record<string, any> {
|
||||||
|
return {
|
||||||
|
id: this.cwForecastId,
|
||||||
|
forecastDescription: this.forecastDescription,
|
||||||
|
opportunity: this.opportunityCwId
|
||||||
|
? { id: this.opportunityCwId, name: this.opportunityName }
|
||||||
|
: null,
|
||||||
|
quantity: this.quantity,
|
||||||
|
status: this.statusCwId
|
||||||
|
? { id: this.statusCwId, name: this.statusName }
|
||||||
|
: null,
|
||||||
|
cancelled: this.cancelled,
|
||||||
|
cancellationType: this.cancellationType,
|
||||||
|
quantityCancelled: this.quantityCancelled,
|
||||||
|
cancelledReason: this.cancelledReason,
|
||||||
|
cancelledDate: this.cancelledDate,
|
||||||
|
catalogItem: this.catalogItemCwId
|
||||||
|
? { id: this.catalogItemCwId, identifier: this.catalogItemIdentifier }
|
||||||
|
: null,
|
||||||
|
productDescription: this.productDescription,
|
||||||
|
productClass: this.productClass,
|
||||||
|
forecastType: this.forecastType,
|
||||||
|
revenue: this.revenue,
|
||||||
|
cost: this.cost,
|
||||||
|
margin: this.margin,
|
||||||
|
profit: this.profit,
|
||||||
|
percentage: this.percentage,
|
||||||
|
includeFlag: this.includeFlag,
|
||||||
|
linkFlag: this.linkFlag,
|
||||||
|
recurringFlag: this.recurringFlag,
|
||||||
|
taxableFlag: this.taxableFlag,
|
||||||
|
recurringRevenue: this.recurringRevenue,
|
||||||
|
recurringCost: this.recurringCost,
|
||||||
|
cycles: this.cycles,
|
||||||
|
sequenceNumber: this.sequenceNumber,
|
||||||
|
subNumber: this.subNumber,
|
||||||
|
cwLastUpdated: this.cwLastUpdated,
|
||||||
|
cwUpdatedBy: this.cwUpdatedBy,
|
||||||
|
onHand: this.onHand,
|
||||||
|
inStock: this.inStock,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,801 @@
|
|||||||
|
import { Company, Opportunity } from "../../generated/prisma/client";
|
||||||
|
import { prisma } from "../constants";
|
||||||
|
import { CompanyController } from "./CompanyController";
|
||||||
|
import { ActivityController } from "./ActivityController";
|
||||||
|
import { fetchOpportunity } from "../modules/cw-utils/opportunities/fetchOpportunity";
|
||||||
|
import { opportunityCw } from "../modules/cw-utils/opportunities/opportunities";
|
||||||
|
import { activityCw } from "../modules/cw-utils/activities/activities";
|
||||||
|
import {
|
||||||
|
fetchCompanySite,
|
||||||
|
serializeCwSite,
|
||||||
|
} from "../modules/cw-utils/sites/companySites";
|
||||||
|
import {
|
||||||
|
CWCustomField,
|
||||||
|
CWForecastItemCreate,
|
||||||
|
CWOpportunity,
|
||||||
|
CWOpportunityNote,
|
||||||
|
} from "../modules/cw-utils/opportunities/opportunity.types";
|
||||||
|
import { resolveMember } from "../modules/cw-utils/members/memberCache";
|
||||||
|
import { ForecastProductController } from "./ForecastProductController";
|
||||||
|
import GenericError from "../Errors/GenericError";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opportunity Controller
|
||||||
|
*
|
||||||
|
* Domain model class that encapsulates an Opportunity entity and provides
|
||||||
|
* methods for accessing, refreshing from ConnectWise, and serializing
|
||||||
|
* opportunity data.
|
||||||
|
*/
|
||||||
|
export class OpportunityController {
|
||||||
|
public readonly id: string;
|
||||||
|
public readonly cwOpportunityId: number;
|
||||||
|
public name: string;
|
||||||
|
public notes: string | null;
|
||||||
|
|
||||||
|
public typeName: string | null;
|
||||||
|
public typeCwId: number | null;
|
||||||
|
public stageName: string | null;
|
||||||
|
public stageCwId: number | null;
|
||||||
|
public statusName: string | null;
|
||||||
|
public statusCwId: number | null;
|
||||||
|
public priorityName: string | null;
|
||||||
|
public priorityCwId: number | null;
|
||||||
|
public ratingName: string | null;
|
||||||
|
public ratingCwId: number | null;
|
||||||
|
public source: string | null;
|
||||||
|
public campaignName: string | null;
|
||||||
|
public campaignCwId: number | null;
|
||||||
|
|
||||||
|
public primarySalesRepName: string | null;
|
||||||
|
public primarySalesRepIdentifier: string | null;
|
||||||
|
public primarySalesRepCwId: number | null;
|
||||||
|
public secondarySalesRepName: string | null;
|
||||||
|
public secondarySalesRepIdentifier: string | null;
|
||||||
|
public secondarySalesRepCwId: number | null;
|
||||||
|
|
||||||
|
public companyCwId: number | null;
|
||||||
|
public companyName: string | null;
|
||||||
|
public contactCwId: number | null;
|
||||||
|
public contactName: string | null;
|
||||||
|
public siteCwId: number | null;
|
||||||
|
public siteName: string | null;
|
||||||
|
public customerPO: string | null;
|
||||||
|
|
||||||
|
public totalSalesTax: number;
|
||||||
|
|
||||||
|
public locationName: string | null;
|
||||||
|
public locationCwId: number | null;
|
||||||
|
public departmentName: string | null;
|
||||||
|
public departmentCwId: number | null;
|
||||||
|
|
||||||
|
public expectedCloseDate: Date | null;
|
||||||
|
public pipelineChangeDate: Date | null;
|
||||||
|
public dateBecameLead: Date | null;
|
||||||
|
public closedDate: Date | null;
|
||||||
|
public closedFlag: boolean;
|
||||||
|
public closedByName: string | null;
|
||||||
|
public closedByCwId: number | null;
|
||||||
|
|
||||||
|
public companyId: string | null;
|
||||||
|
public cwLastUpdated: Date | null;
|
||||||
|
|
||||||
|
// Local product display order — array of CW forecast item IDs.
|
||||||
|
// When non-empty, fetchProducts() uses this instead of CW sequenceNumber.
|
||||||
|
public productSequence: number[];
|
||||||
|
|
||||||
|
public readonly createdAt: Date;
|
||||||
|
public updatedAt: Date;
|
||||||
|
|
||||||
|
private _company: CompanyController | null = null;
|
||||||
|
private _siteData: ReturnType<typeof serializeCwSite> | null = null;
|
||||||
|
private _customFields: CWCustomField[] | null = null;
|
||||||
|
private _activities: ActivityController[] | null = null;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
data: Opportunity & { company?: Company | null },
|
||||||
|
opts?: {
|
||||||
|
company?: CompanyController;
|
||||||
|
customFields?: CWCustomField[];
|
||||||
|
activities?: ActivityController[];
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
this.id = data.id;
|
||||||
|
this.cwOpportunityId = data.cwOpportunityId;
|
||||||
|
this.name = data.name;
|
||||||
|
this.notes = data.notes;
|
||||||
|
|
||||||
|
this.typeName = data.typeName;
|
||||||
|
this.typeCwId = data.typeCwId;
|
||||||
|
this.stageName = data.stageName;
|
||||||
|
this.stageCwId = data.stageCwId;
|
||||||
|
this.statusName = data.statusName;
|
||||||
|
this.statusCwId = data.statusCwId;
|
||||||
|
this.priorityName = data.priorityName;
|
||||||
|
this.priorityCwId = data.priorityCwId;
|
||||||
|
this.ratingName = data.ratingName;
|
||||||
|
this.ratingCwId = data.ratingCwId;
|
||||||
|
this.source = data.source;
|
||||||
|
this.campaignName = data.campaignName;
|
||||||
|
this.campaignCwId = data.campaignCwId;
|
||||||
|
|
||||||
|
this.primarySalesRepName = data.primarySalesRepName;
|
||||||
|
this.primarySalesRepIdentifier = data.primarySalesRepIdentifier;
|
||||||
|
this.primarySalesRepCwId = data.primarySalesRepCwId;
|
||||||
|
this.secondarySalesRepName = data.secondarySalesRepName;
|
||||||
|
this.secondarySalesRepIdentifier = data.secondarySalesRepIdentifier;
|
||||||
|
this.secondarySalesRepCwId = data.secondarySalesRepCwId;
|
||||||
|
|
||||||
|
this.companyCwId = data.companyCwId;
|
||||||
|
this.companyName = data.companyName;
|
||||||
|
this.contactCwId = data.contactCwId;
|
||||||
|
this.contactName = data.contactName;
|
||||||
|
this.siteCwId = data.siteCwId;
|
||||||
|
this.siteName = data.siteName;
|
||||||
|
this.customerPO = data.customerPO;
|
||||||
|
|
||||||
|
this.totalSalesTax = data.totalSalesTax;
|
||||||
|
|
||||||
|
this.locationName = data.locationName;
|
||||||
|
this.locationCwId = data.locationCwId;
|
||||||
|
this.departmentName = data.departmentName;
|
||||||
|
this.departmentCwId = data.departmentCwId;
|
||||||
|
|
||||||
|
this.expectedCloseDate = data.expectedCloseDate;
|
||||||
|
this.pipelineChangeDate = data.pipelineChangeDate;
|
||||||
|
this.dateBecameLead = data.dateBecameLead;
|
||||||
|
this.closedDate = data.closedDate;
|
||||||
|
this.closedFlag = data.closedFlag;
|
||||||
|
this.closedByName = data.closedByName;
|
||||||
|
this.closedByCwId = data.closedByCwId;
|
||||||
|
|
||||||
|
this.companyId = data.companyId;
|
||||||
|
this.cwLastUpdated = data.cwLastUpdated;
|
||||||
|
this.productSequence = data.productSequence;
|
||||||
|
|
||||||
|
this.createdAt = data.createdAt;
|
||||||
|
this.updatedAt = data.updatedAt;
|
||||||
|
|
||||||
|
this._company =
|
||||||
|
opts?.company ??
|
||||||
|
(data.company ? new CompanyController(data.company) : null);
|
||||||
|
|
||||||
|
this._customFields = opts?.customFields ?? null;
|
||||||
|
this._activities = opts?.activities ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch Company
|
||||||
|
*
|
||||||
|
* Lazily loads the associated CompanyController from the database
|
||||||
|
* if not already loaded via the Prisma include.
|
||||||
|
*
|
||||||
|
* @returns {Promise<CompanyController | null>}
|
||||||
|
*/
|
||||||
|
public async fetchCompany(): Promise<CompanyController | null> {
|
||||||
|
if (this._company) {
|
||||||
|
await this._company.hydrateCwData();
|
||||||
|
return this._company;
|
||||||
|
}
|
||||||
|
if (!this.companyId) return null;
|
||||||
|
|
||||||
|
const companyData = await prisma.company.findUnique({
|
||||||
|
where: { id: this.companyId },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!companyData) return null;
|
||||||
|
|
||||||
|
this._company = new CompanyController(companyData);
|
||||||
|
await this._company.hydrateCwData();
|
||||||
|
return this._company;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refresh from ConnectWise
|
||||||
|
*
|
||||||
|
* Fetches the latest opportunity data from CW and updates
|
||||||
|
* the local database record and controller state.
|
||||||
|
*/
|
||||||
|
public async refreshFromCW(): Promise<OpportunityController> {
|
||||||
|
const cwData = await fetchOpportunity(this.cwOpportunityId);
|
||||||
|
const mapped = OpportunityController.mapCwToDb(cwData);
|
||||||
|
|
||||||
|
const updated = await prisma.opportunity.update({
|
||||||
|
where: { id: this.id },
|
||||||
|
data: mapped,
|
||||||
|
include: { company: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
return new OpportunityController(updated);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch raw CW data
|
||||||
|
*
|
||||||
|
* Returns the raw ConnectWise opportunity object without updating the DB.
|
||||||
|
*/
|
||||||
|
public async fetchCwData(): Promise<CWOpportunity> {
|
||||||
|
return fetchOpportunity(this.cwOpportunityId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map CW Opportunity → Prisma create/update payload
|
||||||
|
*
|
||||||
|
* Static helper used by both the controller and the refresh sync.
|
||||||
|
*/
|
||||||
|
public static mapCwToDb(item: CWOpportunity) {
|
||||||
|
return {
|
||||||
|
name: item.name,
|
||||||
|
notes: item.notes ?? null,
|
||||||
|
|
||||||
|
typeName: item.type?.name ?? null,
|
||||||
|
typeCwId: item.type?.id ?? null,
|
||||||
|
stageName: item.stage?.name ?? null,
|
||||||
|
stageCwId: item.stage?.id ?? null,
|
||||||
|
statusName: item.status?.name ?? null,
|
||||||
|
statusCwId: item.status?.id ?? null,
|
||||||
|
priorityName: item.priority?.name ?? null,
|
||||||
|
priorityCwId: item.priority?.id ?? null,
|
||||||
|
ratingName: item.rating?.name ?? null,
|
||||||
|
ratingCwId: item.rating?.id ?? null,
|
||||||
|
source: item.source ?? null,
|
||||||
|
campaignName: item.campaign?.name ?? null,
|
||||||
|
campaignCwId: item.campaign?.id ?? null,
|
||||||
|
|
||||||
|
primarySalesRepName: item.primarySalesRep?.name ?? null,
|
||||||
|
primarySalesRepIdentifier: item.primarySalesRep?.identifier ?? null,
|
||||||
|
primarySalesRepCwId: item.primarySalesRep?.id ?? null,
|
||||||
|
secondarySalesRepName: item.secondarySalesRep?.name ?? null,
|
||||||
|
secondarySalesRepIdentifier: item.secondarySalesRep?.identifier ?? null,
|
||||||
|
secondarySalesRepCwId: item.secondarySalesRep?.id ?? null,
|
||||||
|
|
||||||
|
companyCwId: item.company?.id ?? null,
|
||||||
|
companyName: item.company?.name ?? null,
|
||||||
|
contactCwId: item.contact?.id ?? null,
|
||||||
|
contactName: item.contact?.name ?? null,
|
||||||
|
siteCwId: item.site?.id ?? null,
|
||||||
|
siteName: item.site?.name ?? null,
|
||||||
|
customerPO: item.customerPO ?? null,
|
||||||
|
|
||||||
|
totalSalesTax: item.totalSalesTax ?? 0,
|
||||||
|
|
||||||
|
locationName: item.location?.name ?? null,
|
||||||
|
locationCwId: item.location?.id ?? null,
|
||||||
|
departmentName: item.department?.name ?? null,
|
||||||
|
departmentCwId: item.department?.id ?? null,
|
||||||
|
|
||||||
|
expectedCloseDate: item.expectedCloseDate
|
||||||
|
? new Date(item.expectedCloseDate)
|
||||||
|
: null,
|
||||||
|
pipelineChangeDate: item.pipelineChangeDate
|
||||||
|
? new Date(item.pipelineChangeDate)
|
||||||
|
: null,
|
||||||
|
dateBecameLead: item.dateBecameLead
|
||||||
|
? new Date(item.dateBecameLead)
|
||||||
|
: null,
|
||||||
|
closedDate: item.closedDate ? new Date(item.closedDate) : null,
|
||||||
|
closedFlag: item.closedFlag ?? false,
|
||||||
|
closedByName: item.closedBy?.name ?? null,
|
||||||
|
closedByCwId: item.closedBy?.id ?? null,
|
||||||
|
|
||||||
|
cwLastUpdated: item._info?.lastUpdated
|
||||||
|
? new Date(item._info.lastUpdated)
|
||||||
|
: new Date(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch Site
|
||||||
|
*
|
||||||
|
* Fetches the full site details (address, phone, flags) from ConnectWise
|
||||||
|
* for the site associated with this opportunity.
|
||||||
|
* Requires both companyCwId and siteCwId to be set.
|
||||||
|
*
|
||||||
|
* @returns Serialized site object or null
|
||||||
|
*/
|
||||||
|
public async fetchSite() {
|
||||||
|
if (this._siteData) return this._siteData;
|
||||||
|
if (!this.companyCwId || !this.siteCwId) return null;
|
||||||
|
|
||||||
|
const cwSite = await fetchCompanySite(this.companyCwId, this.siteCwId);
|
||||||
|
this._siteData = serializeCwSite(cwSite);
|
||||||
|
return this._siteData;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch Contacts
|
||||||
|
*
|
||||||
|
* Fetches contacts associated with this opportunity from ConnectWise
|
||||||
|
* and returns a serialized array.
|
||||||
|
*/
|
||||||
|
public async fetchContacts() {
|
||||||
|
const contacts = await opportunityCw.fetchContacts(this.cwOpportunityId);
|
||||||
|
|
||||||
|
return contacts.map((ct) => ({
|
||||||
|
id: ct.id,
|
||||||
|
contact: ct.contact ? { id: ct.contact.id, name: ct.contact.name } : null,
|
||||||
|
company: ct.company
|
||||||
|
? {
|
||||||
|
id: ct.company.id,
|
||||||
|
identifier: ct.company.identifier,
|
||||||
|
name: ct.company.name,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
role: ct.role ? { id: ct.role.id, name: ct.role.name } : null,
|
||||||
|
notes: ct.notes,
|
||||||
|
referralFlag: ct.referralFlag,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch Notes
|
||||||
|
*
|
||||||
|
* Fetches notes associated with this opportunity from ConnectWise
|
||||||
|
* and returns a serialized array.
|
||||||
|
*/
|
||||||
|
public async fetchNotes() {
|
||||||
|
const notes = await opportunityCw.fetchNotes(this.cwOpportunityId);
|
||||||
|
|
||||||
|
return Promise.all(
|
||||||
|
notes.map(async (n) => ({
|
||||||
|
id: n.id,
|
||||||
|
text: n.text,
|
||||||
|
type: n.type ? { id: n.type.id, name: n.type.name } : null,
|
||||||
|
flagged: n.flagged,
|
||||||
|
dateEntered: n._info?.lastUpdated
|
||||||
|
? new Date(n._info.lastUpdated)
|
||||||
|
: null,
|
||||||
|
enteredBy: await resolveMember(n.enteredBy),
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch Single Note
|
||||||
|
*
|
||||||
|
* Fetches a single note by its ID from ConnectWise.
|
||||||
|
*
|
||||||
|
* @param noteId - The CW note ID
|
||||||
|
*/
|
||||||
|
public async fetchNote(noteId: number) {
|
||||||
|
const note = await opportunityCw.fetchNote(this.cwOpportunityId, noteId);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: note.id,
|
||||||
|
text: note.text,
|
||||||
|
type: note.type ? { id: note.type.id, name: note.type.name } : null,
|
||||||
|
flagged: note.flagged,
|
||||||
|
enteredBy: await resolveMember(note.enteredBy),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch Activities
|
||||||
|
*
|
||||||
|
* Fetches activities associated with this opportunity from ConnectWise
|
||||||
|
* and returns an array of ActivityController instances.
|
||||||
|
* Results are cached after the first call.
|
||||||
|
*/
|
||||||
|
public async fetchActivities(): Promise<ActivityController[]> {
|
||||||
|
if (this._activities) return this._activities;
|
||||||
|
|
||||||
|
const collection = await activityCw.fetchByOpportunity(
|
||||||
|
this.cwOpportunityId,
|
||||||
|
);
|
||||||
|
this._activities = collection.map((item) => new ActivityController(item));
|
||||||
|
return this._activities;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch Products
|
||||||
|
*
|
||||||
|
* Fetches products (forecast/revenue items) for this opportunity from
|
||||||
|
* ConnectWise and returns ForecastProductController instances.
|
||||||
|
*/
|
||||||
|
public async fetchProducts(): Promise<ForecastProductController[]> {
|
||||||
|
const [forecast, procProducts] = await Promise.all([
|
||||||
|
opportunityCw.fetchProducts(this.cwOpportunityId),
|
||||||
|
opportunityCw.fetchProcurementProducts(this.cwOpportunityId),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Build a map of forecastDetailId → procurement product cancellation data
|
||||||
|
const cancellationMap = new Map<number, Record<string, unknown>>();
|
||||||
|
for (const pp of procProducts) {
|
||||||
|
const forecastDetailId = pp.forecastDetailId as number | undefined;
|
||||||
|
if (forecastDetailId) {
|
||||||
|
cancellationMap.set(forecastDetailId, pp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply local ordering if productSequence is set, otherwise fall back
|
||||||
|
// to CW sequenceNumber.
|
||||||
|
const forecastItems = forecast.forecastItems ?? [];
|
||||||
|
let ordered: typeof forecastItems;
|
||||||
|
|
||||||
|
if (this.productSequence.length > 0) {
|
||||||
|
const itemById = new Map(forecastItems.map((fi) => [fi.id, fi]));
|
||||||
|
// Items in the specified order first, then any new items not yet sequenced
|
||||||
|
const sequenced = this.productSequence
|
||||||
|
.map((id) => itemById.get(id))
|
||||||
|
.filter((fi): fi is NonNullable<typeof fi> => fi !== undefined);
|
||||||
|
const sequencedIds = new Set(this.productSequence);
|
||||||
|
const unsequenced = forecastItems
|
||||||
|
.filter((fi) => !sequencedIds.has(fi.id))
|
||||||
|
.sort((a, b) => a.sequenceNumber - b.sequenceNumber);
|
||||||
|
ordered = [...sequenced, ...unsequenced];
|
||||||
|
} else {
|
||||||
|
ordered = [...forecastItems].sort(
|
||||||
|
(a, b) => a.sequenceNumber - b.sequenceNumber,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const controllers = ordered.map((item) => {
|
||||||
|
const ctrl = new ForecastProductController(item);
|
||||||
|
const procData = cancellationMap.get(item.id);
|
||||||
|
if (procData) {
|
||||||
|
ctrl.applyCancellationData(procData as any);
|
||||||
|
}
|
||||||
|
return ctrl;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Enrich with internal inventory data from local CatalogItem DB
|
||||||
|
const catalogCwIds = controllers
|
||||||
|
.map((c) => c.catalogItemCwId)
|
||||||
|
.filter((id): id is number => id !== null);
|
||||||
|
|
||||||
|
if (catalogCwIds.length > 0) {
|
||||||
|
const catalogItems = await prisma.catalogItem.findMany({
|
||||||
|
where: { cwCatalogId: { in: catalogCwIds } },
|
||||||
|
select: { cwCatalogId: true, onHand: true },
|
||||||
|
});
|
||||||
|
const inventoryMap = new Map(
|
||||||
|
catalogItems.map((ci) => [ci.cwCatalogId, ci]),
|
||||||
|
);
|
||||||
|
for (const ctrl of controllers) {
|
||||||
|
const inv = ctrl.catalogItemCwId
|
||||||
|
? inventoryMap.get(ctrl.catalogItemCwId)
|
||||||
|
: undefined;
|
||||||
|
if (inv) ctrl.applyInventoryData(inv);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return controllers;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Opportunity Activity / Workflow Methods
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set Internal Review
|
||||||
|
*
|
||||||
|
* The quote is ready to be reviewed before it is ready to be sent.
|
||||||
|
*/
|
||||||
|
public async setInternalReview(): Promise<void> {
|
||||||
|
// TODO: implement
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set Internal Approved
|
||||||
|
*
|
||||||
|
* The quote has been approved and is ready to be sent out.
|
||||||
|
*/
|
||||||
|
public async setInternalApproved(): Promise<void> {
|
||||||
|
// TODO: implement
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set Quote Sent
|
||||||
|
*
|
||||||
|
* The quote has been sent to the customer.
|
||||||
|
*/
|
||||||
|
public async setQuoteSent(): Promise<void> {
|
||||||
|
// TODO: implement
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set Quote Confirmed
|
||||||
|
*
|
||||||
|
* The quote has been received by the customer.
|
||||||
|
*/
|
||||||
|
public async setQuoteConfirmed(): Promise<void> {
|
||||||
|
// TODO: implement
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set Revision Needed
|
||||||
|
*
|
||||||
|
* The quote needs to be revised and is set to stage revision.
|
||||||
|
*/
|
||||||
|
public async setRevisionNeeded(): Promise<void> {
|
||||||
|
// TODO: implement
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set Finalized
|
||||||
|
*
|
||||||
|
* Locks any non-admins from modifying the quote, indicating
|
||||||
|
* this is the final iteration of the quote.
|
||||||
|
*/
|
||||||
|
public async setFinalized(): Promise<void> {
|
||||||
|
// TODO: implement
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert
|
||||||
|
*
|
||||||
|
* Converts the quote to a ticket and updates all necessary fields.
|
||||||
|
*/
|
||||||
|
public async convert(): Promise<void> {
|
||||||
|
// TODO: implement
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add Time
|
||||||
|
*
|
||||||
|
* Adds time to an activity on this opportunity.
|
||||||
|
*
|
||||||
|
* @param activityId - The CW activity ID to add time to
|
||||||
|
* @param user - The user identifier adding time
|
||||||
|
*/
|
||||||
|
public async addTime(activityId: number, user: string): Promise<void> {
|
||||||
|
// TODO: implement
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update Product
|
||||||
|
*
|
||||||
|
* Updates an existing product/line item on this opportunity via PATCH.
|
||||||
|
*
|
||||||
|
* @param forecastItemId - The CW forecast item ID to update
|
||||||
|
* @param data - Key/value pairs to patch
|
||||||
|
*/
|
||||||
|
public async updateProduct(
|
||||||
|
forecastItemId: number,
|
||||||
|
data: Record<string, unknown>,
|
||||||
|
): Promise<ForecastProductController> {
|
||||||
|
try {
|
||||||
|
const updated = await opportunityCw.updateProduct(
|
||||||
|
this.cwOpportunityId,
|
||||||
|
forecastItemId,
|
||||||
|
data,
|
||||||
|
);
|
||||||
|
return new ForecastProductController(updated);
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error(
|
||||||
|
`[updateProduct] Failed to patch forecast item ${forecastItemId} on opportunity ${this.cwOpportunityId}`,
|
||||||
|
JSON.stringify(
|
||||||
|
{
|
||||||
|
data,
|
||||||
|
status: err?.response?.status,
|
||||||
|
statusText: err?.response?.statusText,
|
||||||
|
responseData: err?.response?.data,
|
||||||
|
message: err?.message,
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resequence Products
|
||||||
|
*
|
||||||
|
* Stores the desired display order of forecast item IDs locally in
|
||||||
|
* the database. No CW API calls are made — CW item IDs are stable
|
||||||
|
* and ordering is applied when `fetchProducts()` is called.
|
||||||
|
*
|
||||||
|
* @param orderedIds - Forecast item IDs in the desired display order
|
||||||
|
*/
|
||||||
|
public async resequenceProducts(
|
||||||
|
orderedIds: number[],
|
||||||
|
): Promise<ForecastProductController[]> {
|
||||||
|
// Validate all IDs exist in CW
|
||||||
|
const forecast = await opportunityCw.fetchProducts(this.cwOpportunityId);
|
||||||
|
const existingIds = new Set(
|
||||||
|
(forecast.forecastItems ?? []).map((fi) => fi.id),
|
||||||
|
);
|
||||||
|
for (const id of orderedIds) {
|
||||||
|
if (!existingIds.has(id)) {
|
||||||
|
throw new GenericError({
|
||||||
|
status: 404,
|
||||||
|
name: "ForecastItemNotFound",
|
||||||
|
message: `Forecast item ${id} not found on opportunity ${this.cwOpportunityId}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Persist the sequence locally
|
||||||
|
await prisma.opportunity.update({
|
||||||
|
where: { id: this.id },
|
||||||
|
data: { productSequence: orderedIds },
|
||||||
|
});
|
||||||
|
this.productSequence = orderedIds;
|
||||||
|
|
||||||
|
// Return items in the new order
|
||||||
|
return this.fetchProducts();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add Products
|
||||||
|
*
|
||||||
|
* Adds one or more products/line items to this opportunity via the
|
||||||
|
* ConnectWise forecast endpoint. The caller passes only the fields
|
||||||
|
* the user is permitted to set (already filtered by field-level
|
||||||
|
* permission gating in the route handler).
|
||||||
|
*
|
||||||
|
* Accepts a single item or an array of items.
|
||||||
|
*/
|
||||||
|
public async addProducts(
|
||||||
|
data: CWForecastItemCreate | CWForecastItemCreate[],
|
||||||
|
): Promise<ForecastProductController[]> {
|
||||||
|
try {
|
||||||
|
const created = await opportunityCw.createProducts(
|
||||||
|
this.cwOpportunityId,
|
||||||
|
data,
|
||||||
|
);
|
||||||
|
return created.map((item) => new ForecastProductController(item));
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error(
|
||||||
|
`[addProducts] Failed to create forecast item(s) on opportunity ${this.cwOpportunityId}`,
|
||||||
|
JSON.stringify(
|
||||||
|
{
|
||||||
|
data,
|
||||||
|
status: err?.response?.status,
|
||||||
|
statusText: err?.response?.statusText,
|
||||||
|
responseData: err?.response?.data,
|
||||||
|
message: err?.message,
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
throw new GenericError({
|
||||||
|
status: err?.response?.status ?? 500,
|
||||||
|
name: "AddProductFailed",
|
||||||
|
message:
|
||||||
|
err?.response?.data?.message ??
|
||||||
|
"Failed to add product(s) to opportunity",
|
||||||
|
cause: err?.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add Note
|
||||||
|
*
|
||||||
|
* Creates a new note on this opportunity in ConnectWise.
|
||||||
|
*
|
||||||
|
* @param note - The note text to add
|
||||||
|
* @param user - The user identifier adding the note
|
||||||
|
* @param opts - Optional flags
|
||||||
|
*/
|
||||||
|
public async addNote(
|
||||||
|
note: string,
|
||||||
|
user: string,
|
||||||
|
opts?: { flagged?: boolean },
|
||||||
|
): Promise<CWOpportunityNote> {
|
||||||
|
const created = await opportunityCw.createNote(this.cwOpportunityId, {
|
||||||
|
text: note,
|
||||||
|
flagged: opts?.flagged ?? false,
|
||||||
|
});
|
||||||
|
return created;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update Note
|
||||||
|
*
|
||||||
|
* Updates an existing note on this opportunity in ConnectWise.
|
||||||
|
*
|
||||||
|
* @param noteId - The CW note ID to update
|
||||||
|
* @param data - The fields to update
|
||||||
|
*/
|
||||||
|
public async updateNote(
|
||||||
|
noteId: number,
|
||||||
|
data: { text?: string; flagged?: boolean },
|
||||||
|
): Promise<CWOpportunityNote> {
|
||||||
|
const updated = await opportunityCw.updateNote(
|
||||||
|
this.cwOpportunityId,
|
||||||
|
noteId,
|
||||||
|
data,
|
||||||
|
);
|
||||||
|
return updated;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete Note
|
||||||
|
*
|
||||||
|
* Deletes a note from this opportunity in ConnectWise.
|
||||||
|
*
|
||||||
|
* @param noteId - The CW note ID to delete
|
||||||
|
*/
|
||||||
|
public async deleteNote(noteId: number): Promise<void> {
|
||||||
|
await opportunityCw.deleteNote(this.cwOpportunityId, noteId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* To JSON
|
||||||
|
*
|
||||||
|
* Serializes the opportunity into a safe, API-friendly object.
|
||||||
|
*/
|
||||||
|
public toJson(): Record<string, any> {
|
||||||
|
return {
|
||||||
|
id: this.id,
|
||||||
|
cwOpportunityId: this.cwOpportunityId,
|
||||||
|
name: this.name,
|
||||||
|
notes: this.notes,
|
||||||
|
type: this.typeCwId ? { id: this.typeCwId, name: this.typeName } : null,
|
||||||
|
stage: this.stageCwId
|
||||||
|
? { id: this.stageCwId, name: this.stageName }
|
||||||
|
: null,
|
||||||
|
status: this.statusCwId
|
||||||
|
? { id: this.statusCwId, name: this.statusName }
|
||||||
|
: null,
|
||||||
|
priority: this.priorityCwId
|
||||||
|
? { id: this.priorityCwId, name: this.priorityName }
|
||||||
|
: null,
|
||||||
|
rating: this.ratingCwId
|
||||||
|
? { id: this.ratingCwId, name: this.ratingName }
|
||||||
|
: null,
|
||||||
|
source: this.source,
|
||||||
|
campaign: this.campaignCwId
|
||||||
|
? { id: this.campaignCwId, name: this.campaignName }
|
||||||
|
: null,
|
||||||
|
primarySalesRep: this.primarySalesRepCwId
|
||||||
|
? {
|
||||||
|
id: this.primarySalesRepCwId,
|
||||||
|
identifier: this.primarySalesRepIdentifier,
|
||||||
|
name: this.primarySalesRepName,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
secondarySalesRep: this.secondarySalesRepCwId
|
||||||
|
? {
|
||||||
|
id: this.secondarySalesRepCwId,
|
||||||
|
identifier: this.secondarySalesRepIdentifier,
|
||||||
|
name: this.secondarySalesRepName,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
company: this._company
|
||||||
|
? this._company.toJson({
|
||||||
|
includeAllContacts: true,
|
||||||
|
includeAddress: true,
|
||||||
|
includePrimaryContact: false,
|
||||||
|
})
|
||||||
|
: this.companyCwId
|
||||||
|
? { id: this.companyCwId, name: this.companyName }
|
||||||
|
: null,
|
||||||
|
contact: this.contactCwId
|
||||||
|
? { id: this.contactCwId, name: this.contactName }
|
||||||
|
: null,
|
||||||
|
site: this._siteData
|
||||||
|
? this._siteData
|
||||||
|
: this.siteCwId
|
||||||
|
? { id: this.siteCwId, name: this.siteName }
|
||||||
|
: null,
|
||||||
|
customerPO: this.customerPO,
|
||||||
|
totalSalesTax: this.totalSalesTax,
|
||||||
|
location: this.locationCwId
|
||||||
|
? { id: this.locationCwId, name: this.locationName }
|
||||||
|
: null,
|
||||||
|
department: this.departmentCwId
|
||||||
|
? { id: this.departmentCwId, name: this.departmentName }
|
||||||
|
: null,
|
||||||
|
expectedCloseDate: this.expectedCloseDate,
|
||||||
|
pipelineChangeDate: this.pipelineChangeDate,
|
||||||
|
dateBecameLead: this.dateBecameLead,
|
||||||
|
closedDate: this.closedDate,
|
||||||
|
closedFlag: this.closedFlag,
|
||||||
|
closedBy: this.closedByCwId
|
||||||
|
? { id: this.closedByCwId, name: this.closedByName }
|
||||||
|
: null,
|
||||||
|
companyId: this.companyId,
|
||||||
|
cwLastUpdated: this.cwLastUpdated,
|
||||||
|
productSequence: this.productSequence,
|
||||||
|
createdAt: this.createdAt,
|
||||||
|
updatedAt: this.updatedAt,
|
||||||
|
customFields: this._customFields ?? [],
|
||||||
|
activities: this._activities?.map((a) => a.toJson()) ?? [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,6 +19,7 @@ export default class UserController {
|
|||||||
public login: string;
|
public login: string;
|
||||||
public email: string;
|
public email: string;
|
||||||
public image: string | null;
|
public image: string | null;
|
||||||
|
public cwIdentifier: string | null;
|
||||||
|
|
||||||
private _roles: Collection<string, Role>;
|
private _roles: Collection<string, Role>;
|
||||||
private _permissions: string | null;
|
private _permissions: string | null;
|
||||||
@@ -31,6 +32,7 @@ export default class UserController {
|
|||||||
this.login = userdata.login;
|
this.login = userdata.login;
|
||||||
this.email = userdata.email;
|
this.email = userdata.email;
|
||||||
this.image = userdata.image;
|
this.image = userdata.image;
|
||||||
|
this.cwIdentifier = userdata.cwIdentifier ?? null;
|
||||||
this.updatedAt = userdata.updatedAt;
|
this.updatedAt = userdata.updatedAt;
|
||||||
this.createdAt = userdata.createdAt;
|
this.createdAt = userdata.createdAt;
|
||||||
this._permissions = userdata.permissions ?? null;
|
this._permissions = userdata.permissions ?? null;
|
||||||
@@ -57,6 +59,7 @@ export default class UserController {
|
|||||||
this.login = userdata.login;
|
this.login = userdata.login;
|
||||||
this.email = userdata.email;
|
this.email = userdata.email;
|
||||||
this.image = userdata.image;
|
this.image = userdata.image;
|
||||||
|
this.cwIdentifier = userdata.cwIdentifier ?? null;
|
||||||
this.updatedAt = userdata.updatedAt;
|
this.updatedAt = userdata.updatedAt;
|
||||||
this.createdAt = userdata.createdAt;
|
this.createdAt = userdata.createdAt;
|
||||||
}
|
}
|
||||||
@@ -178,6 +181,46 @@ export default class UserController {
|
|||||||
return decoded.permissions;
|
return decoded.permissions;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read Role Permissions
|
||||||
|
*
|
||||||
|
* Verifies and decodes a role permissions JWT and returns the permission nodes.
|
||||||
|
* Returns an empty array if verification fails.
|
||||||
|
*
|
||||||
|
* @param role - Role record containing the signed permissions token
|
||||||
|
* @returns {string[]} The role permission nodes
|
||||||
|
*/
|
||||||
|
private _readRolePermissions(role: Role): string[] {
|
||||||
|
try {
|
||||||
|
const decoded = jwt.verify(role.permissions, permissionsPrivateKey, {
|
||||||
|
algorithms: ["RS256"],
|
||||||
|
issuer: "roles",
|
||||||
|
subject: role.id,
|
||||||
|
}) as DecodedPermissionsBlock;
|
||||||
|
|
||||||
|
return decoded.permissions;
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read All Permissions
|
||||||
|
*
|
||||||
|
* Aggregates the user's direct permissions and all permissions from their assigned roles
|
||||||
|
* into a single deduplicated array.
|
||||||
|
*
|
||||||
|
* @returns {Promise<string[]>} Combined array of all permission nodes
|
||||||
|
*/
|
||||||
|
public async readAllPermissions(): Promise<string[]> {
|
||||||
|
const directPermissions = this.readPermissions();
|
||||||
|
const rolePermissions = this._roles
|
||||||
|
.map((role) => this._readRolePermissions(role))
|
||||||
|
.flatMap((permissions) => permissions);
|
||||||
|
|
||||||
|
return [...new Set([...directPermissions, ...rolePermissions])];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch Roles
|
* Fetch Roles
|
||||||
*
|
*
|
||||||
@@ -262,9 +305,19 @@ export default class UserController {
|
|||||||
: this._roles.size > 0
|
: this._roles.size > 0
|
||||||
? this._roles.map((v) => v.moniker)
|
? this._roles.map((v) => v.moniker)
|
||||||
: undefined,
|
: undefined,
|
||||||
permissions: opts?.safeReturn ? undefined : this.readPermissions(),
|
permissions: opts?.safeReturn
|
||||||
|
? undefined
|
||||||
|
: (() => {
|
||||||
|
const directPermissions = this.readPermissions();
|
||||||
|
const rolePermissions = this._roles
|
||||||
|
.map((role) => this._readRolePermissions(role))
|
||||||
|
.flatMap((permissions) => permissions);
|
||||||
|
|
||||||
|
return [...new Set([...directPermissions, ...rolePermissions])];
|
||||||
|
})(),
|
||||||
login: opts?.safeReturn ? undefined : this.login,
|
login: opts?.safeReturn ? undefined : this.login,
|
||||||
email: opts?.safeReturn ? undefined : this.email,
|
email: opts?.safeReturn ? undefined : this.email,
|
||||||
|
cwIdentifier: opts?.safeReturn ? undefined : this.cwIdentifier,
|
||||||
image: this.image,
|
image: this.image,
|
||||||
createdAt: this.createdAt,
|
createdAt: this.createdAt,
|
||||||
updatedAt: this.updatedAt,
|
updatedAt: this.updatedAt,
|
||||||
|
|||||||
+94
-19
@@ -12,6 +12,10 @@ import { unifiSites } from "./managers/unifiSites";
|
|||||||
import { refreshCompanies } from "./modules/cw-utils/refreshCompanies";
|
import { refreshCompanies } from "./modules/cw-utils/refreshCompanies";
|
||||||
import { refreshCatalog } from "./modules/cw-utils/procurement/refreshCatalog";
|
import { refreshCatalog } from "./modules/cw-utils/procurement/refreshCatalog";
|
||||||
import { refreshInventory } from "./modules/cw-utils/procurement/refreshInventory";
|
import { refreshInventory } from "./modules/cw-utils/procurement/refreshInventory";
|
||||||
|
import { refreshOpportunities } from "./modules/cw-utils/opportunities/refreshOpportunities";
|
||||||
|
import { refreshOpportunityCache } from "./modules/cache/opportunityCache";
|
||||||
|
import { refreshCwIdentifiers } from "./modules/cw-utils/members/refreshCwIdentifiers";
|
||||||
|
import { userDefinedFieldsCw } from "./modules/cw-utils/userDefinedFields";
|
||||||
import { events, setupEventDebugger } from "./modules/globalEvents";
|
import { events, setupEventDebugger } from "./modules/globalEvents";
|
||||||
import { signPermissions } from "./modules/permission-utils/signPermissions";
|
import { signPermissions } from "./modules/permission-utils/signPermissions";
|
||||||
import { RoleController } from "./controllers/RoleController";
|
import { RoleController } from "./controllers/RoleController";
|
||||||
@@ -20,7 +24,45 @@ import cuid from "cuid";
|
|||||||
// Setup global event debugger in non-production environments
|
// Setup global event debugger in non-production environments
|
||||||
if (Bun.env.NODE_ENV == "development") setupEventDebugger();
|
if (Bun.env.NODE_ENV == "development") setupEventDebugger();
|
||||||
|
|
||||||
|
// Helper to run a startup sync safely — failures are logged but never crash the process.
|
||||||
|
const safeStartup = async (label: string, fn: () => Promise<void>) => {
|
||||||
|
try {
|
||||||
|
await fn();
|
||||||
|
} catch (err) {
|
||||||
|
console.error(
|
||||||
|
`[startup] ${label} failed — will retry on next interval`,
|
||||||
|
err,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Start the HTTP server FIRST so the pod is reachable immediately.
|
||||||
|
// All data-sync tasks run afterwards and are non-blocking.
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
Bun.serve({
|
||||||
|
port: PORT,
|
||||||
|
idleTimeout: 255,
|
||||||
|
websocket: engine.handler().websocket,
|
||||||
|
fetch: (req, server) => {
|
||||||
|
const url = new URL(req.url);
|
||||||
|
|
||||||
|
if (url.pathname.startsWith("/socket.io/")) {
|
||||||
|
return engine.handleRequest(req, server as any);
|
||||||
|
}
|
||||||
|
|
||||||
|
return app.fetch(req, server);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`[startup] Server listening on port ${PORT}`);
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Background initialisation — none of this blocks the server.
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
// Ensure administrator role exists
|
// Ensure administrator role exists
|
||||||
|
await safeStartup("ensureAdminRole", async () => {
|
||||||
const existingAdmin = await prisma.role.findFirst({
|
const existingAdmin = await prisma.role.findFirst({
|
||||||
where: { moniker: "administrator" },
|
where: { moniker: "administrator" },
|
||||||
include: { users: { include: { roles: true } } },
|
include: { users: { include: { roles: true } } },
|
||||||
@@ -43,43 +85,76 @@ if (!existingAdmin) {
|
|||||||
});
|
});
|
||||||
events.emit("role:created", new RoleController(created));
|
events.emit("role:created", new RoleController(created));
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Refresh the internal list of companies every minute
|
// Refresh the internal list of companies every minute
|
||||||
await refreshCompanies();
|
await safeStartup("refreshCompanies", refreshCompanies);
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
return refreshCompanies();
|
return refreshCompanies().catch((err) =>
|
||||||
|
console.error("[interval] refreshCompanies failed", err),
|
||||||
|
);
|
||||||
}, 60 * 1000);
|
}, 60 * 1000);
|
||||||
|
|
||||||
// Refresh the internal catalog every minute
|
// Refresh the internal catalog every minute
|
||||||
await refreshCatalog();
|
await safeStartup("refreshCatalog", refreshCatalog);
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
return refreshCatalog();
|
return refreshCatalog().catch((err) =>
|
||||||
|
console.error("[interval] refreshCatalog failed", err),
|
||||||
|
);
|
||||||
}, 60 * 1000);
|
}, 60 * 1000);
|
||||||
|
|
||||||
// Refresh inventory on hand every 2 minutes
|
// Refresh inventory on hand every 2 minutes
|
||||||
await refreshInventory();
|
await safeStartup("refreshInventory", refreshInventory);
|
||||||
setInterval(
|
setInterval(
|
||||||
() => {
|
() => {
|
||||||
return refreshInventory();
|
return refreshInventory().catch((err) =>
|
||||||
|
console.error("[interval] refreshInventory failed", err),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
2 * 60 * 1000,
|
2 * 60 * 1000,
|
||||||
);
|
);
|
||||||
|
|
||||||
await unifiSites.syncSites();
|
// Refresh opportunities every minute
|
||||||
|
await safeStartup("refreshOpportunities", refreshOpportunities);
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
return unifiSites.syncSites();
|
return refreshOpportunities().catch((err) =>
|
||||||
|
console.error("[interval] refreshOpportunities failed", err),
|
||||||
|
);
|
||||||
}, 60 * 1000);
|
}, 60 * 1000);
|
||||||
|
|
||||||
Bun.serve({
|
// Refresh opportunity CW cache every 30 seconds (activities + company hydration)
|
||||||
port: PORT,
|
await safeStartup("refreshOpportunityCache", refreshOpportunityCache);
|
||||||
websocket: engine.handler().websocket,
|
setInterval(() => {
|
||||||
fetch: (req, server) => {
|
return refreshOpportunityCache().catch((err) =>
|
||||||
const url = new URL(req.url);
|
console.error("[interval] refreshOpportunityCache failed", err),
|
||||||
|
);
|
||||||
|
}, 30 * 1000);
|
||||||
|
|
||||||
if (url.pathname.startsWith("/socket.io/")) {
|
// Refresh User Defined Fields every 5 minutes
|
||||||
return engine.handleRequest(req, server as any);
|
await safeStartup("refreshUDFs", () => userDefinedFieldsCw.refresh());
|
||||||
}
|
setInterval(
|
||||||
|
() => {
|
||||||
return app.fetch(req, server);
|
return userDefinedFieldsCw
|
||||||
|
.refresh()
|
||||||
|
.catch((err) => console.error("[interval] refreshUDFs failed", err));
|
||||||
},
|
},
|
||||||
});
|
5 * 60 * 1000,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Refresh CW identifiers for all users every 30 minutes
|
||||||
|
await safeStartup("refreshCwIdentifiers", refreshCwIdentifiers);
|
||||||
|
setInterval(
|
||||||
|
() => {
|
||||||
|
return refreshCwIdentifiers().catch((err) =>
|
||||||
|
console.error("[interval] refreshCwIdentifiers failed", err),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
30 * 60 * 1000,
|
||||||
|
);
|
||||||
|
|
||||||
|
await safeStartup("syncSites", () => unifiSites.syncSites());
|
||||||
|
setInterval(() => {
|
||||||
|
return unifiSites
|
||||||
|
.syncSites()
|
||||||
|
.catch((err) => console.error("[interval] syncSites failed", err));
|
||||||
|
}, 60 * 1000);
|
||||||
|
|||||||
@@ -0,0 +1,211 @@
|
|||||||
|
import { ActivityController } from "../controllers/ActivityController";
|
||||||
|
import { connectWiseApi } from "../constants";
|
||||||
|
import GenericError from "../Errors/GenericError";
|
||||||
|
import { activityCw } from "../modules/cw-utils/activities/activities";
|
||||||
|
import {
|
||||||
|
CWCreateActivity,
|
||||||
|
CWPatchOperation,
|
||||||
|
} from "../modules/cw-utils/activities/activity.types";
|
||||||
|
|
||||||
|
export const activities = {
|
||||||
|
/**
|
||||||
|
* Fetch Activity
|
||||||
|
*
|
||||||
|
* Fetch a single activity by its ConnectWise activity ID
|
||||||
|
* and return an ActivityController instance.
|
||||||
|
*
|
||||||
|
* @param cwActivityId - The ConnectWise activity ID
|
||||||
|
* @returns {Promise<ActivityController>}
|
||||||
|
*/
|
||||||
|
async fetchItem(cwActivityId: number): Promise<ActivityController> {
|
||||||
|
try {
|
||||||
|
const cwData = await activityCw.fetch(cwActivityId);
|
||||||
|
return new ActivityController(cwData);
|
||||||
|
} catch (error) {
|
||||||
|
const errBody = (error as any).response?.data || error;
|
||||||
|
throw new GenericError({
|
||||||
|
name: "FetchActivityError",
|
||||||
|
message: `Failed to fetch activity ${cwActivityId}`,
|
||||||
|
cause: typeof errBody === "string" ? errBody : JSON.stringify(errBody),
|
||||||
|
status: (error as any).status ?? 502,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch All Activities (Paginated)
|
||||||
|
*
|
||||||
|
* Fetches activities from ConnectWise with optional conditions and pagination.
|
||||||
|
*
|
||||||
|
* @param page - Page number (1-based)
|
||||||
|
* @param rpp - Records per page
|
||||||
|
* @param conditions - Optional CW conditions string for filtering
|
||||||
|
* @returns {Promise<ActivityController[]>}
|
||||||
|
*/
|
||||||
|
async fetchPages(
|
||||||
|
page: number,
|
||||||
|
rpp: number,
|
||||||
|
conditions?: string,
|
||||||
|
): Promise<ActivityController[]> {
|
||||||
|
try {
|
||||||
|
const pageNum = Math.max(page, 1);
|
||||||
|
const conditionsParam = conditions
|
||||||
|
? `&conditions=${encodeURIComponent(conditions)}`
|
||||||
|
: "";
|
||||||
|
const response = await connectWiseApi.get(
|
||||||
|
`/sales/activities?page=${pageNum}&pageSize=${rpp}${conditionsParam}`,
|
||||||
|
);
|
||||||
|
const items = response.data;
|
||||||
|
return items.map((item: any) => new ActivityController(item));
|
||||||
|
} catch (error) {
|
||||||
|
const errBody = (error as any).response?.data || error;
|
||||||
|
throw new GenericError({
|
||||||
|
name: "FetchActivitiesError",
|
||||||
|
message: "Failed to fetch activities from ConnectWise",
|
||||||
|
cause: typeof errBody === "string" ? errBody : JSON.stringify(errBody),
|
||||||
|
status: 502,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch Activities by Company
|
||||||
|
*
|
||||||
|
* Fetches all activities for a company by its ConnectWise company ID.
|
||||||
|
*
|
||||||
|
* @param cwCompanyId - The ConnectWise company ID
|
||||||
|
* @returns {Promise<ActivityController[]>}
|
||||||
|
*/
|
||||||
|
async fetchByCompany(cwCompanyId: number): Promise<ActivityController[]> {
|
||||||
|
try {
|
||||||
|
const collection = await activityCw.fetchByCompany(cwCompanyId);
|
||||||
|
return collection.map((item) => new ActivityController(item));
|
||||||
|
} catch (error) {
|
||||||
|
const errBody = (error as any).response?.data || error;
|
||||||
|
throw new GenericError({
|
||||||
|
name: "FetchCompanyActivitiesError",
|
||||||
|
message: `Failed to fetch activities for company ${cwCompanyId}`,
|
||||||
|
cause: typeof errBody === "string" ? errBody : JSON.stringify(errBody),
|
||||||
|
status: 502,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch Activities by Opportunity
|
||||||
|
*
|
||||||
|
* Fetches all activities for an opportunity by its ConnectWise opportunity ID.
|
||||||
|
*
|
||||||
|
* @param cwOpportunityId - The ConnectWise opportunity ID
|
||||||
|
* @returns {Promise<ActivityController[]>}
|
||||||
|
*/
|
||||||
|
async fetchByOpportunity(
|
||||||
|
cwOpportunityId: number,
|
||||||
|
): Promise<ActivityController[]> {
|
||||||
|
try {
|
||||||
|
const collection = await activityCw.fetchByOpportunity(cwOpportunityId);
|
||||||
|
return collection.map((item) => new ActivityController(item));
|
||||||
|
} catch (error) {
|
||||||
|
const errBody = (error as any).response?.data || error;
|
||||||
|
throw new GenericError({
|
||||||
|
name: "FetchOpportunityActivitiesError",
|
||||||
|
message: `Failed to fetch activities for opportunity ${cwOpportunityId}`,
|
||||||
|
cause: typeof errBody === "string" ? errBody : JSON.stringify(errBody),
|
||||||
|
status: 502,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create Activity
|
||||||
|
*
|
||||||
|
* Creates a new activity in ConnectWise and returns an ActivityController.
|
||||||
|
*
|
||||||
|
* @param data - The activity data to create
|
||||||
|
* @returns {Promise<ActivityController>}
|
||||||
|
*/
|
||||||
|
async create(data: CWCreateActivity): Promise<ActivityController> {
|
||||||
|
try {
|
||||||
|
return await ActivityController.create(data);
|
||||||
|
} catch (error) {
|
||||||
|
const errBody = (error as any).response?.data || error;
|
||||||
|
throw new GenericError({
|
||||||
|
name: "CreateActivityError",
|
||||||
|
message: "Failed to create activity in ConnectWise",
|
||||||
|
cause: typeof errBody === "string" ? errBody : JSON.stringify(errBody),
|
||||||
|
status: 502,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update Activity
|
||||||
|
*
|
||||||
|
* Updates an existing activity in ConnectWise using JSON Patch operations
|
||||||
|
* and returns an updated ActivityController.
|
||||||
|
*
|
||||||
|
* @param cwActivityId - The ConnectWise activity ID to update
|
||||||
|
* @param operations - Array of JSON Patch operations to apply
|
||||||
|
* @returns {Promise<ActivityController>}
|
||||||
|
*/
|
||||||
|
async update(
|
||||||
|
cwActivityId: number,
|
||||||
|
operations: CWPatchOperation[],
|
||||||
|
): Promise<ActivityController> {
|
||||||
|
try {
|
||||||
|
const updated = await activityCw.update(cwActivityId, operations);
|
||||||
|
return new ActivityController(updated);
|
||||||
|
} catch (error) {
|
||||||
|
const errBody = (error as any).response?.data || error;
|
||||||
|
throw new GenericError({
|
||||||
|
name: "UpdateActivityError",
|
||||||
|
message: `Failed to update activity ${cwActivityId}`,
|
||||||
|
cause: typeof errBody === "string" ? errBody : JSON.stringify(errBody),
|
||||||
|
status: 502,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete Activity
|
||||||
|
*
|
||||||
|
* Deletes an activity from ConnectWise.
|
||||||
|
*
|
||||||
|
* @param cwActivityId - The ConnectWise activity ID to delete
|
||||||
|
*/
|
||||||
|
async delete(cwActivityId: number): Promise<void> {
|
||||||
|
try {
|
||||||
|
await activityCw.delete(cwActivityId);
|
||||||
|
} catch (error) {
|
||||||
|
const errBody = (error as any).response?.data || error;
|
||||||
|
throw new GenericError({
|
||||||
|
name: "DeleteActivityError",
|
||||||
|
message: `Failed to delete activity ${cwActivityId}`,
|
||||||
|
cause: typeof errBody === "string" ? errBody : JSON.stringify(errBody),
|
||||||
|
status: 502,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Count Activities
|
||||||
|
*
|
||||||
|
* Returns the total number of activities, optionally filtered.
|
||||||
|
*
|
||||||
|
* @param conditions - Optional CW conditions string for filtering
|
||||||
|
* @returns {Promise<number>}
|
||||||
|
*/
|
||||||
|
async count(conditions?: string): Promise<number> {
|
||||||
|
try {
|
||||||
|
return await activityCw.countItems(conditions);
|
||||||
|
} catch (error) {
|
||||||
|
const errBody = (error as any).response?.data || error;
|
||||||
|
throw new GenericError({
|
||||||
|
name: "CountActivitiesError",
|
||||||
|
message: "Failed to count activities in ConnectWise",
|
||||||
|
cause: typeof errBody === "string" ? errBody : JSON.stringify(errBody),
|
||||||
|
status: 502,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -15,16 +15,19 @@ export const companies = {
|
|||||||
const freshCwData: { data: Company } = await connectWiseApi.get(
|
const freshCwData: { data: Company } = await connectWiseApi.get(
|
||||||
`/company/companies/${search.cw_CompanyId}`,
|
`/company/companies/${search.cw_CompanyId}`,
|
||||||
);
|
);
|
||||||
const defaultContactData = await connectWiseApi.get(
|
|
||||||
(freshCwData.data as Company).defaultContact._info.contact_href,
|
const contactHref = freshCwData.data.defaultContact?._info?.contact_href;
|
||||||
);
|
const defaultContactData = contactHref
|
||||||
|
? await connectWiseApi.get(contactHref)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
const allContactsData = await connectWiseApi.get(
|
const allContactsData = await connectWiseApi.get(
|
||||||
`${freshCwData.data._info.contacts_href}&pageSize=1000`,
|
`${freshCwData.data._info.contacts_href}&pageSize=1000`,
|
||||||
);
|
);
|
||||||
|
|
||||||
return new CompanyController(search, {
|
return new CompanyController(search, {
|
||||||
company: freshCwData.data,
|
company: freshCwData.data,
|
||||||
defaultContact: defaultContactData.data,
|
defaultContact: defaultContactData?.data ?? null,
|
||||||
allContacts: allContactsData.data,
|
allContacts: allContactsData.data,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,412 @@
|
|||||||
|
import { Company } from "../../generated/prisma/client";
|
||||||
|
import { prisma } from "../constants";
|
||||||
|
import { ActivityController } from "../controllers/ActivityController";
|
||||||
|
import { CompanyController } from "../controllers/CompanyController";
|
||||||
|
import { OpportunityController } from "../controllers/OpportunityController";
|
||||||
|
import GenericError from "../Errors/GenericError";
|
||||||
|
import { activityCw } from "../modules/cw-utils/activities/activities";
|
||||||
|
import { opportunityCw } from "../modules/cw-utils/opportunities/opportunities";
|
||||||
|
import { computeCacheTTL } from "../modules/algorithms/computeCacheTTL";
|
||||||
|
import {
|
||||||
|
getCachedActivities,
|
||||||
|
getCachedCompanyCwData,
|
||||||
|
fetchAndCacheActivities,
|
||||||
|
fetchAndCacheCompanyCwData,
|
||||||
|
} from "../modules/cache/opportunityCache";
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Data-source hierarchy helpers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build a CompanyController with hydrated CW data from a Prisma Company record.
|
||||||
|
*
|
||||||
|
* Data-source hierarchy (controlled by `strategy`):
|
||||||
|
*
|
||||||
|
* - `"cache-only"` — Redis cache → bare DB record (no CW call).
|
||||||
|
* Ideal for list views where latency matters and the background
|
||||||
|
* refresh job is responsible for keeping the cache warm.
|
||||||
|
*
|
||||||
|
* - `"cache-then-cw"` (default) — Redis cache → CW API → cache result.
|
||||||
|
* On a cold cache, calls CW to ensure the caller gets full data.
|
||||||
|
*
|
||||||
|
* - `"cw-first"` — CW API (always) → cache result.
|
||||||
|
* Forces a fresh fetch regardless of cache state.
|
||||||
|
*/
|
||||||
|
async function buildCompanyController(
|
||||||
|
company: Company,
|
||||||
|
opts?: {
|
||||||
|
strategy?: "cache-only" | "cache-then-cw" | "cw-first";
|
||||||
|
ttlMs?: number;
|
||||||
|
},
|
||||||
|
): Promise<CompanyController> {
|
||||||
|
const strategy = opts?.strategy ?? "cache-then-cw";
|
||||||
|
const ctrl = new CompanyController(company);
|
||||||
|
|
||||||
|
// ── cw-first: always fetch from CW ──────────────────────────────────
|
||||||
|
if (strategy === "cw-first") {
|
||||||
|
await ctrl.hydrateCwData();
|
||||||
|
if (ctrl.cw_Data && opts?.ttlMs) {
|
||||||
|
await fetchAndCacheCompanyCwData(company.cw_CompanyId, opts.ttlMs).catch(
|
||||||
|
() => {},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return ctrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── cache-only / cache-then-cw: try Redis first ─────────────────────
|
||||||
|
const cached = await getCachedCompanyCwData(company.cw_CompanyId);
|
||||||
|
if (cached) {
|
||||||
|
ctrl.cw_Data = cached;
|
||||||
|
return ctrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
// cache-only stops here — return the bare DB-backed controller
|
||||||
|
if (strategy === "cache-only") return ctrl;
|
||||||
|
|
||||||
|
// cache-then-cw: cache miss — fall through to CW
|
||||||
|
await ctrl.hydrateCwData();
|
||||||
|
if (ctrl.cw_Data && opts?.ttlMs) {
|
||||||
|
await fetchAndCacheCompanyCwData(company.cw_CompanyId, opts.ttlMs).catch(
|
||||||
|
() => {},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch ActivityController[] for an opportunity.
|
||||||
|
*
|
||||||
|
* Same three strategies as {@link buildCompanyController}:
|
||||||
|
*
|
||||||
|
* - `"cache-only"` — Redis → empty array (no CW call).
|
||||||
|
* - `"cache-then-cw"` (default) — Redis → CW API → cache result.
|
||||||
|
* - `"cw-first"` — CW API (always) → cache result.
|
||||||
|
*/
|
||||||
|
async function buildActivities(
|
||||||
|
cwOpportunityId: number,
|
||||||
|
opts?: {
|
||||||
|
strategy?: "cache-only" | "cache-then-cw" | "cw-first";
|
||||||
|
ttlMs?: number;
|
||||||
|
},
|
||||||
|
): Promise<ActivityController[]> {
|
||||||
|
const strategy = opts?.strategy ?? "cache-then-cw";
|
||||||
|
|
||||||
|
// ── cw-first: always fetch from CW ──────────────────────────────────
|
||||||
|
if (strategy === "cw-first") {
|
||||||
|
const collection = await activityCw.fetchByOpportunity(cwOpportunityId);
|
||||||
|
const arr = collection.map((item) => item);
|
||||||
|
if (opts?.ttlMs) {
|
||||||
|
await fetchAndCacheActivities(cwOpportunityId, opts.ttlMs).catch(
|
||||||
|
() => {},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return arr.map((item) => new ActivityController(item));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── cache-only / cache-then-cw: try Redis first ─────────────────────
|
||||||
|
const cached = await getCachedActivities(cwOpportunityId);
|
||||||
|
if (cached) {
|
||||||
|
return cached.map((item) => new ActivityController(item));
|
||||||
|
}
|
||||||
|
|
||||||
|
// cache-only stops here — return empty (background job will fill it)
|
||||||
|
if (strategy === "cache-only") return [];
|
||||||
|
|
||||||
|
// cache-then-cw: cache miss — fall through to CW
|
||||||
|
const collection = await activityCw.fetchByOpportunity(cwOpportunityId);
|
||||||
|
const arr = collection.map((item) => item);
|
||||||
|
if (opts?.ttlMs) {
|
||||||
|
await fetchAndCacheActivities(cwOpportunityId, opts.ttlMs).catch(() => {});
|
||||||
|
}
|
||||||
|
return arr.map((item) => new ActivityController(item));
|
||||||
|
}
|
||||||
|
|
||||||
|
export const opportunities = {
|
||||||
|
/**
|
||||||
|
* Fetch Opportunity
|
||||||
|
*
|
||||||
|
* Fetch an opportunity by its internal ID or ConnectWise opportunity ID
|
||||||
|
* and return an OpportunityController instance.
|
||||||
|
*
|
||||||
|
* **Data-source strategy:**
|
||||||
|
* - `fresh: true` → `"cw-first"` — always fetches from CW, updates DB, caches result.
|
||||||
|
* - `fresh: false` (default) → `"cache-then-cw"` — tries Redis first, falls back to CW on miss.
|
||||||
|
*
|
||||||
|
* Individual fetches always contact CW to update the DB record with
|
||||||
|
* the latest data from ConnectWise, regardless of the cache strategy
|
||||||
|
* for activities/company hydration.
|
||||||
|
*
|
||||||
|
* @param identifier - The internal ID (string) or CW opportunity ID (number)
|
||||||
|
* @param opts - Optional flags
|
||||||
|
* @param opts.fresh - When `true`, bypass the cache and pull directly from CW.
|
||||||
|
* @returns {Promise<OpportunityController>}
|
||||||
|
*/
|
||||||
|
async fetchItem(
|
||||||
|
identifier: string | number,
|
||||||
|
opts?: { fresh?: boolean },
|
||||||
|
): Promise<OpportunityController> {
|
||||||
|
const strategy: "cache-only" | "cache-then-cw" | "cw-first" = opts?.fresh
|
||||||
|
? "cw-first"
|
||||||
|
: "cache-then-cw";
|
||||||
|
const isNumeric =
|
||||||
|
typeof identifier === "number" || /^\d+$/.test(String(identifier));
|
||||||
|
|
||||||
|
// Look up the existing DB record to get the cwOpportunityId
|
||||||
|
const existing = await prisma.opportunity.findFirst({
|
||||||
|
where: isNumeric
|
||||||
|
? { cwOpportunityId: Number(identifier) }
|
||||||
|
: { id: identifier as string },
|
||||||
|
select: { id: true, cwOpportunityId: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!existing) {
|
||||||
|
throw new GenericError({
|
||||||
|
message: "Opportunity not found",
|
||||||
|
name: "OpportunityNotFound",
|
||||||
|
cause: `No opportunity exists with identifier '${identifier}'`,
|
||||||
|
status: 404,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch fresh data from ConnectWise
|
||||||
|
const cwData = await opportunityCw.fetch(existing.cwOpportunityId);
|
||||||
|
|
||||||
|
// Map and update the DB record
|
||||||
|
const mapped = OpportunityController.mapCwToDb(cwData);
|
||||||
|
|
||||||
|
// Resolve internal company link
|
||||||
|
const companyId = cwData.company?.id
|
||||||
|
? ((
|
||||||
|
await prisma.company.findFirst({
|
||||||
|
where: { cw_CompanyId: cwData.company.id },
|
||||||
|
select: { id: true },
|
||||||
|
})
|
||||||
|
)?.id ?? null)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const updated = await prisma.opportunity.update({
|
||||||
|
where: { id: existing.id },
|
||||||
|
data: { ...mapped, companyId },
|
||||||
|
include: { company: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
const ttlMs =
|
||||||
|
computeCacheTTL({
|
||||||
|
closedFlag: updated.closedFlag,
|
||||||
|
closedDate: updated.closedDate,
|
||||||
|
expectedCloseDate: updated.expectedCloseDate,
|
||||||
|
lastUpdated: updated.cwLastUpdated,
|
||||||
|
}) ?? undefined;
|
||||||
|
|
||||||
|
const activities = await buildActivities(updated.cwOpportunityId, {
|
||||||
|
strategy,
|
||||||
|
ttlMs,
|
||||||
|
});
|
||||||
|
|
||||||
|
return new OpportunityController(updated, {
|
||||||
|
company: updated.company
|
||||||
|
? await buildCompanyController(updated.company, { strategy, ttlMs })
|
||||||
|
: undefined,
|
||||||
|
customFields: cwData.customFields ?? [],
|
||||||
|
activities,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch All Opportunities (Paginated)
|
||||||
|
*
|
||||||
|
* Uses the **cache-only** strategy: Redis → bare DB data.
|
||||||
|
* Activities and company hydration come from the Redis cache if
|
||||||
|
* available; otherwise the controller is returned with DB-only data.
|
||||||
|
* The background refresh job is responsible for keeping Redis warm.
|
||||||
|
*
|
||||||
|
* @param page - Page number (1-based)
|
||||||
|
* @param rpp - Records per page
|
||||||
|
* @param opts - Optional filters
|
||||||
|
* @returns {Promise<OpportunityController[]>}
|
||||||
|
*/
|
||||||
|
async fetchPages(
|
||||||
|
page: number,
|
||||||
|
rpp: number,
|
||||||
|
opts?: { includeClosed?: boolean },
|
||||||
|
): Promise<OpportunityController[]> {
|
||||||
|
const skip = (Math.max(page, 1) - 1) * rpp;
|
||||||
|
|
||||||
|
const items = await prisma.opportunity.findMany({
|
||||||
|
where: opts?.includeClosed ? undefined : { closedFlag: false },
|
||||||
|
include: { company: true },
|
||||||
|
skip,
|
||||||
|
take: rpp,
|
||||||
|
orderBy: { createdAt: "desc" },
|
||||||
|
});
|
||||||
|
|
||||||
|
return Promise.all(
|
||||||
|
items.map(async (item) => {
|
||||||
|
return new OpportunityController(item, {
|
||||||
|
company: item.company
|
||||||
|
? await buildCompanyController(item.company, {
|
||||||
|
strategy: "cache-only",
|
||||||
|
})
|
||||||
|
: undefined,
|
||||||
|
activities: await buildActivities(item.cwOpportunityId, {
|
||||||
|
strategy: "cache-only",
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search Opportunities
|
||||||
|
*
|
||||||
|
* Search opportunities by name, company name, contact name, notes,
|
||||||
|
* sales rep, or status with pagination support.
|
||||||
|
*
|
||||||
|
* Uses the **cache-only** strategy (same as `fetchPages`).
|
||||||
|
*
|
||||||
|
* @param query - Search query string
|
||||||
|
* @param page - Page number (1-based)
|
||||||
|
* @param rpp - Records per page
|
||||||
|
* @param opts - Optional filters
|
||||||
|
* @returns {Promise<OpportunityController[]>}
|
||||||
|
*/
|
||||||
|
async search(
|
||||||
|
query: string,
|
||||||
|
page: number,
|
||||||
|
rpp: number,
|
||||||
|
opts?: { includeClosed?: boolean },
|
||||||
|
): Promise<OpportunityController[]> {
|
||||||
|
const skip = (Math.max(page, 1) - 1) * rpp;
|
||||||
|
const numericQuery = /^\d+$/.test(query.trim())
|
||||||
|
? Number(query.trim())
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const items = await prisma.opportunity.findMany({
|
||||||
|
where: {
|
||||||
|
...(opts?.includeClosed ? {} : { closedFlag: false }),
|
||||||
|
OR: [
|
||||||
|
{ name: { contains: query, mode: "insensitive" } },
|
||||||
|
{ companyName: { contains: query, mode: "insensitive" } },
|
||||||
|
{ contactName: { contains: query, mode: "insensitive" } },
|
||||||
|
{ notes: { contains: query, mode: "insensitive" } },
|
||||||
|
{ primarySalesRepName: { contains: query, mode: "insensitive" } },
|
||||||
|
{ statusName: { contains: query, mode: "insensitive" } },
|
||||||
|
{ stageName: { contains: query, mode: "insensitive" } },
|
||||||
|
...(numericQuery !== null
|
||||||
|
? [{ cwOpportunityId: { equals: numericQuery } }]
|
||||||
|
: []),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
include: { company: true },
|
||||||
|
skip,
|
||||||
|
take: rpp,
|
||||||
|
orderBy: { expectedCloseDate: "asc" },
|
||||||
|
});
|
||||||
|
|
||||||
|
return Promise.all(
|
||||||
|
items.map(async (item) => {
|
||||||
|
return new OpportunityController(item, {
|
||||||
|
company: item.company
|
||||||
|
? await buildCompanyController(item.company, {
|
||||||
|
strategy: "cache-only",
|
||||||
|
})
|
||||||
|
: undefined,
|
||||||
|
activities: await buildActivities(item.cwOpportunityId, {
|
||||||
|
strategy: "cache-only",
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Count Opportunities
|
||||||
|
*
|
||||||
|
* @param opts - Optional filters
|
||||||
|
* @returns {Promise<number>}
|
||||||
|
*/
|
||||||
|
async count(opts?: { openOnly?: boolean }): Promise<number> {
|
||||||
|
return prisma.opportunity.count({
|
||||||
|
where: opts?.openOnly ? { closedFlag: false } : undefined,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Count Search Results
|
||||||
|
*
|
||||||
|
* Returns the total number of opportunities matching a search query,
|
||||||
|
* using the same filter logic as `search()`.
|
||||||
|
*
|
||||||
|
* @param query - Search query string
|
||||||
|
* @param opts - Optional filters
|
||||||
|
* @returns {Promise<number>}
|
||||||
|
*/
|
||||||
|
async searchCount(
|
||||||
|
query: string,
|
||||||
|
opts?: { includeClosed?: boolean },
|
||||||
|
): Promise<number> {
|
||||||
|
const numericQuery = /^\d+$/.test(query.trim())
|
||||||
|
? Number(query.trim())
|
||||||
|
: null;
|
||||||
|
|
||||||
|
return prisma.opportunity.count({
|
||||||
|
where: {
|
||||||
|
...(opts?.includeClosed ? {} : { closedFlag: false }),
|
||||||
|
OR: [
|
||||||
|
{ name: { contains: query, mode: "insensitive" } },
|
||||||
|
{ companyName: { contains: query, mode: "insensitive" } },
|
||||||
|
{ contactName: { contains: query, mode: "insensitive" } },
|
||||||
|
{ notes: { contains: query, mode: "insensitive" } },
|
||||||
|
{ primarySalesRepName: { contains: query, mode: "insensitive" } },
|
||||||
|
{ statusName: { contains: query, mode: "insensitive" } },
|
||||||
|
{ stageName: { contains: query, mode: "insensitive" } },
|
||||||
|
...(numericQuery !== null
|
||||||
|
? [{ cwOpportunityId: { equals: numericQuery } }]
|
||||||
|
: []),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch Opportunities by Company
|
||||||
|
*
|
||||||
|
* Fetch all opportunities for a company by its internal company ID.
|
||||||
|
*
|
||||||
|
* Uses the **cache-only** strategy (same as `fetchPages`).
|
||||||
|
*
|
||||||
|
* @param companyId - The internal company ID
|
||||||
|
* @param opts - Optional filters
|
||||||
|
* @returns {Promise<OpportunityController[]>}
|
||||||
|
*/
|
||||||
|
async fetchByCompany(
|
||||||
|
companyId: string,
|
||||||
|
opts?: { includeClosed?: boolean },
|
||||||
|
): Promise<OpportunityController[]> {
|
||||||
|
const items = await prisma.opportunity.findMany({
|
||||||
|
where: {
|
||||||
|
companyId,
|
||||||
|
...(opts?.includeClosed ? {} : { closedFlag: false }),
|
||||||
|
},
|
||||||
|
include: { company: true },
|
||||||
|
orderBy: { expectedCloseDate: "asc" },
|
||||||
|
});
|
||||||
|
|
||||||
|
return Promise.all(
|
||||||
|
items.map(async (item) => {
|
||||||
|
return new OpportunityController(item, {
|
||||||
|
company: item.company
|
||||||
|
? await buildCompanyController(item.company, {
|
||||||
|
strategy: "cache-only",
|
||||||
|
})
|
||||||
|
: undefined,
|
||||||
|
activities: await buildActivities(item.cwOpportunityId, {
|
||||||
|
strategy: "cache-only",
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,332 @@
|
|||||||
|
import { prisma } from "../constants";
|
||||||
|
import { CatalogItemController } from "../controllers/CatalogItemController";
|
||||||
|
import GenericError from "../Errors/GenericError";
|
||||||
|
import {
|
||||||
|
getSubcategoriesForCategory,
|
||||||
|
getSubcategoriesForGroup,
|
||||||
|
ECOSYSTEM_TREE,
|
||||||
|
} from "../modules/catalog-categories/catalogCategories";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Standard include clause used by catalog item queries.
|
||||||
|
* Includes one level of linked items.
|
||||||
|
*/
|
||||||
|
const catalogItemInclude = {
|
||||||
|
linkedItems: true,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter options for catalog item queries.
|
||||||
|
*/
|
||||||
|
export interface CatalogFilterOpts {
|
||||||
|
includeInactive?: boolean;
|
||||||
|
category?: string;
|
||||||
|
subcategory?: string;
|
||||||
|
group?: string;
|
||||||
|
manufacturer?: string;
|
||||||
|
ecosystem?: string;
|
||||||
|
inStock?: boolean;
|
||||||
|
minPrice?: number;
|
||||||
|
maxPrice?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a Prisma `where` clause from filter options.
|
||||||
|
*/
|
||||||
|
function buildFilterWhere(opts: CatalogFilterOpts = {}) {
|
||||||
|
const conditions: Record<string, unknown>[] = [];
|
||||||
|
|
||||||
|
if (!opts.includeInactive) {
|
||||||
|
conditions.push({ inactive: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opts.category) {
|
||||||
|
conditions.push({ category: opts.category });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opts.subcategory) {
|
||||||
|
conditions.push({ subcategory: opts.subcategory });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opts.group && opts.category) {
|
||||||
|
const subcats = getSubcategoriesForGroup(opts.category, opts.group);
|
||||||
|
if (subcats.length > 0) {
|
||||||
|
conditions.push({ subcategory: { in: subcats } });
|
||||||
|
}
|
||||||
|
} else if (opts.group && !opts.category) {
|
||||||
|
// Try to find the group in any category
|
||||||
|
const {
|
||||||
|
CATEGORY_TREE,
|
||||||
|
isCategoryGroup,
|
||||||
|
} = require("../modules/catalog-categories/catalogCategories");
|
||||||
|
for (const cat of CATEGORY_TREE) {
|
||||||
|
const subcats = getSubcategoriesForGroup(cat.name, opts.group);
|
||||||
|
if (subcats.length > 0) {
|
||||||
|
conditions.push({ category: cat.name, subcategory: { in: subcats } });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opts.manufacturer) {
|
||||||
|
conditions.push({
|
||||||
|
manufacturer: { contains: opts.manufacturer, mode: "insensitive" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opts.ecosystem) {
|
||||||
|
const eco = ECOSYSTEM_TREE.find(
|
||||||
|
(e) => e.name.toLowerCase() === opts.ecosystem!.toLowerCase(),
|
||||||
|
);
|
||||||
|
if (eco && eco.manufacturers.length > 0) {
|
||||||
|
conditions.push({
|
||||||
|
OR: eco.manufacturers.map((m) => ({
|
||||||
|
manufacturer: { contains: m.name, mode: "insensitive" as const },
|
||||||
|
subcategory: { startsWith: m.subcategoryPrefix },
|
||||||
|
category: m.category,
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opts.inStock) {
|
||||||
|
conditions.push({ onHand: { gt: 0 } });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opts.minPrice !== undefined) {
|
||||||
|
conditions.push({ price: { gte: opts.minPrice } });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opts.maxPrice !== undefined) {
|
||||||
|
conditions.push({ price: { lte: opts.maxPrice } });
|
||||||
|
}
|
||||||
|
|
||||||
|
return conditions.length > 0 ? { AND: conditions } : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const procurement = {
|
||||||
|
/**
|
||||||
|
* Fetch Catalog Item
|
||||||
|
*
|
||||||
|
* Fetch a catalog item by its internal ID or ConnectWise catalog ID
|
||||||
|
* and return a CatalogItemController instance.
|
||||||
|
*
|
||||||
|
* @param identifier - The internal ID (string) or CW catalog ID (number)
|
||||||
|
* @returns {Promise<CatalogItemController>} - The catalog item controller
|
||||||
|
*/
|
||||||
|
async fetchItem(identifier: string | number): Promise<CatalogItemController> {
|
||||||
|
const isNumeric =
|
||||||
|
typeof identifier === "number" || /^\d+$/.test(String(identifier));
|
||||||
|
|
||||||
|
const item = await prisma.catalogItem.findFirst({
|
||||||
|
where: isNumeric
|
||||||
|
? { cwCatalogId: Number(identifier) }
|
||||||
|
: {
|
||||||
|
OR: [
|
||||||
|
{ id: identifier as string },
|
||||||
|
{ identifier: identifier as string },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
include: catalogItemInclude,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!item) {
|
||||||
|
throw new GenericError({
|
||||||
|
message: "Catalog item not found",
|
||||||
|
name: "CatalogItemNotFound",
|
||||||
|
cause: `No catalog item exists with identifier '${identifier}'`,
|
||||||
|
status: 404,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return new CatalogItemController(item);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch All Catalog Items (Paginated)
|
||||||
|
*
|
||||||
|
* Fetch pages of catalog items for pagination with optional filtering.
|
||||||
|
*
|
||||||
|
* @param page - Page number (1-based)
|
||||||
|
* @param rpp - Records per page
|
||||||
|
* @param opts - Filter options
|
||||||
|
* @returns {Promise<CatalogItemController[]>} - Array of catalog item controllers
|
||||||
|
*/
|
||||||
|
async fetchPages(
|
||||||
|
page: number,
|
||||||
|
rpp: number,
|
||||||
|
opts?: CatalogFilterOpts,
|
||||||
|
): Promise<CatalogItemController[]> {
|
||||||
|
const skip = (Math.max(page, 1) - 1) * rpp;
|
||||||
|
const take = rpp;
|
||||||
|
|
||||||
|
const items = await prisma.catalogItem.findMany({
|
||||||
|
where: buildFilterWhere(opts),
|
||||||
|
skip,
|
||||||
|
take,
|
||||||
|
include: catalogItemInclude,
|
||||||
|
orderBy: { name: "asc" },
|
||||||
|
});
|
||||||
|
|
||||||
|
return items.map((item) => new CatalogItemController(item));
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search Catalog Items
|
||||||
|
*
|
||||||
|
* Search catalog items by name, description, part number, or vendor SKU
|
||||||
|
* with pagination support and optional category/subcategory/ecosystem filters.
|
||||||
|
*
|
||||||
|
* @param query - Search query string
|
||||||
|
* @param page - Page number (1-based)
|
||||||
|
* @param rpp - Records per page
|
||||||
|
* @param opts - Filter options
|
||||||
|
* @returns {Promise<CatalogItemController[]>} - Array of matching catalog item controllers
|
||||||
|
*/
|
||||||
|
async search(
|
||||||
|
query: string,
|
||||||
|
page: number,
|
||||||
|
rpp: number,
|
||||||
|
opts?: CatalogFilterOpts,
|
||||||
|
): Promise<CatalogItemController[]> {
|
||||||
|
const skip = (Math.max(page, 1) - 1) * rpp;
|
||||||
|
const take = rpp;
|
||||||
|
|
||||||
|
const filterWhere = buildFilterWhere(opts) ?? {};
|
||||||
|
|
||||||
|
const items = await prisma.catalogItem.findMany({
|
||||||
|
where: {
|
||||||
|
...filterWhere,
|
||||||
|
OR: [
|
||||||
|
{ identifier: { contains: query, mode: "insensitive" } },
|
||||||
|
{ name: { contains: query, mode: "insensitive" } },
|
||||||
|
{ description: { contains: query, mode: "insensitive" } },
|
||||||
|
{ partNumber: { contains: query, mode: "insensitive" } },
|
||||||
|
{ vendorSku: { contains: query, mode: "insensitive" } },
|
||||||
|
{ manufacturer: { contains: query, mode: "insensitive" } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
skip,
|
||||||
|
take,
|
||||||
|
include: catalogItemInclude,
|
||||||
|
orderBy: { name: "asc" },
|
||||||
|
});
|
||||||
|
|
||||||
|
return items.map((item) => new CatalogItemController(item));
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Count Catalog Items
|
||||||
|
*
|
||||||
|
* Returns the total number of catalog items matching the given filters.
|
||||||
|
*
|
||||||
|
* @param opts - Filter options
|
||||||
|
* @returns {Promise<number>} - Total count
|
||||||
|
*/
|
||||||
|
async count(
|
||||||
|
opts?: CatalogFilterOpts & { activeOnly?: boolean },
|
||||||
|
): Promise<number> {
|
||||||
|
// Support legacy `activeOnly` flag by mapping it to `includeInactive`
|
||||||
|
const filterOpts: CatalogFilterOpts = {
|
||||||
|
...opts,
|
||||||
|
includeInactive:
|
||||||
|
opts?.includeInactive ?? (opts?.activeOnly ? false : true),
|
||||||
|
};
|
||||||
|
if (opts?.activeOnly) filterOpts.includeInactive = false;
|
||||||
|
|
||||||
|
return prisma.catalogItem.count({
|
||||||
|
where: buildFilterWhere(filterOpts),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Count Catalog Items (with search query)
|
||||||
|
*
|
||||||
|
* Returns the total number of catalog items matching a search query and filters.
|
||||||
|
*
|
||||||
|
* @param query - Search query string
|
||||||
|
* @param opts - Filter options
|
||||||
|
* @returns {Promise<number>} - Total count
|
||||||
|
*/
|
||||||
|
async countSearch(query: string, opts?: CatalogFilterOpts): Promise<number> {
|
||||||
|
const filterWhere = buildFilterWhere(opts) ?? {};
|
||||||
|
|
||||||
|
return prisma.catalogItem.count({
|
||||||
|
where: {
|
||||||
|
...filterWhere,
|
||||||
|
OR: [
|
||||||
|
{ identifier: { contains: query, mode: "insensitive" } },
|
||||||
|
{ name: { contains: query, mode: "insensitive" } },
|
||||||
|
{ description: { contains: query, mode: "insensitive" } },
|
||||||
|
{ partNumber: { contains: query, mode: "insensitive" } },
|
||||||
|
{ vendorSku: { contains: query, mode: "insensitive" } },
|
||||||
|
{ manufacturer: { contains: query, mode: "insensitive" } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch Distinct Values
|
||||||
|
*
|
||||||
|
* Returns the distinct values for a given field across all catalog items.
|
||||||
|
* Useful for populating filter dropdowns in the UI.
|
||||||
|
*
|
||||||
|
* @param field - The field to get distinct values for
|
||||||
|
* @param opts - Filter options to scope the distinct query
|
||||||
|
* @returns {Promise<string[]>} - Sorted array of distinct non-null values
|
||||||
|
*/
|
||||||
|
async fetchDistinctValues(
|
||||||
|
field: "category" | "subcategory" | "manufacturer",
|
||||||
|
opts?: CatalogFilterOpts,
|
||||||
|
): Promise<string[]> {
|
||||||
|
const items = await prisma.catalogItem.findMany({
|
||||||
|
where: buildFilterWhere(opts),
|
||||||
|
select: { [field]: true },
|
||||||
|
distinct: [field],
|
||||||
|
orderBy: { [field]: "asc" },
|
||||||
|
});
|
||||||
|
|
||||||
|
return items
|
||||||
|
.map((item: Record<string, unknown>) => item[field] as string | null)
|
||||||
|
.filter((v): v is string => v !== null);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Link Catalog Items
|
||||||
|
*
|
||||||
|
* Links a target catalog item to a source catalog item.
|
||||||
|
*
|
||||||
|
* @param sourceIdentifier - The source item's internal ID, identifier, or CW catalog ID
|
||||||
|
* @param targetIdentifier - The target item's internal ID, identifier, or CW catalog ID
|
||||||
|
* @returns {Promise<CatalogItemController>} - The updated source controller with linked items
|
||||||
|
*/
|
||||||
|
async linkItems(
|
||||||
|
sourceIdentifier: string | number,
|
||||||
|
targetIdentifier: string | number,
|
||||||
|
): Promise<CatalogItemController> {
|
||||||
|
const source = await procurement.fetchItem(sourceIdentifier);
|
||||||
|
const target = await procurement.fetchItem(targetIdentifier);
|
||||||
|
|
||||||
|
return source.linkItem(target.id);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unlink Catalog Items
|
||||||
|
*
|
||||||
|
* Removes the link between a source catalog item and a target catalog item.
|
||||||
|
*
|
||||||
|
* @param sourceIdentifier - The source item's internal ID, identifier, or CW catalog ID
|
||||||
|
* @param targetIdentifier - The target item's internal ID, identifier, or CW catalog ID
|
||||||
|
* @returns {Promise<CatalogItemController>} - The updated source controller
|
||||||
|
*/
|
||||||
|
async unlinkItems(
|
||||||
|
sourceIdentifier: string | number,
|
||||||
|
targetIdentifier: string | number,
|
||||||
|
): Promise<CatalogItemController> {
|
||||||
|
const source = await procurement.fetchItem(sourceIdentifier);
|
||||||
|
const target = await procurement.fetchItem(targetIdentifier);
|
||||||
|
|
||||||
|
return source.unlinkItem(target.id);
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -4,6 +4,7 @@ import { prisma } from "../constants";
|
|||||||
import { SessionTokensObject } from "../controllers/SessionController";
|
import { SessionTokensObject } from "../controllers/SessionController";
|
||||||
import UserController from "../controllers/UserController";
|
import UserController from "../controllers/UserController";
|
||||||
import { fetchMicrosoftUser } from "../modules/fetchMicrosoftUser";
|
import { fetchMicrosoftUser } from "../modules/fetchMicrosoftUser";
|
||||||
|
import { findCwIdentifierByEmail } from "../modules/cw-utils/members/fetchAllMembers";
|
||||||
import { events } from "../modules/globalEvents";
|
import { events } from "../modules/globalEvents";
|
||||||
import { sessions } from "./sessions";
|
import { sessions } from "./sessions";
|
||||||
import * as msal from "@azure/msal-node";
|
import * as msal from "@azure/msal-node";
|
||||||
@@ -90,12 +91,18 @@ export const users = {
|
|||||||
async createUser(token: string): Promise<UserController> {
|
async createUser(token: string): Promise<UserController> {
|
||||||
const msData = await fetchMicrosoftUser(token);
|
const msData = await fetchMicrosoftUser(token);
|
||||||
|
|
||||||
|
// Attempt to resolve the user's ConnectWise identifier by email
|
||||||
|
const cwIdentifier = await findCwIdentifierByEmail(msData.mail).catch(
|
||||||
|
() => null,
|
||||||
|
);
|
||||||
|
|
||||||
const newUser = await prisma.user.create({
|
const newUser = await prisma.user.create({
|
||||||
data: {
|
data: {
|
||||||
userId: msData.id,
|
userId: msData.id,
|
||||||
email: msData.mail,
|
email: msData.mail,
|
||||||
name: `${msData.givenName} ${msData.surname}`,
|
name: `${msData.givenName} ${msData.surname}`,
|
||||||
login: msData.userPrincipalName,
|
login: msData.userPrincipalName,
|
||||||
|
cwIdentifier,
|
||||||
token,
|
token,
|
||||||
},
|
},
|
||||||
include: { roles: true },
|
include: { roles: true },
|
||||||
|
|||||||
@@ -0,0 +1,166 @@
|
|||||||
|
/**
|
||||||
|
* @module computeCacheTTL
|
||||||
|
*
|
||||||
|
* Adaptive Cache TTL Algorithm
|
||||||
|
* ============================
|
||||||
|
*
|
||||||
|
* Determines how long a cached record should live before it must be
|
||||||
|
* re-fetched from the upstream source (e.g. ConnectWise API).
|
||||||
|
*
|
||||||
|
* The algorithm prioritises freshness for records that are actively
|
||||||
|
* being worked on, while avoiding unnecessary API calls for stale or
|
||||||
|
* inactive data.
|
||||||
|
*
|
||||||
|
* ## Spec
|
||||||
|
*
|
||||||
|
* | # | Condition | TTL (ms) | TTL (human) | Rationale |
|
||||||
|
* |---|------------------------------------------------------------------|----------|-------------|--------------------------------------------------------------------|
|
||||||
|
* | 1 | `closedFlag` is `true` | `null` | Do not cache| Closed records are rarely accessed; caching wastes memory. |
|
||||||
|
* | 2 | `expectedCloseDate` OR `lastUpdated` is within the last **5 days**| 30 000 | 30 seconds | High-activity window — data changes frequently and must stay fresh.|
|
||||||
|
* | 3 | `expectedCloseDate` OR `lastUpdated` is within the last **14 days**| 60 000 | 60 seconds | Moderate activity — still relevant, but changes less often. |
|
||||||
|
* | 4 | Everything else (older than 14 days) | 900 000 | 15 minutes | Low activity — safe to serve from cache for longer. |
|
||||||
|
*
|
||||||
|
* ## Evaluation order
|
||||||
|
*
|
||||||
|
* Rules are evaluated **top-to-bottom**; the first matching rule wins.
|
||||||
|
* Rule 2 (5-day window) is a subset of Rule 3 (14-day window), so it
|
||||||
|
* must be checked first.
|
||||||
|
*
|
||||||
|
* ## Inputs
|
||||||
|
*
|
||||||
|
* | Field | Type | Description |
|
||||||
|
* |--------------------|------------------|--------------------------------------------------------------------|
|
||||||
|
* | `closedFlag` | `boolean` | Whether the record is closed / inactive. |
|
||||||
|
* | `expectedCloseDate`| `Date \| null` | The projected close date (future-looking relevance signal). |
|
||||||
|
* | `lastUpdated` | `Date \| null` | The last time the upstream record was modified (backward-looking). |
|
||||||
|
* | `now` | `Date` (optional)| Override for the current timestamp; defaults to `new Date()`. |
|
||||||
|
*
|
||||||
|
* ## Output
|
||||||
|
*
|
||||||
|
* Returns `number | null`:
|
||||||
|
* - A positive integer representing the TTL in **milliseconds**, or
|
||||||
|
* - `null` when the record should **not** be cached at all.
|
||||||
|
*
|
||||||
|
* ## Usage
|
||||||
|
*
|
||||||
|
* ```ts
|
||||||
|
* import { computeCacheTTL } from "../modules/algorithms/computeCacheTTL";
|
||||||
|
*
|
||||||
|
* const ttl = computeCacheTTL({
|
||||||
|
* closedFlag: opportunity.closedFlag,
|
||||||
|
* expectedCloseDate: opportunity.expectedCloseDate,
|
||||||
|
* lastUpdated: opportunity.cwLastUpdated,
|
||||||
|
* });
|
||||||
|
*
|
||||||
|
* if (ttl !== null) {
|
||||||
|
* await redis.set(key, serialised, "PX", ttl);
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Constants
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/** 30 seconds – TTL for high-activity records (within 5 days). */
|
||||||
|
export const TTL_HIGH_ACTIVITY = 30_000;
|
||||||
|
|
||||||
|
/** 60 seconds – TTL for moderate-activity records (within 14 days). */
|
||||||
|
export const TTL_MODERATE_ACTIVITY = 60_000;
|
||||||
|
|
||||||
|
/** 15 minutes – TTL for low-activity / stale records. */
|
||||||
|
export const TTL_LOW_ACTIVITY = 900_000;
|
||||||
|
|
||||||
|
/** 30 days in milliseconds. */
|
||||||
|
const THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1000;
|
||||||
|
|
||||||
|
/** 5 days in milliseconds. */
|
||||||
|
const FIVE_DAYS_MS = 5 * 24 * 60 * 60 * 1000;
|
||||||
|
|
||||||
|
/** 14 days in milliseconds. */
|
||||||
|
const FOURTEEN_DAYS_MS = 14 * 24 * 60 * 60 * 1000;
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Input type
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export interface CacheTTLInput {
|
||||||
|
/** Whether the record is closed / inactive. */
|
||||||
|
closedFlag: boolean;
|
||||||
|
/** When the record was closed — used for recently-closed caching (within 30 days). */
|
||||||
|
closedDate: Date | null;
|
||||||
|
/** The projected close date — serves as a forward-looking relevance signal. */
|
||||||
|
expectedCloseDate: Date | null;
|
||||||
|
/** The date the upstream record was last modified — backward-looking signal. */
|
||||||
|
lastUpdated: Date | null;
|
||||||
|
/**
|
||||||
|
* Override for the current timestamp.
|
||||||
|
* Useful for deterministic testing. Defaults to `new Date()`.
|
||||||
|
*/
|
||||||
|
now?: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Algorithm
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compute the cache TTL for a record based on its activity signals.
|
||||||
|
*
|
||||||
|
* @param input - The record's activity signals. See {@link CacheTTLInput}.
|
||||||
|
* @returns The TTL in milliseconds, or `null` if the record should not be cached.
|
||||||
|
*
|
||||||
|
* @see Module-level JSDoc for the full spec table and evaluation rules.
|
||||||
|
*/
|
||||||
|
export function computeCacheTTL(input: CacheTTLInput): number | null {
|
||||||
|
const {
|
||||||
|
closedFlag,
|
||||||
|
closedDate,
|
||||||
|
expectedCloseDate,
|
||||||
|
lastUpdated,
|
||||||
|
now = new Date(),
|
||||||
|
} = input;
|
||||||
|
|
||||||
|
const nowMs = now.getTime();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether a date falls within a window around `now`.
|
||||||
|
*
|
||||||
|
* "Within" means the date is between `now - windowMs` and `now + windowMs`,
|
||||||
|
* allowing both past updates and future-scheduled dates to qualify.
|
||||||
|
*/
|
||||||
|
const isWithinWindow = (date: Date | null, windowMs: number): boolean => {
|
||||||
|
if (!date) return false;
|
||||||
|
const diff = Math.abs(nowMs - date.getTime());
|
||||||
|
return diff <= windowMs;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Rule 1 — Closed records
|
||||||
|
if (closedFlag) {
|
||||||
|
// Rule 1b — Recently closed (within 30 days) → cache at low-activity TTL
|
||||||
|
if (isWithinWindow(closedDate, THIRTY_DAYS_MS)) {
|
||||||
|
return TTL_LOW_ACTIVITY;
|
||||||
|
}
|
||||||
|
// Rule 1a — Closed longer than 30 days → do not cache
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rule 2 — High activity (5 days)
|
||||||
|
if (
|
||||||
|
isWithinWindow(expectedCloseDate, FIVE_DAYS_MS) ||
|
||||||
|
isWithinWindow(lastUpdated, FIVE_DAYS_MS)
|
||||||
|
) {
|
||||||
|
return TTL_HIGH_ACTIVITY;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rule 3 — Moderate activity (14 days)
|
||||||
|
if (
|
||||||
|
isWithinWindow(expectedCloseDate, FOURTEEN_DAYS_MS) ||
|
||||||
|
isWithinWindow(lastUpdated, FOURTEEN_DAYS_MS)
|
||||||
|
) {
|
||||||
|
return TTL_MODERATE_ACTIVITY;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rule 4 — Low activity / stale
|
||||||
|
return TTL_LOW_ACTIVITY;
|
||||||
|
}
|
||||||
+257
@@ -0,0 +1,257 @@
|
|||||||
|
/**
|
||||||
|
* @module opportunityCache
|
||||||
|
*
|
||||||
|
* Redis-backed cache for expensive ConnectWise API data associated
|
||||||
|
* with opportunities.
|
||||||
|
*
|
||||||
|
* ## What is cached
|
||||||
|
*
|
||||||
|
* Each non-closed opportunity may have two cached payloads keyed by
|
||||||
|
* its `cwOpportunityId`:
|
||||||
|
*
|
||||||
|
* - **Activities** (`opp:activities:{cwOpportunityId}`) — the raw
|
||||||
|
* `CWActivity[]` array fetched from `activityCw.fetchByOpportunity()`.
|
||||||
|
* - **Company CW data** (`opp:company-cw:{cw_CompanyId}`) — the hydrated
|
||||||
|
* company / contacts blob set by `CompanyController.hydrateCwData()`.
|
||||||
|
*
|
||||||
|
* TTLs are computed dynamically via {@link computeCacheTTL} so hot
|
||||||
|
* opportunities refresh every 30 s while stale ones live for 15 min.
|
||||||
|
*
|
||||||
|
* ## Background refresh
|
||||||
|
*
|
||||||
|
* {@link refreshOpportunityCache} is designed to be called on a
|
||||||
|
* 30-second interval from `src/index.ts`. It scans all non-closed
|
||||||
|
* DB opportunities, checks which cache keys have expired, and
|
||||||
|
* re-fetches only those from ConnectWise.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { prisma, redis } from "../../constants";
|
||||||
|
import { activityCw } from "../cw-utils/activities/activities";
|
||||||
|
import { computeCacheTTL } from "../algorithms/computeCacheTTL";
|
||||||
|
import { connectWiseApi } from "../../constants";
|
||||||
|
import { fetchCwCompanyById } from "../cw-utils/fetchCompany";
|
||||||
|
import { events } from "../globalEvents";
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Key helpers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
const ACTIVITY_PREFIX = "opp:activities:";
|
||||||
|
const COMPANY_CW_PREFIX = "opp:company-cw:";
|
||||||
|
|
||||||
|
/** Redis key for cached activities by CW opportunity ID. */
|
||||||
|
export const activityCacheKey = (cwOppId: number) =>
|
||||||
|
`${ACTIVITY_PREFIX}${cwOppId}`;
|
||||||
|
|
||||||
|
/** Redis key for cached company CW hydration data by CW company ID. */
|
||||||
|
export const companyCwCacheKey = (cwCompanyId: number) =>
|
||||||
|
`${COMPANY_CW_PREFIX}${cwCompanyId}`;
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Read helpers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve cached CW activities for an opportunity.
|
||||||
|
*
|
||||||
|
* @returns The parsed `CWActivity[]` or `null` on cache miss.
|
||||||
|
*/
|
||||||
|
export async function getCachedActivities(
|
||||||
|
cwOpportunityId: number,
|
||||||
|
): Promise<any[] | null> {
|
||||||
|
const raw = await redis.get(activityCacheKey(cwOpportunityId));
|
||||||
|
if (!raw) return null;
|
||||||
|
try {
|
||||||
|
return JSON.parse(raw);
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve cached company CW hydration data.
|
||||||
|
*
|
||||||
|
* @returns `{ company, defaultContact, allContacts }` or `null` on cache miss.
|
||||||
|
*/
|
||||||
|
export async function getCachedCompanyCwData(
|
||||||
|
cwCompanyId: number,
|
||||||
|
): Promise<{ company: any; defaultContact: any; allContacts: any[] } | null> {
|
||||||
|
const raw = await redis.get(companyCwCacheKey(cwCompanyId));
|
||||||
|
if (!raw) return null;
|
||||||
|
try {
|
||||||
|
return JSON.parse(raw);
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Write helpers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch activities from CW and cache them with the appropriate TTL.
|
||||||
|
*
|
||||||
|
* @returns The raw `CWActivity[]` collection (as plain array).
|
||||||
|
*/
|
||||||
|
export async function fetchAndCacheActivities(
|
||||||
|
cwOpportunityId: number,
|
||||||
|
ttlMs: number,
|
||||||
|
): Promise<any[]> {
|
||||||
|
const collection = await activityCw.fetchByOpportunity(cwOpportunityId);
|
||||||
|
const arr = collection.map((item) => item);
|
||||||
|
await redis.set(
|
||||||
|
activityCacheKey(cwOpportunityId),
|
||||||
|
JSON.stringify(arr),
|
||||||
|
"PX",
|
||||||
|
ttlMs,
|
||||||
|
);
|
||||||
|
return arr;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch company CW data (company, contacts) and cache with the given TTL.
|
||||||
|
*
|
||||||
|
* @returns The hydration blob or `null` if the company doesn't exist in CW.
|
||||||
|
*/
|
||||||
|
export async function fetchAndCacheCompanyCwData(
|
||||||
|
cwCompanyId: number,
|
||||||
|
ttlMs: number,
|
||||||
|
): Promise<{ company: any; defaultContact: any; allContacts: any[] } | null> {
|
||||||
|
const cwCompany = await fetchCwCompanyById(cwCompanyId);
|
||||||
|
if (!cwCompany) return null;
|
||||||
|
|
||||||
|
const contactHref = cwCompany.defaultContact?._info?.contact_href;
|
||||||
|
const defaultContactData = contactHref
|
||||||
|
? await connectWiseApi.get(contactHref)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const allContactsData = await connectWiseApi.get(
|
||||||
|
`${cwCompany._info.contacts_href}&pageSize=1000`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const blob = {
|
||||||
|
company: cwCompany,
|
||||||
|
defaultContact: defaultContactData?.data ?? null,
|
||||||
|
allContacts: allContactsData.data,
|
||||||
|
};
|
||||||
|
|
||||||
|
await redis.set(
|
||||||
|
companyCwCacheKey(cwCompanyId),
|
||||||
|
JSON.stringify(blob),
|
||||||
|
"PX",
|
||||||
|
ttlMs,
|
||||||
|
);
|
||||||
|
|
||||||
|
return blob;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Background refresh
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refresh the opportunity cache.
|
||||||
|
*
|
||||||
|
* Scans all non-closed opportunities in the database, computes a TTL for each,
|
||||||
|
* checks whether the cache key still exists, and re-fetches from ConnectWise
|
||||||
|
* only for entries that have expired.
|
||||||
|
*
|
||||||
|
* Designed to be called every **30 seconds** from the process entry point.
|
||||||
|
*/
|
||||||
|
export async function refreshOpportunityCache(): Promise<void> {
|
||||||
|
// Include non-closed AND recently-closed (within 30 days) opportunities
|
||||||
|
const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);
|
||||||
|
|
||||||
|
const opportunities = await prisma.opportunity.findMany({
|
||||||
|
where: {
|
||||||
|
OR: [
|
||||||
|
{ closedFlag: false },
|
||||||
|
{ closedFlag: true, closedDate: { gte: thirtyDaysAgo } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
cwOpportunityId: true,
|
||||||
|
closedFlag: true,
|
||||||
|
closedDate: true,
|
||||||
|
expectedCloseDate: true,
|
||||||
|
cwLastUpdated: true,
|
||||||
|
company: { select: { cw_CompanyId: true } },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
events.emit("cache:opportunities:refresh:started", {
|
||||||
|
totalOpportunities: opportunities.length,
|
||||||
|
});
|
||||||
|
|
||||||
|
let activitiesRefreshed = 0;
|
||||||
|
let companiesRefreshed = 0;
|
||||||
|
let skipped = 0;
|
||||||
|
|
||||||
|
// Batch-check which activity keys already exist via a pipeline
|
||||||
|
const pipeline = redis.pipeline();
|
||||||
|
for (const opp of opportunities) {
|
||||||
|
pipeline.exists(activityCacheKey(opp.cwOpportunityId));
|
||||||
|
}
|
||||||
|
const existsResults = await pipeline.exec();
|
||||||
|
|
||||||
|
const refreshTasks: Promise<void>[] = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < opportunities.length; i++) {
|
||||||
|
const opp = opportunities[i]!;
|
||||||
|
|
||||||
|
const ttl = computeCacheTTL({
|
||||||
|
closedFlag: opp.closedFlag,
|
||||||
|
closedDate: opp.closedDate,
|
||||||
|
expectedCloseDate: opp.expectedCloseDate,
|
||||||
|
lastUpdated: opp.cwLastUpdated,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Skip closed (ttl === null) — should not happen because of the query filter,
|
||||||
|
// but guard anyway.
|
||||||
|
if (ttl === null) {
|
||||||
|
skipped++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// existsResults entries are [error, result] tuples
|
||||||
|
const activityExists = existsResults?.[i]?.[1] === 1;
|
||||||
|
|
||||||
|
if (!activityExists) {
|
||||||
|
refreshTasks.push(
|
||||||
|
fetchAndCacheActivities(opp.cwOpportunityId, ttl).then(() => {
|
||||||
|
activitiesRefreshed++;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also refresh company CW data if the key is missing
|
||||||
|
if (opp.company?.cw_CompanyId) {
|
||||||
|
const cwCompanyId = opp.company.cw_CompanyId;
|
||||||
|
refreshTasks.push(
|
||||||
|
(async () => {
|
||||||
|
const companyExists = await redis.exists(
|
||||||
|
companyCwCacheKey(cwCompanyId),
|
||||||
|
);
|
||||||
|
if (!companyExists) {
|
||||||
|
await fetchAndCacheCompanyCwData(cwCompanyId, ttl);
|
||||||
|
companiesRefreshed++;
|
||||||
|
}
|
||||||
|
})(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run all refresh tasks concurrently with bounded concurrency
|
||||||
|
const CONCURRENCY = 10;
|
||||||
|
for (let i = 0; i < refreshTasks.length; i += CONCURRENCY) {
|
||||||
|
await Promise.allSettled(refreshTasks.slice(i, i + CONCURRENCY));
|
||||||
|
}
|
||||||
|
|
||||||
|
events.emit("cache:opportunities:refresh:completed", {
|
||||||
|
totalOpportunities: opportunities.length,
|
||||||
|
activitiesRefreshed,
|
||||||
|
companiesRefreshed,
|
||||||
|
skipped,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,498 @@
|
|||||||
|
/**
|
||||||
|
* Catalog Categories & Ecosystems
|
||||||
|
*
|
||||||
|
* This module defines the complete category/subcategory hierarchy and
|
||||||
|
* ecosystem decision trees used for product filtering in the UI.
|
||||||
|
*
|
||||||
|
* --- Terminology ---
|
||||||
|
*
|
||||||
|
* Category: Top-level CW category (e.g. "Technology", "Field", "General").
|
||||||
|
* A category is NEVER a subcategory.
|
||||||
|
*
|
||||||
|
* Subcategory: The CW subcategory name stored on each catalog item.
|
||||||
|
* At the second level of the tree, if there are no children
|
||||||
|
* beneath it then the node name IS the subcategory.
|
||||||
|
* If children exist, the second-level node is an *umbrella*
|
||||||
|
* that groups related subcategories — the children are the
|
||||||
|
* actual subcategory names.
|
||||||
|
*
|
||||||
|
* Ecosystem: A cross-cutting product grouping defined by manufacturer +
|
||||||
|
* category + subcategory-prefix rules. Ecosystems let the UI
|
||||||
|
* present a "Networking" or "Video Surveillance" view that
|
||||||
|
* spans manufacturers regardless of where CW filed them.
|
||||||
|
*
|
||||||
|
* --- Data shapes ---
|
||||||
|
*
|
||||||
|
* SubcategoryNode – a leaf: `{ name, cwId? }`
|
||||||
|
* CategoryGroup – an umbrella with children: `{ name, children[] }`
|
||||||
|
* CategoryEntry – either a leaf OR a group at the 2nd level
|
||||||
|
* TopLevelCategory – `{ name, cwId?, entries[] }`
|
||||||
|
*
|
||||||
|
* The `CATEGORY_TREE` export is the single source of truth; helpers derive
|
||||||
|
* flat lists, lookup maps, and search predicates from it.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// ─── Data types ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export interface SubcategoryNode {
|
||||||
|
/** The exact CW subcategory name */
|
||||||
|
name: string;
|
||||||
|
/** CW subcategory id (optional, for reference) */
|
||||||
|
cwId?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CategoryGroup {
|
||||||
|
/** Display name of the umbrella (e.g. "Network", "Cables", "AlarmBurg") */
|
||||||
|
name: string;
|
||||||
|
/** The subcategories that belong to this umbrella */
|
||||||
|
children: SubcategoryNode[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A second-level entry is either a direct subcategory or an umbrella group */
|
||||||
|
export type CategoryEntry = SubcategoryNode | CategoryGroup;
|
||||||
|
|
||||||
|
export interface TopLevelCategory {
|
||||||
|
/** The CW category name */
|
||||||
|
name: string;
|
||||||
|
/** CW category id (optional, for reference) */
|
||||||
|
cwId?: number;
|
||||||
|
/** Second-level entries under this category */
|
||||||
|
entries: CategoryEntry[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Helper type guard */
|
||||||
|
export function isCategoryGroup(entry: CategoryEntry): entry is CategoryGroup {
|
||||||
|
return "children" in entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Ecosystem types ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export interface EcosystemManufacturer {
|
||||||
|
/** Manufacturer name as stored in CW */
|
||||||
|
name: string;
|
||||||
|
/** CW manufacturer id */
|
||||||
|
cwId?: number;
|
||||||
|
/** Which CW category these products fall under */
|
||||||
|
category: string;
|
||||||
|
/** Subcategory prefix — matches any subcategory starting with this string */
|
||||||
|
subcategoryPrefix: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Ecosystem {
|
||||||
|
/** Display name (e.g. "Networking", "Video Surveillance") */
|
||||||
|
name: string;
|
||||||
|
/** Manufacturers that belong to this ecosystem */
|
||||||
|
manufacturers: EcosystemManufacturer[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Category Tree ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export const CATEGORY_TREE: TopLevelCategory[] = [
|
||||||
|
{
|
||||||
|
name: "Technology",
|
||||||
|
cwId: 18,
|
||||||
|
entries: [
|
||||||
|
{ name: "GeneralEquip", cwId: 57 },
|
||||||
|
{ name: "Home Entertainment", cwId: 114 },
|
||||||
|
{ name: "Monitor", cwId: 115 },
|
||||||
|
{ name: "Printers", cwId: 120 },
|
||||||
|
{ name: "Storage", cwId: 108 },
|
||||||
|
{
|
||||||
|
name: "Network",
|
||||||
|
children: [
|
||||||
|
{ name: "Network-Other", cwId: 174 },
|
||||||
|
{ name: "Network-Router", cwId: 119 },
|
||||||
|
{ name: "Network-Switch", cwId: 112 },
|
||||||
|
{ name: "Network-Wireless", cwId: 111 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Computer",
|
||||||
|
children: [
|
||||||
|
{ name: "Computer-Components", cwId: 109 },
|
||||||
|
{ name: "Computer-Desktop", cwId: 106 },
|
||||||
|
{ name: "Computer-Laptop", cwId: 107 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Recurring",
|
||||||
|
children: [
|
||||||
|
{ name: "Recurring - Online", cwId: 83 },
|
||||||
|
{ name: "Recurring - Other", cwId: 84 },
|
||||||
|
{ name: "Recurring - Protection", cwId: 81 },
|
||||||
|
{ name: "Recurring - Telephone", cwId: 133 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Telephone",
|
||||||
|
children: [
|
||||||
|
{ name: "Tele-HSet-Digital", cwId: 116 },
|
||||||
|
{ name: "Tele-HSet-IP", cwId: 206 },
|
||||||
|
{ name: "Tele-HSet-SLT" },
|
||||||
|
{ name: "Tele-Misc", cwId: 75 },
|
||||||
|
{ name: "Tele-Paging", cwId: 76 },
|
||||||
|
{ name: "Tele-SystemCards", cwId: 135 },
|
||||||
|
{ name: "Tele-Systems", cwId: 78 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "General",
|
||||||
|
cwId: 25,
|
||||||
|
entries: [
|
||||||
|
{ name: "Batteries", cwId: 80 },
|
||||||
|
{ name: "Battery Backups", cwId: 144 },
|
||||||
|
{ name: "BulkWire", cwId: 200 },
|
||||||
|
{
|
||||||
|
name: "Cables",
|
||||||
|
children: [
|
||||||
|
{ name: "Cables-Adapters", cwId: 182 },
|
||||||
|
{ name: "Cables-HDMI", cwId: 176 },
|
||||||
|
{ name: "Cables-Network", cwId: 87 },
|
||||||
|
{ name: "Cables-Other", cwId: 177 },
|
||||||
|
{ name: "Cables-USB", cwId: 178 },
|
||||||
|
{ name: "Cables-VGA", cwId: 179 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{ name: "Elec Cords & Adapters", cwId: 142 },
|
||||||
|
{ name: "Enclosures", cwId: 141 },
|
||||||
|
{ name: "PowerSupply", cwId: 167 },
|
||||||
|
{
|
||||||
|
name: "RackEquip",
|
||||||
|
children: [
|
||||||
|
{ name: "RackEquip-Rack", cwId: 143 },
|
||||||
|
{ name: "RackEquip-Shelves", cwId: 190 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Field",
|
||||||
|
cwId: 28,
|
||||||
|
entries: [
|
||||||
|
{ name: "Conduit" },
|
||||||
|
{ name: "Electric", cwId: 199 },
|
||||||
|
{ name: "GateControl", cwId: 45 },
|
||||||
|
{ name: "Locksets" },
|
||||||
|
{ name: "Other", cwId: 46 },
|
||||||
|
{ name: "Relays", cwId: 168 },
|
||||||
|
{
|
||||||
|
name: "AccessControl",
|
||||||
|
children: [
|
||||||
|
{ name: "AccessControl-Controllers", cwId: 137 },
|
||||||
|
{ name: "AccessControl-Credential", cwId: 183 },
|
||||||
|
{ name: "AccessControl-LockDevices", cwId: 138 },
|
||||||
|
{ name: "AccessControl-Other", cwId: 44 },
|
||||||
|
{ name: "AccessControl-Readers", cwId: 136 },
|
||||||
|
{ name: "AccessControl-VideoEntry", cwId: 139 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "AlarmBurg",
|
||||||
|
children: [
|
||||||
|
{ name: "AlarmBurg-Communicators", cwId: 96 },
|
||||||
|
{ name: "AlarmBurg-Keypads", cwId: 93 },
|
||||||
|
{ name: "AlarmBurg-Modules", cwId: 140 },
|
||||||
|
{ name: "AlarmBurg-Other", cwId: 92 },
|
||||||
|
{ name: "AlarmBurg-Panels", cwId: 42 },
|
||||||
|
{ name: "AlarmBurg-Sensors-Wireless", cwId: 147 },
|
||||||
|
{ name: "AlarmBurg-Sensors-Wired", cwId: 146 },
|
||||||
|
{ name: "AlarmBurg-Siren", cwId: 145 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "AlarmFire",
|
||||||
|
children: [
|
||||||
|
{ name: "AlarmFire-Communicators", cwId: 97 },
|
||||||
|
{ name: "AlarmFire-Devices", cwId: 169 },
|
||||||
|
{ name: "AlarmFire-Modules", cwId: 170 },
|
||||||
|
{ name: "AlarmFire-Other", cwId: 98 },
|
||||||
|
{ name: "AlarmFire-Panels", cwId: 95 },
|
||||||
|
{ name: "AlarmFire-Sensors", cwId: 94 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Automation",
|
||||||
|
children: [
|
||||||
|
{ name: "Automation-General", cwId: 99 },
|
||||||
|
{ name: "Automation-HVAC", cwId: 181 },
|
||||||
|
{ name: "Automation-Lights", cwId: 180 },
|
||||||
|
{ name: "Automation-Locks", cwId: 192 },
|
||||||
|
{ name: "Automation-Thermostat" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "AV",
|
||||||
|
children: [
|
||||||
|
{ name: "AV-Adapters&Cables", cwId: 171 },
|
||||||
|
{ name: "AV-Components", cwId: 172 },
|
||||||
|
{ name: "AV-Mounts", cwId: 191 },
|
||||||
|
{ name: "AV-Other", cwId: 184 },
|
||||||
|
{ name: "AV-Speakers", cwId: 173 },
|
||||||
|
{ name: "AV-Television", cwId: 175 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "StrCbl",
|
||||||
|
children: [
|
||||||
|
{ name: "StrCbl-Jacks", cwId: 186 },
|
||||||
|
{ name: "StrCbl-PatchPanel", cwId: 187 },
|
||||||
|
{ name: "StrCbl-Plates", cwId: 185 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Surveillance",
|
||||||
|
children: [
|
||||||
|
{ name: "Surveillance-Accs", cwId: 90 },
|
||||||
|
{ name: "Surveillance-CamerasAnalog", cwId: 89 },
|
||||||
|
{ name: "Surveillance-CamerasIP", cwId: 88 },
|
||||||
|
{ name: "Surveillance-NVR", cwId: 43 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// ─── Ecosystem Tree ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export const ECOSYSTEM_TREE: Ecosystem[] = [
|
||||||
|
{
|
||||||
|
name: "Networking",
|
||||||
|
manufacturers: [
|
||||||
|
{
|
||||||
|
name: "Ubiquiti",
|
||||||
|
cwId: 248,
|
||||||
|
category: "Technology",
|
||||||
|
subcategoryPrefix: "Network-",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "TP-Link",
|
||||||
|
cwId: 259,
|
||||||
|
category: "Technology",
|
||||||
|
subcategoryPrefix: "Network-",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Video Surveillance",
|
||||||
|
manufacturers: [
|
||||||
|
{
|
||||||
|
name: "Uniview",
|
||||||
|
cwId: 239,
|
||||||
|
category: "Field",
|
||||||
|
subcategoryPrefix: "Surveillance-",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Hikvision",
|
||||||
|
cwId: 299,
|
||||||
|
category: "Field",
|
||||||
|
subcategoryPrefix: "Surveillance-",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Alarm.com",
|
||||||
|
cwId: 294,
|
||||||
|
category: "Field",
|
||||||
|
subcategoryPrefix: "Surveillance-",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Burg/Alarm",
|
||||||
|
manufacturers: [
|
||||||
|
{
|
||||||
|
name: "Qolsys",
|
||||||
|
cwId: 376,
|
||||||
|
category: "Field",
|
||||||
|
subcategoryPrefix: "AlarmBurg-",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "DSC",
|
||||||
|
cwId: 287,
|
||||||
|
category: "Field",
|
||||||
|
subcategoryPrefix: "AlarmBurg-",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// ─── Derived helpers ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a flat list of all subcategory names under a given category.
|
||||||
|
*/
|
||||||
|
export function getSubcategoriesForCategory(categoryName: string): string[] {
|
||||||
|
const category = CATEGORY_TREE.find((c) => c.name === categoryName);
|
||||||
|
if (!category) return [];
|
||||||
|
|
||||||
|
const subcats: string[] = [];
|
||||||
|
for (const entry of category.entries) {
|
||||||
|
if (isCategoryGroup(entry)) {
|
||||||
|
for (const child of entry.children) {
|
||||||
|
subcats.push(child.name);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
subcats.push(entry.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return subcats;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns all subcategory names under a given umbrella group within a category.
|
||||||
|
* e.g. getSubcategoriesForGroup("Field", "AlarmBurg") → ["AlarmBurg-Communicators", ...]
|
||||||
|
*/
|
||||||
|
export function getSubcategoriesForGroup(
|
||||||
|
categoryName: string,
|
||||||
|
groupName: string,
|
||||||
|
): string[] {
|
||||||
|
const category = CATEGORY_TREE.find((c) => c.name === categoryName);
|
||||||
|
if (!category) return [];
|
||||||
|
|
||||||
|
const group = category.entries.find(
|
||||||
|
(e) => isCategoryGroup(e) && e.name === groupName,
|
||||||
|
);
|
||||||
|
if (!group || !isCategoryGroup(group)) return [];
|
||||||
|
|
||||||
|
return group.children.map((c) => c.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns all top-level category names.
|
||||||
|
*/
|
||||||
|
export function getCategoryNames(): string[] {
|
||||||
|
return CATEGORY_TREE.map((c) => c.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the umbrella group name for a given subcategory, or null if it's a
|
||||||
|
* direct entry (not under an umbrella).
|
||||||
|
*/
|
||||||
|
export function getGroupForSubcategory(
|
||||||
|
subcategoryName: string,
|
||||||
|
): { category: string; group: string } | null {
|
||||||
|
for (const cat of CATEGORY_TREE) {
|
||||||
|
for (const entry of cat.entries) {
|
||||||
|
if (isCategoryGroup(entry)) {
|
||||||
|
if (entry.children.some((c) => c.name === subcategoryName)) {
|
||||||
|
return { category: cat.name, group: entry.name };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the full tree serialized for the API / UI consumption.
|
||||||
|
* Each top-level category includes its entries, with umbrella groups
|
||||||
|
* expanded to show children.
|
||||||
|
*/
|
||||||
|
export function serializeCategoryTree() {
|
||||||
|
return CATEGORY_TREE.map((cat) => ({
|
||||||
|
name: cat.name,
|
||||||
|
cwId: cat.cwId ?? null,
|
||||||
|
entries: cat.entries.map((entry) => {
|
||||||
|
if (isCategoryGroup(entry)) {
|
||||||
|
return {
|
||||||
|
type: "group" as const,
|
||||||
|
name: entry.name,
|
||||||
|
subcategories: entry.children.map((c) => ({
|
||||||
|
name: c.name,
|
||||||
|
cwId: c.cwId ?? null,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
type: "subcategory" as const,
|
||||||
|
name: entry.name,
|
||||||
|
cwId: (entry as SubcategoryNode).cwId ?? null,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the ecosystem tree serialized for the API / UI consumption.
|
||||||
|
*/
|
||||||
|
export function serializeEcosystemTree() {
|
||||||
|
return ECOSYSTEM_TREE.map((eco) => ({
|
||||||
|
name: eco.name,
|
||||||
|
manufacturers: eco.manufacturers.map((m) => ({
|
||||||
|
name: m.name,
|
||||||
|
cwId: m.cwId ?? null,
|
||||||
|
category: m.category,
|
||||||
|
subcategoryPrefix: m.subcategoryPrefix,
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a flat list of every known subcategory name across all categories.
|
||||||
|
*/
|
||||||
|
export function getAllSubcategoryNames(): string[] {
|
||||||
|
const names: string[] = [];
|
||||||
|
for (const cat of CATEGORY_TREE) {
|
||||||
|
for (const entry of cat.entries) {
|
||||||
|
if (isCategoryGroup(entry)) {
|
||||||
|
for (const child of entry.children) {
|
||||||
|
names.push(child.name);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
names.push(entry.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return names;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given a CW subcategory name, resolves which top-level category it belongs to.
|
||||||
|
*/
|
||||||
|
export function getCategoryForSubcategory(
|
||||||
|
subcategoryName: string,
|
||||||
|
): string | null {
|
||||||
|
for (const cat of CATEGORY_TREE) {
|
||||||
|
for (const entry of cat.entries) {
|
||||||
|
if (isCategoryGroup(entry)) {
|
||||||
|
if (entry.children.some((c) => c.name === subcategoryName)) {
|
||||||
|
return cat.name;
|
||||||
|
}
|
||||||
|
} else if (entry.name === subcategoryName) {
|
||||||
|
return cat.name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given a CW manufacturer name, returns which ecosystems it belongs to.
|
||||||
|
*/
|
||||||
|
export function getEcosystemsForManufacturer(
|
||||||
|
manufacturerName: string,
|
||||||
|
): string[] {
|
||||||
|
return ECOSYSTEM_TREE.filter((eco) =>
|
||||||
|
eco.manufacturers.some(
|
||||||
|
(m) => m.name.toLowerCase() === manufacturerName.toLowerCase(),
|
||||||
|
),
|
||||||
|
).map((eco) => eco.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a catalog item (by manufacturer + subcategory) matches a given ecosystem.
|
||||||
|
*/
|
||||||
|
export function matchesEcosystem(
|
||||||
|
ecosystemName: string,
|
||||||
|
manufacturer: string | null,
|
||||||
|
subcategory: string | null,
|
||||||
|
): boolean {
|
||||||
|
const eco = ECOSYSTEM_TREE.find((e) => e.name === ecosystemName);
|
||||||
|
if (!eco) return false;
|
||||||
|
|
||||||
|
return eco.manufacturers.some(
|
||||||
|
(m) =>
|
||||||
|
m.name.toLowerCase() === (manufacturer ?? "").toLowerCase() &&
|
||||||
|
(subcategory ?? "").startsWith(m.subcategoryPrefix),
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,168 @@
|
|||||||
|
import { Collection } from "@discordjs/collection";
|
||||||
|
import { connectWiseApi } from "../../../constants";
|
||||||
|
import {
|
||||||
|
CWActivity,
|
||||||
|
CWActivitySummary,
|
||||||
|
CWCreateActivity,
|
||||||
|
CWPatchOperation,
|
||||||
|
} from "./activity.types";
|
||||||
|
|
||||||
|
export const activityCw = {
|
||||||
|
/**
|
||||||
|
* Count Activities
|
||||||
|
*
|
||||||
|
* Returns the total number of activities in ConnectWise.
|
||||||
|
* Optionally accepts CW conditions string for filtered counts.
|
||||||
|
*/
|
||||||
|
countItems: async (conditions?: string): Promise<number> => {
|
||||||
|
const query = conditions
|
||||||
|
? `/sales/activities/count?conditions=${encodeURIComponent(conditions)}`
|
||||||
|
: "/sales/activities/count";
|
||||||
|
const response = await connectWiseApi.get(query);
|
||||||
|
return response.data.count;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch All Activity Summaries
|
||||||
|
*
|
||||||
|
* Lightweight fetch returning only id and _info (for lastUpdated comparison).
|
||||||
|
* Paginates through all activities.
|
||||||
|
*/
|
||||||
|
fetchAllSummaries: async (): Promise<
|
||||||
|
Collection<number, CWActivitySummary>
|
||||||
|
> => {
|
||||||
|
const allItems = new Collection<number, CWActivitySummary>();
|
||||||
|
const pageSize = 1000;
|
||||||
|
|
||||||
|
const count = await activityCw.countItems();
|
||||||
|
const totalPages = Math.ceil(count / pageSize);
|
||||||
|
|
||||||
|
for (let page = 0; page < totalPages; page++) {
|
||||||
|
const response = await connectWiseApi.get(
|
||||||
|
`/sales/activities?page=${page + 1}&pageSize=${pageSize}&fields=id,_info`,
|
||||||
|
);
|
||||||
|
const items: CWActivitySummary[] = response.data;
|
||||||
|
|
||||||
|
for (const item of items) {
|
||||||
|
allItems.set(item.id, item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return allItems;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch All Activities (Full)
|
||||||
|
*
|
||||||
|
* Fetches all activities with complete data. Paginates through
|
||||||
|
* the full list. Optionally accepts CW conditions string for filtering.
|
||||||
|
*/
|
||||||
|
fetchAll: async (
|
||||||
|
conditions?: string,
|
||||||
|
): Promise<Collection<number, CWActivity>> => {
|
||||||
|
const allItems = new Collection<number, CWActivity>();
|
||||||
|
const pageSize = 1000;
|
||||||
|
|
||||||
|
const count = await activityCw.countItems(conditions);
|
||||||
|
const totalPages = Math.ceil(count / pageSize);
|
||||||
|
|
||||||
|
for (let page = 0; page < totalPages; page++) {
|
||||||
|
const conditionsParam = conditions
|
||||||
|
? `&conditions=${encodeURIComponent(conditions)}`
|
||||||
|
: "";
|
||||||
|
const response = await connectWiseApi.get(
|
||||||
|
`/sales/activities?page=${page + 1}&pageSize=${pageSize}${conditionsParam}`,
|
||||||
|
);
|
||||||
|
const items: CWActivity[] = response.data;
|
||||||
|
|
||||||
|
for (const item of items) {
|
||||||
|
allItems.set(item.id, item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return allItems;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch Single Activity
|
||||||
|
*
|
||||||
|
* Fetches a single activity by its ConnectWise ID.
|
||||||
|
*/
|
||||||
|
fetch: async (id: number): Promise<CWActivity> => {
|
||||||
|
const response = await connectWiseApi.get(`/sales/activities/${id}`);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch Activities by Company
|
||||||
|
*
|
||||||
|
* Fetches all activities associated with a specific ConnectWise company ID.
|
||||||
|
*/
|
||||||
|
fetchByCompany: async (
|
||||||
|
cwCompanyId: number,
|
||||||
|
): Promise<Collection<number, CWActivity>> => {
|
||||||
|
return activityCw.fetchAll(`company/id=${cwCompanyId}`);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch Activities by Opportunity
|
||||||
|
*
|
||||||
|
* Fetches all activities associated with a specific opportunity ID.
|
||||||
|
*/
|
||||||
|
fetchByOpportunity: async (
|
||||||
|
opportunityId: number,
|
||||||
|
): Promise<Collection<number, CWActivity>> => {
|
||||||
|
return activityCw.fetchAll(`opportunity/id=${opportunityId}`);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create Activity
|
||||||
|
*
|
||||||
|
* Creates a new activity in ConnectWise.
|
||||||
|
*/
|
||||||
|
create: async (activity: CWCreateActivity): Promise<CWActivity> => {
|
||||||
|
const response = await connectWiseApi.post("/sales/activities", activity);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update Activity (PATCH)
|
||||||
|
*
|
||||||
|
* Updates an existing activity using JSON Patch operations.
|
||||||
|
*/
|
||||||
|
update: async (
|
||||||
|
id: number,
|
||||||
|
operations: CWPatchOperation[],
|
||||||
|
): Promise<CWActivity> => {
|
||||||
|
const response = await connectWiseApi.patch(
|
||||||
|
`/sales/activities/${id}`,
|
||||||
|
operations,
|
||||||
|
);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replace Activity (PUT)
|
||||||
|
*
|
||||||
|
* Replaces an entire activity record in ConnectWise.
|
||||||
|
*/
|
||||||
|
replace: async (
|
||||||
|
id: number,
|
||||||
|
activity: CWCreateActivity,
|
||||||
|
): Promise<CWActivity> => {
|
||||||
|
const response = await connectWiseApi.put(
|
||||||
|
`/sales/activities/${id}`,
|
||||||
|
activity,
|
||||||
|
);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete Activity
|
||||||
|
*
|
||||||
|
* Deletes an activity by its ConnectWise ID.
|
||||||
|
*/
|
||||||
|
delete: async (id: number): Promise<void> => {
|
||||||
|
await connectWiseApi.delete(`/sales/activities/${id}`);
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
interface CWReference {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
_info?: Record<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CWMemberReference {
|
||||||
|
id: number;
|
||||||
|
identifier: string;
|
||||||
|
name: string;
|
||||||
|
_info?: Record<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CWCompanyReference {
|
||||||
|
id: number;
|
||||||
|
identifier: string;
|
||||||
|
name: string;
|
||||||
|
_info?: Record<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CWContactReference {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
_info?: Record<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CWActivity {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
type: CWReference;
|
||||||
|
company: CWCompanyReference;
|
||||||
|
contact: CWContactReference;
|
||||||
|
phoneNumber: string;
|
||||||
|
email: string;
|
||||||
|
status: CWReference;
|
||||||
|
opportunity: CWReference;
|
||||||
|
ticket: CWReference;
|
||||||
|
agreement: CWReference;
|
||||||
|
campaign: CWReference;
|
||||||
|
notes: string;
|
||||||
|
dateStart: string;
|
||||||
|
dateEnd: string;
|
||||||
|
assignTo: CWMemberReference;
|
||||||
|
scheduleStatus: CWReference;
|
||||||
|
reminder: CWReference;
|
||||||
|
where: CWReference;
|
||||||
|
notifyFlag: boolean;
|
||||||
|
mobileGuid: string;
|
||||||
|
currency: CWReference;
|
||||||
|
customFields: CWActivityCustomField[];
|
||||||
|
_info: CWActivityInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CWActivityCustomField {
|
||||||
|
id: number;
|
||||||
|
caption: string;
|
||||||
|
type: string;
|
||||||
|
entryMethod: string;
|
||||||
|
numberOfDecimals: number;
|
||||||
|
value: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CWActivityInfo {
|
||||||
|
lastUpdated: string;
|
||||||
|
updatedBy: string;
|
||||||
|
dateEntered: string;
|
||||||
|
enteredBy: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CWActivitySummary {
|
||||||
|
id: number;
|
||||||
|
_info?: Record<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CWCreateActivity {
|
||||||
|
name: string;
|
||||||
|
type?: { id: number };
|
||||||
|
company?: { id: number };
|
||||||
|
contact?: { id: number };
|
||||||
|
phoneNumber?: string;
|
||||||
|
email?: string;
|
||||||
|
status?: { id: number };
|
||||||
|
opportunity?: { id: number };
|
||||||
|
ticket?: { id: number };
|
||||||
|
agreement?: { id: number };
|
||||||
|
campaign?: { id: number };
|
||||||
|
notes?: string;
|
||||||
|
dateStart?: string;
|
||||||
|
dateEnd?: string;
|
||||||
|
assignTo?: { id: number };
|
||||||
|
scheduleStatus?: { id: number };
|
||||||
|
reminder?: { id: number };
|
||||||
|
where?: { id: number };
|
||||||
|
notifyFlag?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CWUpdateActivity {
|
||||||
|
name?: string;
|
||||||
|
type?: { id: number };
|
||||||
|
company?: { id: number };
|
||||||
|
contact?: { id: number };
|
||||||
|
phoneNumber?: string;
|
||||||
|
email?: string;
|
||||||
|
status?: { id: number };
|
||||||
|
opportunity?: { id: number };
|
||||||
|
ticket?: { id: number };
|
||||||
|
agreement?: { id: number };
|
||||||
|
campaign?: { id: number };
|
||||||
|
notes?: string;
|
||||||
|
dateStart?: string;
|
||||||
|
dateEnd?: string;
|
||||||
|
assignTo?: { id: number };
|
||||||
|
scheduleStatus?: { id: number };
|
||||||
|
reminder?: { id: number };
|
||||||
|
where?: { id: number };
|
||||||
|
notifyFlag?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CWPatchOperation {
|
||||||
|
op: "replace" | "add" | "remove";
|
||||||
|
path: string;
|
||||||
|
value: unknown;
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import GenericError from "../../../Errors/GenericError";
|
||||||
|
import { activityCw } from "./activities";
|
||||||
|
import { CWActivity, CWCreateActivity } from "./activity.types";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new activity in ConnectWise.
|
||||||
|
*
|
||||||
|
* @param activity - The activity data to create
|
||||||
|
* @returns The newly created CW activity object
|
||||||
|
* @throws GenericError if the creation fails
|
||||||
|
*/
|
||||||
|
export const createActivity = async (
|
||||||
|
activity: CWCreateActivity,
|
||||||
|
): Promise<CWActivity> => {
|
||||||
|
try {
|
||||||
|
return await activityCw.create(activity);
|
||||||
|
} catch (error) {
|
||||||
|
const errBody = (error as any).response?.data || error;
|
||||||
|
console.error("Error creating activity:", errBody);
|
||||||
|
throw new GenericError({
|
||||||
|
name: "CreateActivityError",
|
||||||
|
message: "Failed to create activity in ConnectWise",
|
||||||
|
cause: typeof errBody === "string" ? errBody : JSON.stringify(errBody),
|
||||||
|
status: 502,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import GenericError from "../../../Errors/GenericError";
|
||||||
|
import { activityCw } from "./activities";
|
||||||
|
import { CWActivity } from "./activity.types";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch a single activity by its ConnectWise ID.
|
||||||
|
*
|
||||||
|
* @param cwActivityId - The ConnectWise activity ID
|
||||||
|
* @returns The full CW activity object
|
||||||
|
* @throws GenericError if the fetch fails
|
||||||
|
*/
|
||||||
|
export const fetchActivity = async (
|
||||||
|
cwActivityId: number,
|
||||||
|
): Promise<CWActivity> => {
|
||||||
|
try {
|
||||||
|
return await activityCw.fetch(cwActivityId);
|
||||||
|
} catch (error) {
|
||||||
|
const errBody = (error as any).response?.data || error;
|
||||||
|
console.error(`Error fetching activity with ID ${cwActivityId}:`, errBody);
|
||||||
|
throw new GenericError({
|
||||||
|
name: "FetchActivityError",
|
||||||
|
message: `Failed to fetch activity ${cwActivityId}`,
|
||||||
|
cause: typeof errBody === "string" ? errBody : JSON.stringify(errBody),
|
||||||
|
status: 502,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { Collection } from "@discordjs/collection";
|
||||||
|
import GenericError from "../../../Errors/GenericError";
|
||||||
|
import { activityCw } from "./activities";
|
||||||
|
import { CWActivity } from "./activity.types";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch all activities from ConnectWise with optional conditions.
|
||||||
|
*
|
||||||
|
* @param conditions - Optional CW conditions string for filtering
|
||||||
|
* @returns A Collection of CW activities keyed by their ID
|
||||||
|
* @throws GenericError if the fetch fails
|
||||||
|
*/
|
||||||
|
export const fetchAllActivities = async (
|
||||||
|
conditions?: string,
|
||||||
|
): Promise<Collection<number, CWActivity>> => {
|
||||||
|
try {
|
||||||
|
return await activityCw.fetchAll(conditions);
|
||||||
|
} catch (error) {
|
||||||
|
const errBody = (error as any).response?.data || error;
|
||||||
|
console.error("Error fetching all activities:", errBody);
|
||||||
|
throw new GenericError({
|
||||||
|
name: "FetchAllActivitiesError",
|
||||||
|
message: "Failed to fetch activities from ConnectWise",
|
||||||
|
cause: typeof errBody === "string" ? errBody : JSON.stringify(errBody),
|
||||||
|
status: 502,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
export { activityCw } from "./activities";
|
||||||
|
export { fetchActivity } from "./fetchActivity";
|
||||||
|
export { fetchAllActivities } from "./fetchAllActivities";
|
||||||
|
export { createActivity } from "./createActivity";
|
||||||
|
export { updateActivity } from "./updateActivity";
|
||||||
|
|
||||||
|
export type {
|
||||||
|
CWActivity,
|
||||||
|
CWActivitySummary,
|
||||||
|
CWActivityCustomField,
|
||||||
|
CWActivityInfo,
|
||||||
|
CWCreateActivity,
|
||||||
|
CWUpdateActivity,
|
||||||
|
CWPatchOperation,
|
||||||
|
} from "./activity.types";
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import GenericError from "../../../Errors/GenericError";
|
||||||
|
import { activityCw } from "./activities";
|
||||||
|
import { CWActivity, CWPatchOperation } from "./activity.types";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update an existing activity in ConnectWise using JSON Patch operations.
|
||||||
|
*
|
||||||
|
* @param cwActivityId - The ConnectWise activity ID to update
|
||||||
|
* @param operations - Array of JSON Patch operations to apply
|
||||||
|
* @returns The updated CW activity object
|
||||||
|
* @throws GenericError if the update fails
|
||||||
|
*/
|
||||||
|
export const updateActivity = async (
|
||||||
|
cwActivityId: number,
|
||||||
|
operations: CWPatchOperation[],
|
||||||
|
): Promise<CWActivity> => {
|
||||||
|
try {
|
||||||
|
return await activityCw.update(cwActivityId, operations);
|
||||||
|
} catch (error) {
|
||||||
|
const errBody = (error as any).response?.data || error;
|
||||||
|
console.error(`Error updating activity with ID ${cwActivityId}:`, errBody);
|
||||||
|
throw new GenericError({
|
||||||
|
name: "UpdateActivityError",
|
||||||
|
message: `Failed to update activity ${cwActivityId}`,
|
||||||
|
cause: typeof errBody === "string" ? errBody : JSON.stringify(errBody),
|
||||||
|
status: 502,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import { Collection } from "@discordjs/collection";
|
||||||
|
import { connectWiseApi } from "../../../constants";
|
||||||
|
|
||||||
|
export interface CWMember {
|
||||||
|
id: number;
|
||||||
|
identifier: string;
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
officeEmail: string;
|
||||||
|
inactiveFlag: boolean;
|
||||||
|
_info: Record<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch All CW Members
|
||||||
|
*
|
||||||
|
* Fetches every member from ConnectWise using pagination and returns them
|
||||||
|
* in a Collection keyed by their identifier (e.g. "jroberts").
|
||||||
|
*
|
||||||
|
* @returns {Promise<Collection<string, CWMember>>} Collection of CW members keyed by identifier
|
||||||
|
*/
|
||||||
|
export const fetchAllCwMembers = async (): Promise<
|
||||||
|
Collection<string, CWMember>
|
||||||
|
> => {
|
||||||
|
const members = new Collection<string, CWMember>();
|
||||||
|
const pageSize = 1000;
|
||||||
|
|
||||||
|
const { data: countData } = await connectWiseApi.get("/system/members/count");
|
||||||
|
const totalPages = Math.ceil(countData.count / pageSize);
|
||||||
|
|
||||||
|
for (let page = 0; page < totalPages; page++) {
|
||||||
|
const { data } = await connectWiseApi.get<CWMember[]>(
|
||||||
|
`/system/members?page=${page + 1}&pageSize=${pageSize}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const member of data) {
|
||||||
|
members.set(member.identifier, member);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return members;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find CW Member Identifier by Email
|
||||||
|
*
|
||||||
|
* Looks up a ConnectWise member whose `officeEmail` matches the provided
|
||||||
|
* email address (case-insensitive) and returns their `identifier` string
|
||||||
|
* (e.g. "jroberts"). Returns `null` if no match is found.
|
||||||
|
*
|
||||||
|
* @param email - The email address to search for
|
||||||
|
* @param members - Optional pre-fetched member collection to search against (avoids extra API call)
|
||||||
|
* @returns {Promise<string | null>} The CW identifier or null
|
||||||
|
*/
|
||||||
|
export const findCwIdentifierByEmail = async (
|
||||||
|
email: string,
|
||||||
|
members?: Collection<string, CWMember>,
|
||||||
|
): Promise<string | null> => {
|
||||||
|
const allMembers = members ?? (await fetchAllCwMembers());
|
||||||
|
const normalised = email.toLowerCase();
|
||||||
|
|
||||||
|
const match = allMembers.find(
|
||||||
|
(m) => m.officeEmail?.toLowerCase() === normalised,
|
||||||
|
);
|
||||||
|
|
||||||
|
return match?.identifier ?? null;
|
||||||
|
};
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
import { Collection } from "@discordjs/collection";
|
||||||
|
import { prisma } from "../../../constants";
|
||||||
|
import { redis } from "../../../constants";
|
||||||
|
import { CWMember } from "./fetchAllMembers";
|
||||||
|
|
||||||
|
const REDIS_KEY = "cw:members";
|
||||||
|
|
||||||
|
export interface ResolvedMember {
|
||||||
|
/** Local database user ID (null if no matching local user) */
|
||||||
|
id: string | null;
|
||||||
|
/** CW member identifier (e.g. "jroberts") */
|
||||||
|
identifier: string;
|
||||||
|
/** Full name resolved from CW member cache, or raw identifier as fallback */
|
||||||
|
name: string;
|
||||||
|
/** ConnectWise member ID */
|
||||||
|
cwMemberId: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CW Member Cache
|
||||||
|
*
|
||||||
|
* Dual-layer cache (in-memory + Redis) of ConnectWise members keyed by
|
||||||
|
* their identifier (e.g. "jroberts"). Populated by `refreshCwIdentifiers`
|
||||||
|
* on startup and every 30 minutes thereafter.
|
||||||
|
*/
|
||||||
|
let memberCache = new Collection<string, CWMember>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the member cache contents.
|
||||||
|
*
|
||||||
|
* Replaces both the in-memory Collection and the Redis snapshot.
|
||||||
|
*
|
||||||
|
* @param members - Collection of CW members keyed by identifier
|
||||||
|
*/
|
||||||
|
export const setMemberCache = async (members: Collection<string, CWMember>) => {
|
||||||
|
memberCache = members;
|
||||||
|
await redis.set(REDIS_KEY, JSON.stringify([...members.values()]));
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the current member cache.
|
||||||
|
*
|
||||||
|
* Returns the in-memory Collection. If empty, attempts to hydrate from Redis
|
||||||
|
* first. Returns whatever is available (may be empty if Redis is also cold).
|
||||||
|
*/
|
||||||
|
export const getMemberCache = async (): Promise<
|
||||||
|
Collection<string, CWMember>
|
||||||
|
> => {
|
||||||
|
if (memberCache.size > 0) return memberCache;
|
||||||
|
|
||||||
|
const stored = await redis.get(REDIS_KEY);
|
||||||
|
if (stored) {
|
||||||
|
const parsed: CWMember[] = JSON.parse(stored);
|
||||||
|
memberCache = new Collection(parsed.map((m) => [m.identifier, m]));
|
||||||
|
}
|
||||||
|
|
||||||
|
return memberCache;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve CW Identifier to Full Name
|
||||||
|
*
|
||||||
|
* Looks up a ConnectWise member by their identifier in the in-memory cache
|
||||||
|
* and returns their full name. Falls back to the raw identifier if not found.
|
||||||
|
*
|
||||||
|
* @param identifier - The CW member identifier (e.g. "jroberts")
|
||||||
|
* @returns The member's full name (e.g. "John Roberts") or the raw identifier
|
||||||
|
*/
|
||||||
|
export const resolveMemberName = (identifier: string): string => {
|
||||||
|
const member = memberCache.get(identifier);
|
||||||
|
if (!member) return identifier;
|
||||||
|
return `${member.firstName} ${member.lastName}`.trim() || identifier;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve CW Identifier to Full Member Info
|
||||||
|
*
|
||||||
|
* Looks up a ConnectWise member by their identifier in the in-memory cache
|
||||||
|
* and cross-references with the local database to return a complete member
|
||||||
|
* reference including local user ID, CW identifier, full name, and CW member ID.
|
||||||
|
*
|
||||||
|
* @param identifier - The CW member identifier (e.g. "jroberts")
|
||||||
|
* @returns {Promise<ResolvedMember>} Resolved member info
|
||||||
|
*/
|
||||||
|
export const resolveMember = async (
|
||||||
|
identifier: string,
|
||||||
|
): Promise<ResolvedMember> => {
|
||||||
|
const cwMember = memberCache.get(identifier);
|
||||||
|
const name = cwMember
|
||||||
|
? `${cwMember.firstName} ${cwMember.lastName}`.trim() || identifier
|
||||||
|
: identifier;
|
||||||
|
|
||||||
|
const localUser = await prisma.user.findFirst({
|
||||||
|
where: { cwIdentifier: identifier },
|
||||||
|
select: { id: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: localUser?.id ?? null,
|
||||||
|
identifier,
|
||||||
|
name,
|
||||||
|
cwMemberId: cwMember?.id ?? null,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import { connectWiseApi, prisma } from "../../../constants";
|
||||||
|
import { events } from "../../globalEvents";
|
||||||
|
import { fetchAllCwMembers, findCwIdentifierByEmail } from "./fetchAllMembers";
|
||||||
|
import { setMemberCache } from "./memberCache";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refresh CW Identifiers
|
||||||
|
*
|
||||||
|
* Fetches all CW members and all users from the database, then updates
|
||||||
|
* each user's `cwIdentifier` field by matching their email to a CW member's
|
||||||
|
* `officeEmail`. Only users whose identifier has changed (or was previously
|
||||||
|
* null) are updated to avoid unnecessary writes.
|
||||||
|
*
|
||||||
|
* Also refreshes the in-memory member cache used for name resolution.
|
||||||
|
*/
|
||||||
|
export const refreshCwIdentifiers = async () => {
|
||||||
|
events.emit("cw:members:refresh:started");
|
||||||
|
|
||||||
|
const allMembers = await fetchAllCwMembers();
|
||||||
|
await setMemberCache(allMembers);
|
||||||
|
const allUsers = await prisma.user.findMany({
|
||||||
|
select: { id: true, email: true, cwIdentifier: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
let updatedCount = 0;
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
allUsers.map(async (user) => {
|
||||||
|
const identifier = await findCwIdentifierByEmail(user.email, allMembers);
|
||||||
|
|
||||||
|
if (identifier !== user.cwIdentifier) {
|
||||||
|
await prisma.user.update({
|
||||||
|
where: { id: user.id },
|
||||||
|
data: { cwIdentifier: identifier },
|
||||||
|
});
|
||||||
|
updatedCount++;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
events.emit("cw:members:refresh:completed", {
|
||||||
|
totalMembers: allMembers.size,
|
||||||
|
totalUsers: allUsers.length,
|
||||||
|
usersUpdated: updatedCount,
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { Collection } from "@discordjs/collection";
|
||||||
|
import GenericError from "../../../Errors/GenericError";
|
||||||
|
import { opportunityCw } from "./opportunities";
|
||||||
|
import { CWOpportunity } from "./opportunity.types";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch all opportunities from ConnectWise with optional conditions.
|
||||||
|
*
|
||||||
|
* @param conditions - Optional CW conditions string for filtering
|
||||||
|
* @returns A Collection of CW opportunities keyed by their ID
|
||||||
|
* @throws GenericError if the fetch fails
|
||||||
|
*/
|
||||||
|
export const fetchAllOpportunities = async (
|
||||||
|
conditions?: string,
|
||||||
|
): Promise<Collection<number, CWOpportunity>> => {
|
||||||
|
try {
|
||||||
|
return await opportunityCw.fetchAll(conditions);
|
||||||
|
} catch (error) {
|
||||||
|
const errBody = (error as any).response?.data || error;
|
||||||
|
console.error("Error fetching all opportunities:", errBody);
|
||||||
|
throw new GenericError({
|
||||||
|
name: "FetchAllOpportunitiesError",
|
||||||
|
message: "Failed to fetch opportunities from ConnectWise",
|
||||||
|
cause: typeof errBody === "string" ? errBody : JSON.stringify(errBody),
|
||||||
|
status: 502,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { Collection } from "@discordjs/collection";
|
||||||
|
import GenericError from "../../../Errors/GenericError";
|
||||||
|
import { opportunityCw } from "./opportunities";
|
||||||
|
import { CWOpportunity } from "./opportunity.types";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch all opportunities for a specific company from ConnectWise.
|
||||||
|
*
|
||||||
|
* @param cwCompanyId - The ConnectWise company ID
|
||||||
|
* @returns A Collection of CW opportunities for the company keyed by their ID
|
||||||
|
* @throws GenericError if the fetch fails
|
||||||
|
*/
|
||||||
|
export const fetchCompanyOpportunities = async (
|
||||||
|
cwCompanyId: number,
|
||||||
|
): Promise<Collection<number, CWOpportunity>> => {
|
||||||
|
try {
|
||||||
|
return await opportunityCw.fetchByCompany(cwCompanyId);
|
||||||
|
} catch (error) {
|
||||||
|
const errBody = (error as any).response?.data || error;
|
||||||
|
console.error(
|
||||||
|
`Error fetching opportunities for company ${cwCompanyId}:`,
|
||||||
|
errBody,
|
||||||
|
);
|
||||||
|
throw new GenericError({
|
||||||
|
name: "FetchCompanyOpportunitiesError",
|
||||||
|
message: `Failed to fetch opportunities for company ${cwCompanyId}`,
|
||||||
|
cause: typeof errBody === "string" ? errBody : JSON.stringify(errBody),
|
||||||
|
status: 502,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user