diff --git a/API_ROUTES.md b/API_ROUTES.md index 64512c8..197c6f2 100644 --- a/API_ROUTES.md +++ b/API_ROUTES.md @@ -137,7 +137,46 @@ See [PERMISSIONS.md](PERMISSIONS.md) for the full list of field-level permission ## Authentication Routes -## ConnectWise Callback Routes +## ConnectWise Routes + +### Fetch CW Members + +**GET** `/cw/members` + +Returns all ConnectWise members from the server-side member cache, sorted alphabetically by name. By default only active members are returned. + +**Authentication Required:** Yes (any authenticated user) + +**Permissions Required:** None + +**Query Parameters:** + +| Parameter | Type | Default | Description | +| --------- | ------ | ------- | ------------------------------------------ | +| `active` | string | `true` | Set to `false` to include inactive members | + +**Response:** + +```json +{ + "status": 200, + "message": "CW members fetched successfully!", + "data": [ + { + "id": 250, + "identifier": "jroberts", + "firstName": "John", + "lastName": "Roberts", + "name": "John Roberts", + "officeEmail": "jroberts@totaltech.net", + "inactive": false + } + ], + "successful": true +} +``` + +--- ### Receive ConnectWise Callback @@ -3344,6 +3383,242 @@ Refresh an opportunity's local data by fetching the latest from ConnectWise. The --- +### Create Opportunity + +**POST** `/sales/opportunities` + +Create a new opportunity in ConnectWise. The created opportunity is synced to the local database and returned in the response. `name` and `expectedCloseDate` are required; all other fields are optional. + +**Authentication Required:** Yes + +**Required Permissions:** `sales.opportunity.create` + +**Request Body:** + +```json +{ + "name": "Acme Corp Network Refresh", + "expectedCloseDate": "2026-06-01", + "notes": "Initial scoping phase", + "rating": { "id": 1 }, + "type": { "id": 2 }, + "stage": { "id": 1 }, + "status": { "id": 1 }, + "priority": { "id": 2 }, + "campaign": { "id": 5 }, + "primarySalesRep": { "id": 10 }, + "secondarySalesRep": { "id": 12 }, + "company": { "id": 100 }, + "contact": { "id": 200 }, + "site": { "id": 50 }, + "source": "Referral", + "customerPO": "PO-12345", + "locationId": 1, + "businessUnitId": 5 +} +``` + +| Field | Type | Required | Description | +| ------------------- | ------------------------ | -------- | --------------------------------------------------------- | +| `name` | `string` | Yes | Opportunity name | +| `expectedCloseDate` | `string` | Yes | Expected close date (date string, e.g. `2026-06-01`) | +| `primarySalesRep` | `{ id: number }` | Yes | CW member reference for primary sales rep | +| `company` | `{ id: number }` | Yes | CW company reference | +| `contact` | `{ id: number }` | Yes | CW contact reference | +| `notes` | `string` | No | Opportunity description / notes | +| `rating` | `{ id: number }` | No | CW rating reference | +| `type` | `{ id: number }` | No | CW opportunity type reference | +| `stage` | `{ id: number }` | No | CW pipeline stage reference | +| `status` | `{ id: number }` | No | CW status reference | +| `priority` | `{ id: number }` | No | CW priority reference | +| `campaign` | `{ id: number }` | No | CW campaign reference | +| `secondarySalesRep` | `{ id: number } \| null` | No | CW member reference for secondary sales rep (null clears) | +| `site` | `{ id: number } \| null` | No | CW site reference (null clears) | +| `source` | `string \| null` | No | Opportunity source (null clears) | +| `customerPO` | `string \| null` | No | Customer PO number (null clears) | +| `locationId` | `number` | No | CW location ID | +| `businessUnitId` | `number` | No | CW business unit ID | + +**Response (201):** + +```json +{ + "status": 201, + "message": "Opportunity created successfully!", + "data": { + "id": "clx...", + "cwOpportunityId": 789, + "name": "Acme Corp Network Refresh", + "notes": "Initial scoping phase", + "type": { "id": 2, "name": "Existing" }, + "stage": { "id": 1, "name": "Prospect" }, + "status": { "id": 1, "name": "Open" }, + "priority": { "id": 2, "name": "High" }, + "rating": { "id": 1, "name": "Hot" }, + "source": "Referral", + "campaign": { "id": 5, "name": "Q2 Push" }, + "primarySalesRep": { + "id": 10, + "identifier": "JDoe", + "name": "John Doe" + }, + "secondarySalesRep": { + "id": 12, + "identifier": "ASmith", + "name": "Alice Smith" + }, + "company": { "id": 100, "name": "Acme Corp" }, + "contact": { "id": 200, "name": "Jane Smith" }, + "site": { "id": 50, "name": "Main Office" }, + "customerPO": "PO-12345", + "totalSalesTax": 0, + "probability": 0, + "location": { "id": 1, "name": "Murray" }, + "department": null, + "expectedCloseDate": "2026-06-01T00:00:00.000Z", + "pipelineChangeDate": null, + "dateBecameLead": null, + "closedDate": null, + "closedFlag": false, + "closedBy": null, + "companyId": "clx...", + "cwLastUpdated": "2026-03-07T10:00:00.000Z", + "createdAt": "2026-03-07T10:00:00.000Z", + "updatedAt": "2026-03-07T10:00:00.000Z", + "customFields": [], + "activities": [] + }, + "successful": true +} +``` + +**Error Responses:** + +| Status | Scenario | +| ------- | ------------------------------------------------------- | +| 400 | Zod validation failure (missing name/expectedCloseDate) | +| 401 | Missing or invalid auth token | +| 403 | User lacks `sales.opportunity.create` permission | +| 4xx/5xx | ConnectWise API error (forwarded status + message) | +| 500 | Unexpected server error | + +--- + +### Update Opportunity + +**PATCH** `/sales/opportunities/:identifier` + +Update an opportunity's fields in ConnectWise (e.g. rating, sales rep, company, contact, site, description). Only the provided fields are patched; omitted fields remain unchanged. The updated opportunity is synced back to the local database and returned in the response. + +**Authentication Required:** Yes + +**Required Permissions:** `sales.opportunity.update` + +**Path Parameters:** + +- `identifier` — Internal ID (cuid) or ConnectWise opportunity ID (numeric) + +**Request Body (all fields optional, at least one required):** + +```json +{ + "name": "Acme Corp Network Refresh — Phase 2", + "notes": "Updated project scope to include wireless", + "rating": { "id": 1 }, + "type": { "id": 2 }, + "stage": { "id": 4 }, + "status": { "id": 1 }, + "priority": { "id": 2 }, + "campaign": { "id": 5 }, + "primarySalesRep": { "id": 10 }, + "secondarySalesRep": { "id": 12 }, + "company": { "id": 100 }, + "contact": { "id": 200 }, + "site": { "id": 50 }, + "expectedCloseDate": "2026-05-01", + "customerPO": "PO-12345", + "source": "Referral", + "locationId": 1, + "businessUnitId": 5 +} +``` + +| Field | Type | Description | +| ------------------- | ------------------------ | --------------------------------------------------------- | +| `name` | `string` | Opportunity name | +| `notes` | `string` | Opportunity description / notes | +| `rating` | `{ id: number }` | CW rating reference | +| `type` | `{ id: number }` | CW opportunity type reference | +| `stage` | `{ id: number }` | CW pipeline stage reference | +| `status` | `{ id: number }` | CW status reference | +| `priority` | `{ id: number }` | CW priority reference | +| `campaign` | `{ id: number }` | CW campaign reference | +| `primarySalesRep` | `{ id: number }` | CW member reference for primary sales rep | +| `secondarySalesRep` | `{ id: number } \| null` | CW member reference for secondary sales rep (null clears) | +| `company` | `{ id: number }` | CW company reference | +| `contact` | `{ id: number } \| null` | CW contact reference (null clears) | +| `site` | `{ id: number } \| null` | CW site reference (null clears) | +| `expectedCloseDate` | `string` | Expected close date (ISO date string) | +| `customerPO` | `string \| null` | Customer PO number (null clears) | +| `source` | `string \| null` | Opportunity source (null clears) | +| `locationId` | `number` | CW location ID | +| `businessUnitId` | `number` | CW business unit ID | + +**Response:** + +```json +{ + "status": 200, + "message": "Opportunity updated successfully!", + "data": { + "id": "clx...", + "cwOpportunityId": 456, + "name": "Acme Corp Network Refresh — Phase 2", + "description": "Updated project scope to include wireless", + "type": { "id": 2, "name": "Existing" }, + "stage": { "id": 4, "name": "Negotiation" }, + "status": { "id": 1, "name": "Open" }, + "priority": { "id": 2, "name": "High" }, + "rating": { "id": 1, "name": "Hot" }, + "source": "Referral", + "campaign": { "id": 5, "name": "Q2 Push" }, + "primarySalesRep": { + "id": 10, + "identifier": "JDoe", + "name": "John Doe" + }, + "secondarySalesRep": { + "id": 12, + "identifier": "ASmith", + "name": "Alice Smith" + }, + "company": { "id": 100, "name": "Acme Corp" }, + "contact": { "id": 200, "name": "Jane Smith" }, + "site": { "id": 50, "name": "Main Office" }, + "customerPO": "PO-12345", + "totalSalesTax": 0, + "probability": 50, + "location": { "id": 1, "name": "Murray" }, + "department": { "id": 5, "name": "Sales" }, + "expectedCloseDate": "2026-05-01T00:00:00.000Z", + "pipelineChangeDate": "2026-02-25T00:00:00.000Z", + "dateBecameLead": "2026-01-10T00:00:00.000Z", + "closedDate": null, + "closedFlag": false, + "closedBy": null, + "companyId": "clx...", + "cwLastUpdated": "2026-03-07T10:00:00.000Z", + "createdAt": "2026-02-01T00:00:00.000Z", + "updatedAt": "2026-03-07T10:00:00.000Z", + "customFields": [], + "activities": [] + }, + "successful": true +} +``` + +--- + ### Get Opportunity Products **GET** `/sales/opportunities/:identifier/products` diff --git a/PERMISSIONS.md b/PERMISSIONS.md index 1a67fde..041da18 100644 --- a/PERMISSIONS.md +++ b/PERMISSIONS.md @@ -124,13 +124,16 @@ Admin-specific UI permissions that control visibility and data loading for admin | `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` | -### ConnectWise Callback Routes +### ConnectWise Routes + +`GET /v1/cw/members` requires only authentication (any logged-in user) and does **not** require a specific permission node. `POST /v1/cw/callback/:secret/:resource` is intentionally unauthenticated for inbound ConnectWise callbacks and does **not** require a permission node. -| Permission Node | Description | Used In | Dependencies | -| --------------- | ------------------------------------------------------------------------------- | ------------------------------------------------ | ------------ | -| _None_ | Inbound callback route; secured operationally (network controls / source trust) | [src/api/cw/callback.ts](src/api/cw/callback.ts) | N/A | +| Permission Node | Description | Used In | Dependencies | +| --------------- | ------------------------------------------------------------------------------- | -------------------------------------------------------- | ------------ | +| _None_ | Fetch CW members (auth only) | [src/api/cw/fetchMembers.ts](src/api/cw/fetchMembers.ts) | N/A | +| _None_ | Inbound callback route; secured operationally (network controls / source trust) | [src/api/cw/callback.ts](src/api/cw/callback.ts) | N/A | ### Sales Permissions @@ -143,6 +146,8 @@ Permissions for accessing and managing sales opportunities. Opportunities are sy | `sales.opportunity.fetch` | Fetch a single opportunity and its CW sub-resources (products, notes, contacts) | [src/api/sales/opportunities/[id]/fetch.ts](src/api/sales/opportunities/[id]/fetch.ts), [src/api/sales/opportunities/[id]/products/fetchAll.ts](src/api/sales/opportunities/[id]/products/fetchAll.ts), [src/api/sales/opportunities/[id]/notes/fetchAll.ts](src/api/sales/opportunities/[id]/notes/fetchAll.ts), [src/api/sales/opportunities/[id]/notes/fetch.ts](src/api/sales/opportunities/[id]/notes/fetch.ts), [src/api/sales/opportunities/[id]/contacts/fetchAll.ts](src/api/sales/opportunities/[id]/contacts/fetchAll.ts), [src/api/sockets/events/liveQuotePreview.ts](src/api/sockets/events/liveQuotePreview.ts) | | | `sales.opportunity.fetch.many` | Fetch multiple opportunities (paginated/searchable), count, or opportunity types | [src/api/sales/opportunities/fetchAll.ts](src/api/sales/opportunities/fetchAll.ts), [src/api/sales/opportunities/count.ts](src/api/sales/opportunities/count.ts), [src/api/sales/opportunities/fetchTypes.ts](src/api/sales/opportunities/fetchTypes.ts) | | | `sales.opportunity.refresh` | Refresh a single opportunity's local data from ConnectWise | [src/api/sales/opportunities/[id]/refresh.ts](src/api/sales/opportunities/[id]/refresh.ts) | `sales.opportunity.fetch` | +| `sales.opportunity.update` | Update an opportunity's fields (rating, sales rep, company, contact, site, description, etc.) in ConnectWise | [src/api/sales/opportunities/[id]/update.ts](src/api/sales/opportunities/[id]/update.ts) | `sales.opportunity.fetch` | +| `sales.opportunity.create` | Create a new opportunity in ConnectWise | [src/api/sales/opportunities/create.ts](src/api/sales/opportunities/create.ts) | | | `sales.opportunity.note.create` | Create a new note on an opportunity | [src/api/sales/opportunities/[id]/notes/create.ts](src/api/sales/opportunities/[id]/notes/create.ts) | `sales.opportunity.fetch` | | `sales.opportunity.note.update` | Update an existing note on an opportunity | [src/api/sales/opportunities/[id]/notes/update.ts](src/api/sales/opportunities/[id]/notes/update.ts) | `sales.opportunity.fetch` | | `sales.opportunity.note.delete` | Delete a note from an opportunity | [src/api/sales/opportunities/[id]/notes/delete.ts](src/api/sales/opportunities/[id]/notes/delete.ts) | `sales.opportunity.fetch` | @@ -155,6 +160,8 @@ Permissions for accessing and managing sales opportunities. Opportunities are sy | `sales.opportunity.quote.preview` | Generate a preview-stamped quote PDF for an opportunity without storing it. | [src/api/sales/opportunities/[id]/quotes/preview.ts](src/api/sales/opportunities/[id]/quotes/preview.ts) | `sales.opportunity.fetch` | | `sales.opportunity.quote.download` | Download a committed quote PDF. Each download is recorded with timestamp and user info. | [src/api/sales/opportunities/[id]/quotes/download.ts](src/api/sales/opportunities/[id]/quotes/download.ts) | `sales.opportunity.fetch` | | `sales.opportunity.quote.fetch_downloads` | Fetch download/print history for all quotes on an opportunity. Admin-level permission. | [src/api/sales/opportunities/[id]/quotes/fetchDownloads.ts](src/api/sales/opportunities/[id]/quotes/fetchDownloads.ts) | `sales.opportunity.fetch` | +| `sales.opportunity.view_margin` | View margin and markup data on opportunity products. Controls visibility of margin %, markup %, and related progress bars in the UI. | UI-only (client-side gate) | `sales.opportunity.fetch` | +| `sales.opportunity.view_cost` | View cost data on opportunity products. Controls visibility of unit cost, total cost, and recurring cost in the UI. | UI-only (client-side gate) | `sales.opportunity.fetch` |
Field-level permissions for sales.opportunity.product.add diff --git a/generated/prisma/browser.ts b/generated/prisma/browser.ts index 55953ea..dfcba99 100644 --- a/generated/prisma/browser.ts +++ b/generated/prisma/browser.ts @@ -72,3 +72,8 @@ export type Credential = Prisma.CredentialModel * */ export type GeneratedQuotes = Prisma.GeneratedQuotesModel +/** + * Model CwMember + * + */ +export type CwMember = Prisma.CwMemberModel diff --git a/generated/prisma/client.ts b/generated/prisma/client.ts index e4e816e..d95dc7e 100644 --- a/generated/prisma/client.ts +++ b/generated/prisma/client.ts @@ -94,3 +94,8 @@ export type Credential = Prisma.CredentialModel * */ export type GeneratedQuotes = Prisma.GeneratedQuotesModel +/** + * Model CwMember + * + */ +export type CwMember = Prisma.CwMemberModel diff --git a/generated/prisma/internal/class.ts b/generated/prisma/internal/class.ts index 955c952..cdb7b12 100644 --- a/generated/prisma/internal/class.ts +++ b/generated/prisma/internal/class.ts @@ -20,7 +20,7 @@ const config: runtime.GetPrismaClientConfig = { "clientVersion": "7.3.0", "engineVersion": "9d6ad21cbbceab97458517b147a6a09ff43aa735", "activeProvider": "postgresql", - "inlineSchema": "// This is your Prisma schema file,\n// learn more about it in the docs: https://pris.ly/d/prisma-schema\n\n// Looking for ways to speed up your queries, or scale easily with your serverless or edge functions?\n// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init\n\ngenerator client {\n provider = \"prisma-client\"\n output = \"../generated/prisma\"\n}\n\ndatasource db {\n provider = \"postgresql\"\n}\n\nmodel Session {\n id String @id @default(uuid())\n sessionKey String @unique @default(cuid())\n userId String\n expires DateTime\n refreshTokenGenerated Boolean @default(false)\n refreshedAt DateTime?\n invalidatedAt DateTime?\n user User @relation(fields: [userId], references: [id], onDelete: Cascade)\n}\n\nmodel User {\n id String @id @default(cuid())\n roles Role[]\n permissions String?\n login String @unique\n name String?\n email String @unique\n emailVerified DateTime?\n image String?\n\n cwIdentifier String?\n\n userId String @unique\n token String?\n\n sessions Session[]\n\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n generatedQuotes GeneratedQuotes[]\n}\n\nmodel Role {\n id String @id @default(uuid())\n title String\n moniker String @unique // e.g. admin, super_admin, moderator\n\n permissions String\n users User[]\n\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n}\n\nmodel UnifiSite {\n id String @id @default(cuid())\n name String\n\n siteId String @unique\n\n companyId String?\n company Company? @relation(fields: [companyId], references: [id])\n\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n}\n\nmodel Company {\n id String @id @default(cuid())\n name String\n\n cw_CompanyId Int @unique\n cw_Identifier String @unique\n\n credentials Credential[]\n unifiSites UnifiSite[]\n opportunities Opportunity[]\n\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n}\n\nmodel CatalogItem {\n id String @id @default(cuid())\n cwCatalogId Int @unique\n identifier String? @unique\n name String\n description String?\n customerDescription String?\n internalNotes String?\n\n linkedItems CatalogItem[] @relation(\"LinkedItems\")\n linkedTo CatalogItem[] @relation(\"LinkedItems\")\n\n category String?\n categoryCwId Int?\n subcategory String?\n subcategoryCwId Int?\n\n manufacturer String?\n manufactureCwId Int?\n\n partNumber String?\n\n vendorName String?\n vendorSku String?\n vendorCwId Int?\n\n price Float\n cost Float\n\n inactive Boolean @default(false)\n salesTaxable Boolean @default(true)\n\n onHand Int @default(0)\n cwLastUpdated DateTime?\n\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n}\n\nmodel Opportunity {\n id String @id @default(cuid())\n cwOpportunityId Int @unique\n name String\n notes String?\n\n generatedQuotes GeneratedQuotes[]\n\n // Stage / status / priority / type / rating stored as JSON references\n // so we don't need separate lookup tables for CW enums\n typeName String?\n typeCwId Int?\n stageName String?\n stageCwId Int?\n statusName String?\n statusCwId Int?\n priorityName String?\n priorityCwId Int?\n ratingName String?\n ratingCwId Int?\n source String?\n campaignName String?\n campaignCwId Int?\n\n // Sales rep references\n primarySalesRepName String?\n primarySalesRepIdentifier String?\n primarySalesRepCwId Int?\n secondarySalesRepName String?\n secondarySalesRepIdentifier String?\n secondarySalesRepCwId Int?\n\n // Company / contact / site\n companyCwId Int?\n companyName String?\n contactCwId Int?\n contactName String?\n siteCwId Int?\n siteName String?\n customerPO String?\n\n // Financials\n totalSalesTax Float @default(0)\n probability Float @default(0)\n\n // Location / department\n locationName String?\n locationCwId Int?\n departmentName String?\n departmentCwId Int?\n\n // Dates\n expectedCloseDate DateTime?\n pipelineChangeDate DateTime?\n dateBecameLead DateTime?\n closedDate DateTime?\n closedFlag Boolean @default(false)\n closedByName String?\n closedByCwId Int?\n\n // Internal relation to Company (optional, linked by cwCompanyId)\n companyId String?\n company Company? @relation(fields: [companyId], references: [id])\n\n // Local product sequence — array of CW forecast item IDs in display order.\n // When present, fetchProducts() uses this order instead of CW sequenceNumber.\n productSequence Int[] @default([])\n\n cwLastUpdated DateTime?\n\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n}\n\nmodel CredentialType {\n id String @id @default(cuid())\n name String @unique\n\n permissionScope String\n icon String?\n fields Json\n\n credentials Credential[]\n\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n}\n\nmodel SecureValue {\n id String @id @default(cuid())\n name String\n\n content String // Encrypted content\n hash String // Hash of the original content for integrity verification and Search\n\n credentialId String\n credential Credential @relation(fields: [credentialId], references: [id], onDelete: Cascade)\n\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n}\n\nmodel Credential {\n id String @id @default(cuid())\n name String\n notes String?\n subCredentialOfId String?\n subCredentialOf Credential? @relation(\"SubCredentials\", fields: [subCredentialOfId], references: [id], onDelete: Cascade)\n subCredentials Credential[] @relation(\"SubCredentials\")\n\n typeId String\n type CredentialType @relation(fields: [typeId], references: [id], onDelete: Cascade)\n\n fields Json\n\n companyId String\n company Company @relation(fields: [companyId], references: [id], onDelete: Cascade)\n\n securevalues SecureValue[]\n\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n}\n\nmodel GeneratedQuotes {\n id String @id @default(uuid())\n\n quoteRegenData Json @default(\"{}\") // Store any additional data needed for quote regeneration, such as product details, pricing, etc.\n quoteRegenParams Json @default(\"{}\") // Store parameters used for quote regeneration, such as template ID, formatting options, etc.\n quoteRegenHash String @unique @default(\"\")\n\n downloads Json @default(\"[]\") // Array of download records with timestamp and user info\n\n quoteFile Bytes\n quoteFileName String\n\n opportunityId String\n opportunity Opportunity @relation(fields: [opportunityId], references: [id], onDelete: Cascade)\n\n createdById String?\n createdBy User? @relation(fields: [createdById], references: [id], onDelete: SetNull)\n\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n}\n", + "inlineSchema": "// This is your Prisma schema file,\n// learn more about it in the docs: https://pris.ly/d/prisma-schema\n\n// Looking for ways to speed up your queries, or scale easily with your serverless or edge functions?\n// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init\n\ngenerator client {\n provider = \"prisma-client\"\n output = \"../generated/prisma\"\n}\n\ndatasource db {\n provider = \"postgresql\"\n}\n\nmodel Session {\n id String @id @default(uuid())\n sessionKey String @unique @default(cuid())\n userId String\n expires DateTime\n refreshTokenGenerated Boolean @default(false)\n refreshedAt DateTime?\n invalidatedAt DateTime?\n user User @relation(fields: [userId], references: [id], onDelete: Cascade)\n}\n\nmodel User {\n id String @id @default(cuid())\n roles Role[]\n permissions String?\n login String @unique\n name String?\n email String @unique\n emailVerified DateTime?\n image String?\n\n cwIdentifier String?\n\n userId String @unique\n token String?\n\n sessions Session[]\n\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n generatedQuotes GeneratedQuotes[]\n}\n\nmodel Role {\n id String @id @default(uuid())\n title String\n moniker String @unique // e.g. admin, super_admin, moderator\n\n permissions String\n users User[]\n\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n}\n\nmodel UnifiSite {\n id String @id @default(cuid())\n name String\n\n siteId String @unique\n\n companyId String?\n company Company? @relation(fields: [companyId], references: [id])\n\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n}\n\nmodel Company {\n id String @id @default(cuid())\n name String\n\n cw_CompanyId Int @unique\n cw_Identifier String @unique\n\n credentials Credential[]\n unifiSites UnifiSite[]\n opportunities Opportunity[]\n\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n}\n\nmodel CatalogItem {\n id String @id @default(cuid())\n cwCatalogId Int @unique\n identifier String? @unique\n name String\n description String?\n customerDescription String?\n internalNotes String?\n\n linkedItems CatalogItem[] @relation(\"LinkedItems\")\n linkedTo CatalogItem[] @relation(\"LinkedItems\")\n\n category String?\n categoryCwId Int?\n subcategory String?\n subcategoryCwId Int?\n\n manufacturer String?\n manufactureCwId Int?\n\n partNumber String?\n\n vendorName String?\n vendorSku String?\n vendorCwId Int?\n\n price Float\n cost Float\n\n inactive Boolean @default(false)\n salesTaxable Boolean @default(true)\n\n onHand Int @default(0)\n cwLastUpdated DateTime?\n\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n}\n\nmodel Opportunity {\n id String @id @default(cuid())\n cwOpportunityId Int @unique\n name String\n notes String?\n\n generatedQuotes GeneratedQuotes[]\n\n // Stage / status / priority / type / rating stored as JSON references\n // so we don't need separate lookup tables for CW enums\n typeName String?\n typeCwId Int?\n stageName String?\n stageCwId Int?\n statusName String?\n statusCwId Int?\n priorityName String?\n priorityCwId Int?\n ratingName String?\n ratingCwId Int?\n source String?\n campaignName String?\n campaignCwId Int?\n\n // Sales rep references\n primarySalesRepName String?\n primarySalesRepIdentifier String?\n primarySalesRepCwId Int?\n secondarySalesRepName String?\n secondarySalesRepIdentifier String?\n secondarySalesRepCwId Int?\n\n // Company / contact / site\n companyCwId Int?\n companyName String?\n contactCwId Int?\n contactName String?\n siteCwId Int?\n siteName String?\n customerPO String?\n\n // Financials\n totalSalesTax Float @default(0)\n probability Float @default(0)\n\n // Location / department\n locationName String?\n locationCwId Int?\n departmentName String?\n departmentCwId Int?\n\n // Dates\n expectedCloseDate DateTime?\n pipelineChangeDate DateTime?\n dateBecameLead DateTime?\n closedDate DateTime?\n closedFlag Boolean @default(false)\n closedByName String?\n closedByCwId Int?\n\n // Internal relation to Company (optional, linked by cwCompanyId)\n companyId String?\n company Company? @relation(fields: [companyId], references: [id])\n\n // Local product sequence — array of CW forecast item IDs in display order.\n // When present, fetchProducts() uses this order instead of CW sequenceNumber.\n productSequence Int[] @default([])\n\n cwLastUpdated DateTime?\n\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n}\n\nmodel CredentialType {\n id String @id @default(cuid())\n name String @unique\n\n permissionScope String\n icon String?\n fields Json\n\n credentials Credential[]\n\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n}\n\nmodel SecureValue {\n id String @id @default(cuid())\n name String\n\n content String // Encrypted content\n hash String // Hash of the original content for integrity verification and Search\n\n credentialId String\n credential Credential @relation(fields: [credentialId], references: [id], onDelete: Cascade)\n\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n}\n\nmodel Credential {\n id String @id @default(cuid())\n name String\n notes String?\n subCredentialOfId String?\n subCredentialOf Credential? @relation(\"SubCredentials\", fields: [subCredentialOfId], references: [id], onDelete: Cascade)\n subCredentials Credential[] @relation(\"SubCredentials\")\n\n typeId String\n type CredentialType @relation(fields: [typeId], references: [id], onDelete: Cascade)\n\n fields Json\n\n companyId String\n company Company @relation(fields: [companyId], references: [id], onDelete: Cascade)\n\n securevalues SecureValue[]\n\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n}\n\nmodel GeneratedQuotes {\n id String @id @default(uuid())\n\n quoteRegenData Json @default(\"{}\") // Store any additional data needed for quote regeneration, such as product details, pricing, etc.\n quoteRegenParams Json @default(\"{}\") // Store parameters used for quote regeneration, such as template ID, formatting options, etc.\n quoteRegenHash String @unique @default(\"\")\n\n downloads Json @default(\"[]\") // Array of download records with timestamp and user info\n\n quoteFile Bytes\n quoteFileName String\n\n opportunityId String\n opportunity Opportunity @relation(fields: [opportunityId], references: [id], onDelete: Cascade)\n\n createdById String?\n createdBy User? @relation(fields: [createdById], references: [id], onDelete: SetNull)\n\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n}\n\nmodel CwMember {\n id String @id @default(cuid())\n\n cwMemberId Int @unique\n identifier String @unique\n firstName String\n lastName String\n officeEmail String?\n inactiveFlag Boolean @default(false)\n\n apiKey String?\n\n cwLastUpdated DateTime?\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n}\n", "runtimeDataModel": { "models": {}, "enums": {}, @@ -28,7 +28,7 @@ const config: runtime.GetPrismaClientConfig = { } } -config.runtimeDataModel = JSON.parse("{\"models\":{\"Session\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"sessionKey\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"userId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"expires\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"refreshTokenGenerated\",\"kind\":\"scalar\",\"type\":\"Boolean\"},{\"name\":\"refreshedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"invalidatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"user\",\"kind\":\"object\",\"type\":\"User\",\"relationName\":\"SessionToUser\"}],\"dbName\":null},\"User\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"roles\",\"kind\":\"object\",\"type\":\"Role\",\"relationName\":\"RoleToUser\"},{\"name\":\"permissions\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"login\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"email\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"emailVerified\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"image\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"cwIdentifier\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"userId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"token\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"sessions\",\"kind\":\"object\",\"type\":\"Session\",\"relationName\":\"SessionToUser\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"generatedQuotes\",\"kind\":\"object\",\"type\":\"GeneratedQuotes\",\"relationName\":\"GeneratedQuotesToUser\"}],\"dbName\":null},\"Role\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"title\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"moniker\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"permissions\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"users\",\"kind\":\"object\",\"type\":\"User\",\"relationName\":\"RoleToUser\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"}],\"dbName\":null},\"UnifiSite\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"siteId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"companyId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"company\",\"kind\":\"object\",\"type\":\"Company\",\"relationName\":\"CompanyToUnifiSite\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"}],\"dbName\":null},\"Company\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"cw_CompanyId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"cw_Identifier\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"credentials\",\"kind\":\"object\",\"type\":\"Credential\",\"relationName\":\"CompanyToCredential\"},{\"name\":\"unifiSites\",\"kind\":\"object\",\"type\":\"UnifiSite\",\"relationName\":\"CompanyToUnifiSite\"},{\"name\":\"opportunities\",\"kind\":\"object\",\"type\":\"Opportunity\",\"relationName\":\"CompanyToOpportunity\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"}],\"dbName\":null},\"CatalogItem\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"cwCatalogId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"identifier\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"description\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"customerDescription\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"internalNotes\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"linkedItems\",\"kind\":\"object\",\"type\":\"CatalogItem\",\"relationName\":\"LinkedItems\"},{\"name\":\"linkedTo\",\"kind\":\"object\",\"type\":\"CatalogItem\",\"relationName\":\"LinkedItems\"},{\"name\":\"category\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"categoryCwId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"subcategory\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"subcategoryCwId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"manufacturer\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"manufactureCwId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"partNumber\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"vendorName\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"vendorSku\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"vendorCwId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"price\",\"kind\":\"scalar\",\"type\":\"Float\"},{\"name\":\"cost\",\"kind\":\"scalar\",\"type\":\"Float\"},{\"name\":\"inactive\",\"kind\":\"scalar\",\"type\":\"Boolean\"},{\"name\":\"salesTaxable\",\"kind\":\"scalar\",\"type\":\"Boolean\"},{\"name\":\"onHand\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"cwLastUpdated\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"}],\"dbName\":null},\"Opportunity\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"cwOpportunityId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"notes\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"generatedQuotes\",\"kind\":\"object\",\"type\":\"GeneratedQuotes\",\"relationName\":\"GeneratedQuotesToOpportunity\"},{\"name\":\"typeName\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"typeCwId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"stageName\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"stageCwId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"statusName\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"statusCwId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"priorityName\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"priorityCwId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"ratingName\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"ratingCwId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"source\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"campaignName\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"campaignCwId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"primarySalesRepName\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"primarySalesRepIdentifier\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"primarySalesRepCwId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"secondarySalesRepName\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"secondarySalesRepIdentifier\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"secondarySalesRepCwId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"companyCwId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"companyName\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"contactCwId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"contactName\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"siteCwId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"siteName\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"customerPO\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"totalSalesTax\",\"kind\":\"scalar\",\"type\":\"Float\"},{\"name\":\"probability\",\"kind\":\"scalar\",\"type\":\"Float\"},{\"name\":\"locationName\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"locationCwId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"departmentName\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"departmentCwId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"expectedCloseDate\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"pipelineChangeDate\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"dateBecameLead\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"closedDate\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"closedFlag\",\"kind\":\"scalar\",\"type\":\"Boolean\"},{\"name\":\"closedByName\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"closedByCwId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"companyId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"company\",\"kind\":\"object\",\"type\":\"Company\",\"relationName\":\"CompanyToOpportunity\"},{\"name\":\"productSequence\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"cwLastUpdated\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"}],\"dbName\":null},\"CredentialType\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"permissionScope\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"icon\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"fields\",\"kind\":\"scalar\",\"type\":\"Json\"},{\"name\":\"credentials\",\"kind\":\"object\",\"type\":\"Credential\",\"relationName\":\"CredentialToCredentialType\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"}],\"dbName\":null},\"SecureValue\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"content\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"hash\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"credentialId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"credential\",\"kind\":\"object\",\"type\":\"Credential\",\"relationName\":\"CredentialToSecureValue\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"}],\"dbName\":null},\"Credential\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"notes\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"subCredentialOfId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"subCredentialOf\",\"kind\":\"object\",\"type\":\"Credential\",\"relationName\":\"SubCredentials\"},{\"name\":\"subCredentials\",\"kind\":\"object\",\"type\":\"Credential\",\"relationName\":\"SubCredentials\"},{\"name\":\"typeId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"type\",\"kind\":\"object\",\"type\":\"CredentialType\",\"relationName\":\"CredentialToCredentialType\"},{\"name\":\"fields\",\"kind\":\"scalar\",\"type\":\"Json\"},{\"name\":\"companyId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"company\",\"kind\":\"object\",\"type\":\"Company\",\"relationName\":\"CompanyToCredential\"},{\"name\":\"securevalues\",\"kind\":\"object\",\"type\":\"SecureValue\",\"relationName\":\"CredentialToSecureValue\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"}],\"dbName\":null},\"GeneratedQuotes\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"quoteRegenData\",\"kind\":\"scalar\",\"type\":\"Json\"},{\"name\":\"quoteRegenParams\",\"kind\":\"scalar\",\"type\":\"Json\"},{\"name\":\"quoteRegenHash\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"downloads\",\"kind\":\"scalar\",\"type\":\"Json\"},{\"name\":\"quoteFile\",\"kind\":\"scalar\",\"type\":\"Bytes\"},{\"name\":\"quoteFileName\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"opportunityId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"opportunity\",\"kind\":\"object\",\"type\":\"Opportunity\",\"relationName\":\"GeneratedQuotesToOpportunity\"},{\"name\":\"createdById\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"createdBy\",\"kind\":\"object\",\"type\":\"User\",\"relationName\":\"GeneratedQuotesToUser\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"}],\"dbName\":null}},\"enums\":{},\"types\":{}}") +config.runtimeDataModel = JSON.parse("{\"models\":{\"Session\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"sessionKey\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"userId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"expires\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"refreshTokenGenerated\",\"kind\":\"scalar\",\"type\":\"Boolean\"},{\"name\":\"refreshedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"invalidatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"user\",\"kind\":\"object\",\"type\":\"User\",\"relationName\":\"SessionToUser\"}],\"dbName\":null},\"User\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"roles\",\"kind\":\"object\",\"type\":\"Role\",\"relationName\":\"RoleToUser\"},{\"name\":\"permissions\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"login\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"email\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"emailVerified\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"image\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"cwIdentifier\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"userId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"token\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"sessions\",\"kind\":\"object\",\"type\":\"Session\",\"relationName\":\"SessionToUser\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"generatedQuotes\",\"kind\":\"object\",\"type\":\"GeneratedQuotes\",\"relationName\":\"GeneratedQuotesToUser\"}],\"dbName\":null},\"Role\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"title\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"moniker\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"permissions\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"users\",\"kind\":\"object\",\"type\":\"User\",\"relationName\":\"RoleToUser\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"}],\"dbName\":null},\"UnifiSite\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"siteId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"companyId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"company\",\"kind\":\"object\",\"type\":\"Company\",\"relationName\":\"CompanyToUnifiSite\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"}],\"dbName\":null},\"Company\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"cw_CompanyId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"cw_Identifier\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"credentials\",\"kind\":\"object\",\"type\":\"Credential\",\"relationName\":\"CompanyToCredential\"},{\"name\":\"unifiSites\",\"kind\":\"object\",\"type\":\"UnifiSite\",\"relationName\":\"CompanyToUnifiSite\"},{\"name\":\"opportunities\",\"kind\":\"object\",\"type\":\"Opportunity\",\"relationName\":\"CompanyToOpportunity\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"}],\"dbName\":null},\"CatalogItem\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"cwCatalogId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"identifier\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"description\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"customerDescription\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"internalNotes\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"linkedItems\",\"kind\":\"object\",\"type\":\"CatalogItem\",\"relationName\":\"LinkedItems\"},{\"name\":\"linkedTo\",\"kind\":\"object\",\"type\":\"CatalogItem\",\"relationName\":\"LinkedItems\"},{\"name\":\"category\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"categoryCwId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"subcategory\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"subcategoryCwId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"manufacturer\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"manufactureCwId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"partNumber\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"vendorName\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"vendorSku\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"vendorCwId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"price\",\"kind\":\"scalar\",\"type\":\"Float\"},{\"name\":\"cost\",\"kind\":\"scalar\",\"type\":\"Float\"},{\"name\":\"inactive\",\"kind\":\"scalar\",\"type\":\"Boolean\"},{\"name\":\"salesTaxable\",\"kind\":\"scalar\",\"type\":\"Boolean\"},{\"name\":\"onHand\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"cwLastUpdated\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"}],\"dbName\":null},\"Opportunity\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"cwOpportunityId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"notes\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"generatedQuotes\",\"kind\":\"object\",\"type\":\"GeneratedQuotes\",\"relationName\":\"GeneratedQuotesToOpportunity\"},{\"name\":\"typeName\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"typeCwId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"stageName\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"stageCwId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"statusName\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"statusCwId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"priorityName\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"priorityCwId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"ratingName\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"ratingCwId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"source\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"campaignName\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"campaignCwId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"primarySalesRepName\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"primarySalesRepIdentifier\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"primarySalesRepCwId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"secondarySalesRepName\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"secondarySalesRepIdentifier\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"secondarySalesRepCwId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"companyCwId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"companyName\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"contactCwId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"contactName\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"siteCwId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"siteName\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"customerPO\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"totalSalesTax\",\"kind\":\"scalar\",\"type\":\"Float\"},{\"name\":\"probability\",\"kind\":\"scalar\",\"type\":\"Float\"},{\"name\":\"locationName\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"locationCwId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"departmentName\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"departmentCwId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"expectedCloseDate\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"pipelineChangeDate\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"dateBecameLead\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"closedDate\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"closedFlag\",\"kind\":\"scalar\",\"type\":\"Boolean\"},{\"name\":\"closedByName\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"closedByCwId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"companyId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"company\",\"kind\":\"object\",\"type\":\"Company\",\"relationName\":\"CompanyToOpportunity\"},{\"name\":\"productSequence\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"cwLastUpdated\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"}],\"dbName\":null},\"CredentialType\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"permissionScope\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"icon\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"fields\",\"kind\":\"scalar\",\"type\":\"Json\"},{\"name\":\"credentials\",\"kind\":\"object\",\"type\":\"Credential\",\"relationName\":\"CredentialToCredentialType\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"}],\"dbName\":null},\"SecureValue\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"content\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"hash\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"credentialId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"credential\",\"kind\":\"object\",\"type\":\"Credential\",\"relationName\":\"CredentialToSecureValue\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"}],\"dbName\":null},\"Credential\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"notes\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"subCredentialOfId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"subCredentialOf\",\"kind\":\"object\",\"type\":\"Credential\",\"relationName\":\"SubCredentials\"},{\"name\":\"subCredentials\",\"kind\":\"object\",\"type\":\"Credential\",\"relationName\":\"SubCredentials\"},{\"name\":\"typeId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"type\",\"kind\":\"object\",\"type\":\"CredentialType\",\"relationName\":\"CredentialToCredentialType\"},{\"name\":\"fields\",\"kind\":\"scalar\",\"type\":\"Json\"},{\"name\":\"companyId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"company\",\"kind\":\"object\",\"type\":\"Company\",\"relationName\":\"CompanyToCredential\"},{\"name\":\"securevalues\",\"kind\":\"object\",\"type\":\"SecureValue\",\"relationName\":\"CredentialToSecureValue\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"}],\"dbName\":null},\"GeneratedQuotes\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"quoteRegenData\",\"kind\":\"scalar\",\"type\":\"Json\"},{\"name\":\"quoteRegenParams\",\"kind\":\"scalar\",\"type\":\"Json\"},{\"name\":\"quoteRegenHash\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"downloads\",\"kind\":\"scalar\",\"type\":\"Json\"},{\"name\":\"quoteFile\",\"kind\":\"scalar\",\"type\":\"Bytes\"},{\"name\":\"quoteFileName\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"opportunityId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"opportunity\",\"kind\":\"object\",\"type\":\"Opportunity\",\"relationName\":\"GeneratedQuotesToOpportunity\"},{\"name\":\"createdById\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"createdBy\",\"kind\":\"object\",\"type\":\"User\",\"relationName\":\"GeneratedQuotesToUser\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"}],\"dbName\":null},\"CwMember\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"cwMemberId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"identifier\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"firstName\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"lastName\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"officeEmail\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"inactiveFlag\",\"kind\":\"scalar\",\"type\":\"Boolean\"},{\"name\":\"apiKey\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"cwLastUpdated\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"}],\"dbName\":null}},\"enums\":{},\"types\":{}}") async function decodeBase64AsWasm(wasmBase64: string): Promise { const { Buffer } = await import('node:buffer') @@ -285,6 +285,16 @@ export interface PrismaClient< * ``` */ get generatedQuotes(): Prisma.GeneratedQuotesDelegate; + + /** + * `prisma.cwMember`: Exposes CRUD operations for the **CwMember** model. + * Example usage: + * ```ts + * // Fetch zero or more CwMembers + * const cwMembers = await prisma.cwMember.findMany() + * ``` + */ + get cwMember(): Prisma.CwMemberDelegate; } export function getPrismaClientClass(): PrismaClientConstructor { diff --git a/generated/prisma/internal/prismaNamespace.ts b/generated/prisma/internal/prismaNamespace.ts index 01066fe..f2f18c5 100644 --- a/generated/prisma/internal/prismaNamespace.ts +++ b/generated/prisma/internal/prismaNamespace.ts @@ -394,7 +394,8 @@ export const ModelName = { CredentialType: 'CredentialType', SecureValue: 'SecureValue', Credential: 'Credential', - GeneratedQuotes: 'GeneratedQuotes' + GeneratedQuotes: 'GeneratedQuotes', + CwMember: 'CwMember' } as const export type ModelName = (typeof ModelName)[keyof typeof ModelName] @@ -410,7 +411,7 @@ export type TypeMap + fields: Prisma.CwMemberFieldRefs + operations: { + findUnique: { + args: Prisma.CwMemberFindUniqueArgs + result: runtime.Types.Utils.PayloadToResult | null + } + findUniqueOrThrow: { + args: Prisma.CwMemberFindUniqueOrThrowArgs + result: runtime.Types.Utils.PayloadToResult + } + findFirst: { + args: Prisma.CwMemberFindFirstArgs + result: runtime.Types.Utils.PayloadToResult | null + } + findFirstOrThrow: { + args: Prisma.CwMemberFindFirstOrThrowArgs + result: runtime.Types.Utils.PayloadToResult + } + findMany: { + args: Prisma.CwMemberFindManyArgs + result: runtime.Types.Utils.PayloadToResult[] + } + create: { + args: Prisma.CwMemberCreateArgs + result: runtime.Types.Utils.PayloadToResult + } + createMany: { + args: Prisma.CwMemberCreateManyArgs + result: BatchPayload + } + createManyAndReturn: { + args: Prisma.CwMemberCreateManyAndReturnArgs + result: runtime.Types.Utils.PayloadToResult[] + } + delete: { + args: Prisma.CwMemberDeleteArgs + result: runtime.Types.Utils.PayloadToResult + } + update: { + args: Prisma.CwMemberUpdateArgs + result: runtime.Types.Utils.PayloadToResult + } + deleteMany: { + args: Prisma.CwMemberDeleteManyArgs + result: BatchPayload + } + updateMany: { + args: Prisma.CwMemberUpdateManyArgs + result: BatchPayload + } + updateManyAndReturn: { + args: Prisma.CwMemberUpdateManyAndReturnArgs + result: runtime.Types.Utils.PayloadToResult[] + } + upsert: { + args: Prisma.CwMemberUpsertArgs + result: runtime.Types.Utils.PayloadToResult + } + aggregate: { + args: Prisma.CwMemberAggregateArgs + result: runtime.Types.Utils.Optional + } + groupBy: { + args: Prisma.CwMemberGroupByArgs + result: runtime.Types.Utils.Optional[] + } + count: { + args: Prisma.CwMemberCountArgs + result: runtime.Types.Utils.Optional | number + } + } + } } } & { other: { @@ -1477,6 +1552,23 @@ export const GeneratedQuotesScalarFieldEnum = { export type GeneratedQuotesScalarFieldEnum = (typeof GeneratedQuotesScalarFieldEnum)[keyof typeof GeneratedQuotesScalarFieldEnum] +export const CwMemberScalarFieldEnum = { + id: 'id', + cwMemberId: 'cwMemberId', + identifier: 'identifier', + firstName: 'firstName', + lastName: 'lastName', + officeEmail: 'officeEmail', + inactiveFlag: 'inactiveFlag', + apiKey: 'apiKey', + cwLastUpdated: 'cwLastUpdated', + createdAt: 'createdAt', + updatedAt: 'updatedAt' +} as const + +export type CwMemberScalarFieldEnum = (typeof CwMemberScalarFieldEnum)[keyof typeof CwMemberScalarFieldEnum] + + export const SortOrder = { asc: 'asc', desc: 'desc' @@ -1719,6 +1811,7 @@ export type GlobalOmitConfig = { secureValue?: Prisma.SecureValueOmit credential?: Prisma.CredentialOmit generatedQuotes?: Prisma.GeneratedQuotesOmit + cwMember?: Prisma.CwMemberOmit } /* Types for Logging */ diff --git a/generated/prisma/internal/prismaNamespaceBrowser.ts b/generated/prisma/internal/prismaNamespaceBrowser.ts index 39bb161..d203a3a 100644 --- a/generated/prisma/internal/prismaNamespaceBrowser.ts +++ b/generated/prisma/internal/prismaNamespaceBrowser.ts @@ -61,7 +61,8 @@ export const ModelName = { CredentialType: 'CredentialType', SecureValue: 'SecureValue', Credential: 'Credential', - GeneratedQuotes: 'GeneratedQuotes' + GeneratedQuotes: 'GeneratedQuotes', + CwMember: 'CwMember' } as const export type ModelName = (typeof ModelName)[keyof typeof ModelName] @@ -290,6 +291,23 @@ export const GeneratedQuotesScalarFieldEnum = { export type GeneratedQuotesScalarFieldEnum = (typeof GeneratedQuotesScalarFieldEnum)[keyof typeof GeneratedQuotesScalarFieldEnum] +export const CwMemberScalarFieldEnum = { + id: 'id', + cwMemberId: 'cwMemberId', + identifier: 'identifier', + firstName: 'firstName', + lastName: 'lastName', + officeEmail: 'officeEmail', + inactiveFlag: 'inactiveFlag', + apiKey: 'apiKey', + cwLastUpdated: 'cwLastUpdated', + createdAt: 'createdAt', + updatedAt: 'updatedAt' +} as const + +export type CwMemberScalarFieldEnum = (typeof CwMemberScalarFieldEnum)[keyof typeof CwMemberScalarFieldEnum] + + export const SortOrder = { asc: 'asc', desc: 'desc' diff --git a/generated/prisma/models.ts b/generated/prisma/models.ts index 91d4be3..f71cab0 100644 --- a/generated/prisma/models.ts +++ b/generated/prisma/models.ts @@ -19,4 +19,5 @@ export type * from './models/CredentialType.ts' export type * from './models/SecureValue.ts' export type * from './models/Credential.ts' export type * from './models/GeneratedQuotes.ts' +export type * from './models/CwMember.ts' export type * from './commonInputTypes.ts' \ No newline at end of file diff --git a/generated/prisma/models/CwMember.ts b/generated/prisma/models/CwMember.ts new file mode 100644 index 0000000..9912ff0 --- /dev/null +++ b/generated/prisma/models/CwMember.ts @@ -0,0 +1,1356 @@ + +/* !!! This is code generated by Prisma. Do not edit directly. !!! */ +/* eslint-disable */ +// biome-ignore-all lint: generated file +// @ts-nocheck +/* + * This file exports the `CwMember` model and its related types. + * + * 🟢 You can import this file directly. + */ +import type * as runtime from "@prisma/client/runtime/client" +import type * as $Enums from "../enums.ts" +import type * as Prisma from "../internal/prismaNamespace.ts" + +/** + * Model CwMember + * + */ +export type CwMemberModel = runtime.Types.Result.DefaultSelection + +export type AggregateCwMember = { + _count: CwMemberCountAggregateOutputType | null + _avg: CwMemberAvgAggregateOutputType | null + _sum: CwMemberSumAggregateOutputType | null + _min: CwMemberMinAggregateOutputType | null + _max: CwMemberMaxAggregateOutputType | null +} + +export type CwMemberAvgAggregateOutputType = { + cwMemberId: number | null +} + +export type CwMemberSumAggregateOutputType = { + cwMemberId: number | null +} + +export type CwMemberMinAggregateOutputType = { + id: string | null + cwMemberId: number | null + identifier: string | null + firstName: string | null + lastName: string | null + officeEmail: string | null + inactiveFlag: boolean | null + apiKey: string | null + cwLastUpdated: Date | null + createdAt: Date | null + updatedAt: Date | null +} + +export type CwMemberMaxAggregateOutputType = { + id: string | null + cwMemberId: number | null + identifier: string | null + firstName: string | null + lastName: string | null + officeEmail: string | null + inactiveFlag: boolean | null + apiKey: string | null + cwLastUpdated: Date | null + createdAt: Date | null + updatedAt: Date | null +} + +export type CwMemberCountAggregateOutputType = { + id: number + cwMemberId: number + identifier: number + firstName: number + lastName: number + officeEmail: number + inactiveFlag: number + apiKey: number + cwLastUpdated: number + createdAt: number + updatedAt: number + _all: number +} + + +export type CwMemberAvgAggregateInputType = { + cwMemberId?: true +} + +export type CwMemberSumAggregateInputType = { + cwMemberId?: true +} + +export type CwMemberMinAggregateInputType = { + id?: true + cwMemberId?: true + identifier?: true + firstName?: true + lastName?: true + officeEmail?: true + inactiveFlag?: true + apiKey?: true + cwLastUpdated?: true + createdAt?: true + updatedAt?: true +} + +export type CwMemberMaxAggregateInputType = { + id?: true + cwMemberId?: true + identifier?: true + firstName?: true + lastName?: true + officeEmail?: true + inactiveFlag?: true + apiKey?: true + cwLastUpdated?: true + createdAt?: true + updatedAt?: true +} + +export type CwMemberCountAggregateInputType = { + id?: true + cwMemberId?: true + identifier?: true + firstName?: true + lastName?: true + officeEmail?: true + inactiveFlag?: true + apiKey?: true + cwLastUpdated?: true + createdAt?: true + updatedAt?: true + _all?: true +} + +export type CwMemberAggregateArgs = { + /** + * Filter which CwMember to aggregate. + */ + where?: Prisma.CwMemberWhereInput + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/sorting Sorting Docs} + * + * Determine the order of CwMembers to fetch. + */ + orderBy?: Prisma.CwMemberOrderByWithRelationInput | Prisma.CwMemberOrderByWithRelationInput[] + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination#cursor-based-pagination Cursor Docs} + * + * Sets the start position + */ + cursor?: Prisma.CwMemberWhereUniqueInput + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination Pagination Docs} + * + * Take `±n` CwMembers from the position of the cursor. + */ + take?: number + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination Pagination Docs} + * + * Skip the first `n` CwMembers. + */ + skip?: number + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/aggregations Aggregation Docs} + * + * Count returned CwMembers + **/ + _count?: true | CwMemberCountAggregateInputType + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/aggregations Aggregation Docs} + * + * Select which fields to average + **/ + _avg?: CwMemberAvgAggregateInputType + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/aggregations Aggregation Docs} + * + * Select which fields to sum + **/ + _sum?: CwMemberSumAggregateInputType + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/aggregations Aggregation Docs} + * + * Select which fields to find the minimum value + **/ + _min?: CwMemberMinAggregateInputType + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/aggregations Aggregation Docs} + * + * Select which fields to find the maximum value + **/ + _max?: CwMemberMaxAggregateInputType +} + +export type GetCwMemberAggregateType = { + [P in keyof T & keyof AggregateCwMember]: P extends '_count' | 'count' + ? T[P] extends true + ? number + : Prisma.GetScalarType + : Prisma.GetScalarType +} + + + + +export type CwMemberGroupByArgs = { + where?: Prisma.CwMemberWhereInput + orderBy?: Prisma.CwMemberOrderByWithAggregationInput | Prisma.CwMemberOrderByWithAggregationInput[] + by: Prisma.CwMemberScalarFieldEnum[] | Prisma.CwMemberScalarFieldEnum + having?: Prisma.CwMemberScalarWhereWithAggregatesInput + take?: number + skip?: number + _count?: CwMemberCountAggregateInputType | true + _avg?: CwMemberAvgAggregateInputType + _sum?: CwMemberSumAggregateInputType + _min?: CwMemberMinAggregateInputType + _max?: CwMemberMaxAggregateInputType +} + +export type CwMemberGroupByOutputType = { + id: string + cwMemberId: number + identifier: string + firstName: string + lastName: string + officeEmail: string | null + inactiveFlag: boolean + apiKey: string | null + cwLastUpdated: Date | null + createdAt: Date + updatedAt: Date + _count: CwMemberCountAggregateOutputType | null + _avg: CwMemberAvgAggregateOutputType | null + _sum: CwMemberSumAggregateOutputType | null + _min: CwMemberMinAggregateOutputType | null + _max: CwMemberMaxAggregateOutputType | null +} + +type GetCwMemberGroupByPayload = Prisma.PrismaPromise< + Array< + Prisma.PickEnumerable & + { + [P in ((keyof T) & (keyof CwMemberGroupByOutputType))]: P extends '_count' + ? T[P] extends boolean + ? number + : Prisma.GetScalarType + : Prisma.GetScalarType + } + > + > + + + +export type CwMemberWhereInput = { + AND?: Prisma.CwMemberWhereInput | Prisma.CwMemberWhereInput[] + OR?: Prisma.CwMemberWhereInput[] + NOT?: Prisma.CwMemberWhereInput | Prisma.CwMemberWhereInput[] + id?: Prisma.StringFilter<"CwMember"> | string + cwMemberId?: Prisma.IntFilter<"CwMember"> | number + identifier?: Prisma.StringFilter<"CwMember"> | string + firstName?: Prisma.StringFilter<"CwMember"> | string + lastName?: Prisma.StringFilter<"CwMember"> | string + officeEmail?: Prisma.StringNullableFilter<"CwMember"> | string | null + inactiveFlag?: Prisma.BoolFilter<"CwMember"> | boolean + apiKey?: Prisma.StringNullableFilter<"CwMember"> | string | null + cwLastUpdated?: Prisma.DateTimeNullableFilter<"CwMember"> | Date | string | null + createdAt?: Prisma.DateTimeFilter<"CwMember"> | Date | string + updatedAt?: Prisma.DateTimeFilter<"CwMember"> | Date | string +} + +export type CwMemberOrderByWithRelationInput = { + id?: Prisma.SortOrder + cwMemberId?: Prisma.SortOrder + identifier?: Prisma.SortOrder + firstName?: Prisma.SortOrder + lastName?: Prisma.SortOrder + officeEmail?: Prisma.SortOrderInput | Prisma.SortOrder + inactiveFlag?: Prisma.SortOrder + apiKey?: Prisma.SortOrderInput | Prisma.SortOrder + cwLastUpdated?: Prisma.SortOrderInput | Prisma.SortOrder + createdAt?: Prisma.SortOrder + updatedAt?: Prisma.SortOrder +} + +export type CwMemberWhereUniqueInput = Prisma.AtLeast<{ + id?: string + cwMemberId?: number + identifier?: string + AND?: Prisma.CwMemberWhereInput | Prisma.CwMemberWhereInput[] + OR?: Prisma.CwMemberWhereInput[] + NOT?: Prisma.CwMemberWhereInput | Prisma.CwMemberWhereInput[] + firstName?: Prisma.StringFilter<"CwMember"> | string + lastName?: Prisma.StringFilter<"CwMember"> | string + officeEmail?: Prisma.StringNullableFilter<"CwMember"> | string | null + inactiveFlag?: Prisma.BoolFilter<"CwMember"> | boolean + apiKey?: Prisma.StringNullableFilter<"CwMember"> | string | null + cwLastUpdated?: Prisma.DateTimeNullableFilter<"CwMember"> | Date | string | null + createdAt?: Prisma.DateTimeFilter<"CwMember"> | Date | string + updatedAt?: Prisma.DateTimeFilter<"CwMember"> | Date | string +}, "id" | "cwMemberId" | "identifier"> + +export type CwMemberOrderByWithAggregationInput = { + id?: Prisma.SortOrder + cwMemberId?: Prisma.SortOrder + identifier?: Prisma.SortOrder + firstName?: Prisma.SortOrder + lastName?: Prisma.SortOrder + officeEmail?: Prisma.SortOrderInput | Prisma.SortOrder + inactiveFlag?: Prisma.SortOrder + apiKey?: Prisma.SortOrderInput | Prisma.SortOrder + cwLastUpdated?: Prisma.SortOrderInput | Prisma.SortOrder + createdAt?: Prisma.SortOrder + updatedAt?: Prisma.SortOrder + _count?: Prisma.CwMemberCountOrderByAggregateInput + _avg?: Prisma.CwMemberAvgOrderByAggregateInput + _max?: Prisma.CwMemberMaxOrderByAggregateInput + _min?: Prisma.CwMemberMinOrderByAggregateInput + _sum?: Prisma.CwMemberSumOrderByAggregateInput +} + +export type CwMemberScalarWhereWithAggregatesInput = { + AND?: Prisma.CwMemberScalarWhereWithAggregatesInput | Prisma.CwMemberScalarWhereWithAggregatesInput[] + OR?: Prisma.CwMemberScalarWhereWithAggregatesInput[] + NOT?: Prisma.CwMemberScalarWhereWithAggregatesInput | Prisma.CwMemberScalarWhereWithAggregatesInput[] + id?: Prisma.StringWithAggregatesFilter<"CwMember"> | string + cwMemberId?: Prisma.IntWithAggregatesFilter<"CwMember"> | number + identifier?: Prisma.StringWithAggregatesFilter<"CwMember"> | string + firstName?: Prisma.StringWithAggregatesFilter<"CwMember"> | string + lastName?: Prisma.StringWithAggregatesFilter<"CwMember"> | string + officeEmail?: Prisma.StringNullableWithAggregatesFilter<"CwMember"> | string | null + inactiveFlag?: Prisma.BoolWithAggregatesFilter<"CwMember"> | boolean + apiKey?: Prisma.StringNullableWithAggregatesFilter<"CwMember"> | string | null + cwLastUpdated?: Prisma.DateTimeNullableWithAggregatesFilter<"CwMember"> | Date | string | null + createdAt?: Prisma.DateTimeWithAggregatesFilter<"CwMember"> | Date | string + updatedAt?: Prisma.DateTimeWithAggregatesFilter<"CwMember"> | Date | string +} + +export type CwMemberCreateInput = { + id?: string + cwMemberId: number + identifier: string + firstName: string + lastName: string + officeEmail?: string | null + inactiveFlag?: boolean + apiKey?: string | null + cwLastUpdated?: Date | string | null + createdAt?: Date | string + updatedAt?: Date | string +} + +export type CwMemberUncheckedCreateInput = { + id?: string + cwMemberId: number + identifier: string + firstName: string + lastName: string + officeEmail?: string | null + inactiveFlag?: boolean + apiKey?: string | null + cwLastUpdated?: Date | string | null + createdAt?: Date | string + updatedAt?: Date | string +} + +export type CwMemberUpdateInput = { + id?: Prisma.StringFieldUpdateOperationsInput | string + cwMemberId?: Prisma.IntFieldUpdateOperationsInput | number + identifier?: Prisma.StringFieldUpdateOperationsInput | string + firstName?: Prisma.StringFieldUpdateOperationsInput | string + lastName?: Prisma.StringFieldUpdateOperationsInput | string + officeEmail?: Prisma.NullableStringFieldUpdateOperationsInput | string | null + inactiveFlag?: Prisma.BoolFieldUpdateOperationsInput | boolean + apiKey?: Prisma.NullableStringFieldUpdateOperationsInput | string | null + cwLastUpdated?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null + createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string + updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string +} + +export type CwMemberUncheckedUpdateInput = { + id?: Prisma.StringFieldUpdateOperationsInput | string + cwMemberId?: Prisma.IntFieldUpdateOperationsInput | number + identifier?: Prisma.StringFieldUpdateOperationsInput | string + firstName?: Prisma.StringFieldUpdateOperationsInput | string + lastName?: Prisma.StringFieldUpdateOperationsInput | string + officeEmail?: Prisma.NullableStringFieldUpdateOperationsInput | string | null + inactiveFlag?: Prisma.BoolFieldUpdateOperationsInput | boolean + apiKey?: Prisma.NullableStringFieldUpdateOperationsInput | string | null + cwLastUpdated?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null + createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string + updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string +} + +export type CwMemberCreateManyInput = { + id?: string + cwMemberId: number + identifier: string + firstName: string + lastName: string + officeEmail?: string | null + inactiveFlag?: boolean + apiKey?: string | null + cwLastUpdated?: Date | string | null + createdAt?: Date | string + updatedAt?: Date | string +} + +export type CwMemberUpdateManyMutationInput = { + id?: Prisma.StringFieldUpdateOperationsInput | string + cwMemberId?: Prisma.IntFieldUpdateOperationsInput | number + identifier?: Prisma.StringFieldUpdateOperationsInput | string + firstName?: Prisma.StringFieldUpdateOperationsInput | string + lastName?: Prisma.StringFieldUpdateOperationsInput | string + officeEmail?: Prisma.NullableStringFieldUpdateOperationsInput | string | null + inactiveFlag?: Prisma.BoolFieldUpdateOperationsInput | boolean + apiKey?: Prisma.NullableStringFieldUpdateOperationsInput | string | null + cwLastUpdated?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null + createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string + updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string +} + +export type CwMemberUncheckedUpdateManyInput = { + id?: Prisma.StringFieldUpdateOperationsInput | string + cwMemberId?: Prisma.IntFieldUpdateOperationsInput | number + identifier?: Prisma.StringFieldUpdateOperationsInput | string + firstName?: Prisma.StringFieldUpdateOperationsInput | string + lastName?: Prisma.StringFieldUpdateOperationsInput | string + officeEmail?: Prisma.NullableStringFieldUpdateOperationsInput | string | null + inactiveFlag?: Prisma.BoolFieldUpdateOperationsInput | boolean + apiKey?: Prisma.NullableStringFieldUpdateOperationsInput | string | null + cwLastUpdated?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null + createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string + updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string +} + +export type CwMemberCountOrderByAggregateInput = { + id?: Prisma.SortOrder + cwMemberId?: Prisma.SortOrder + identifier?: Prisma.SortOrder + firstName?: Prisma.SortOrder + lastName?: Prisma.SortOrder + officeEmail?: Prisma.SortOrder + inactiveFlag?: Prisma.SortOrder + apiKey?: Prisma.SortOrder + cwLastUpdated?: Prisma.SortOrder + createdAt?: Prisma.SortOrder + updatedAt?: Prisma.SortOrder +} + +export type CwMemberAvgOrderByAggregateInput = { + cwMemberId?: Prisma.SortOrder +} + +export type CwMemberMaxOrderByAggregateInput = { + id?: Prisma.SortOrder + cwMemberId?: Prisma.SortOrder + identifier?: Prisma.SortOrder + firstName?: Prisma.SortOrder + lastName?: Prisma.SortOrder + officeEmail?: Prisma.SortOrder + inactiveFlag?: Prisma.SortOrder + apiKey?: Prisma.SortOrder + cwLastUpdated?: Prisma.SortOrder + createdAt?: Prisma.SortOrder + updatedAt?: Prisma.SortOrder +} + +export type CwMemberMinOrderByAggregateInput = { + id?: Prisma.SortOrder + cwMemberId?: Prisma.SortOrder + identifier?: Prisma.SortOrder + firstName?: Prisma.SortOrder + lastName?: Prisma.SortOrder + officeEmail?: Prisma.SortOrder + inactiveFlag?: Prisma.SortOrder + apiKey?: Prisma.SortOrder + cwLastUpdated?: Prisma.SortOrder + createdAt?: Prisma.SortOrder + updatedAt?: Prisma.SortOrder +} + +export type CwMemberSumOrderByAggregateInput = { + cwMemberId?: Prisma.SortOrder +} + + + +export type CwMemberSelect = runtime.Types.Extensions.GetSelect<{ + id?: boolean + cwMemberId?: boolean + identifier?: boolean + firstName?: boolean + lastName?: boolean + officeEmail?: boolean + inactiveFlag?: boolean + apiKey?: boolean + cwLastUpdated?: boolean + createdAt?: boolean + updatedAt?: boolean +}, ExtArgs["result"]["cwMember"]> + +export type CwMemberSelectCreateManyAndReturn = runtime.Types.Extensions.GetSelect<{ + id?: boolean + cwMemberId?: boolean + identifier?: boolean + firstName?: boolean + lastName?: boolean + officeEmail?: boolean + inactiveFlag?: boolean + apiKey?: boolean + cwLastUpdated?: boolean + createdAt?: boolean + updatedAt?: boolean +}, ExtArgs["result"]["cwMember"]> + +export type CwMemberSelectUpdateManyAndReturn = runtime.Types.Extensions.GetSelect<{ + id?: boolean + cwMemberId?: boolean + identifier?: boolean + firstName?: boolean + lastName?: boolean + officeEmail?: boolean + inactiveFlag?: boolean + apiKey?: boolean + cwLastUpdated?: boolean + createdAt?: boolean + updatedAt?: boolean +}, ExtArgs["result"]["cwMember"]> + +export type CwMemberSelectScalar = { + id?: boolean + cwMemberId?: boolean + identifier?: boolean + firstName?: boolean + lastName?: boolean + officeEmail?: boolean + inactiveFlag?: boolean + apiKey?: boolean + cwLastUpdated?: boolean + createdAt?: boolean + updatedAt?: boolean +} + +export type CwMemberOmit = runtime.Types.Extensions.GetOmit<"id" | "cwMemberId" | "identifier" | "firstName" | "lastName" | "officeEmail" | "inactiveFlag" | "apiKey" | "cwLastUpdated" | "createdAt" | "updatedAt", ExtArgs["result"]["cwMember"]> + +export type $CwMemberPayload = { + name: "CwMember" + objects: {} + scalars: runtime.Types.Extensions.GetPayloadResult<{ + id: string + cwMemberId: number + identifier: string + firstName: string + lastName: string + officeEmail: string | null + inactiveFlag: boolean + apiKey: string | null + cwLastUpdated: Date | null + createdAt: Date + updatedAt: Date + }, ExtArgs["result"]["cwMember"]> + composites: {} +} + +export type CwMemberGetPayload = runtime.Types.Result.GetResult + +export type CwMemberCountArgs = + Omit & { + select?: CwMemberCountAggregateInputType | true + } + +export interface CwMemberDelegate { + [K: symbol]: { types: Prisma.TypeMap['model']['CwMember'], meta: { name: 'CwMember' } } + /** + * Find zero or one CwMember that matches the filter. + * @param {CwMemberFindUniqueArgs} args - Arguments to find a CwMember + * @example + * // Get one CwMember + * const cwMember = await prisma.cwMember.findUnique({ + * where: { + * // ... provide filter here + * } + * }) + */ + findUnique(args: Prisma.SelectSubset>): Prisma.Prisma__CwMemberClient, T, "findUnique", GlobalOmitOptions> | null, null, ExtArgs, GlobalOmitOptions> + + /** + * Find one CwMember that matches the filter or throw an error with `error.code='P2025'` + * if no matches were found. + * @param {CwMemberFindUniqueOrThrowArgs} args - Arguments to find a CwMember + * @example + * // Get one CwMember + * const cwMember = await prisma.cwMember.findUniqueOrThrow({ + * where: { + * // ... provide filter here + * } + * }) + */ + findUniqueOrThrow(args: Prisma.SelectSubset>): Prisma.Prisma__CwMemberClient, T, "findUniqueOrThrow", GlobalOmitOptions>, never, ExtArgs, GlobalOmitOptions> + + /** + * Find the first CwMember that matches the filter. + * Note, that providing `undefined` is treated as the value not being there. + * Read more here: https://pris.ly/d/null-undefined + * @param {CwMemberFindFirstArgs} args - Arguments to find a CwMember + * @example + * // Get one CwMember + * const cwMember = await prisma.cwMember.findFirst({ + * where: { + * // ... provide filter here + * } + * }) + */ + findFirst(args?: Prisma.SelectSubset>): Prisma.Prisma__CwMemberClient, T, "findFirst", GlobalOmitOptions> | null, null, ExtArgs, GlobalOmitOptions> + + /** + * Find the first CwMember that matches the filter or + * throw `PrismaKnownClientError` with `P2025` code if no matches were found. + * Note, that providing `undefined` is treated as the value not being there. + * Read more here: https://pris.ly/d/null-undefined + * @param {CwMemberFindFirstOrThrowArgs} args - Arguments to find a CwMember + * @example + * // Get one CwMember + * const cwMember = await prisma.cwMember.findFirstOrThrow({ + * where: { + * // ... provide filter here + * } + * }) + */ + findFirstOrThrow(args?: Prisma.SelectSubset>): Prisma.Prisma__CwMemberClient, T, "findFirstOrThrow", GlobalOmitOptions>, never, ExtArgs, GlobalOmitOptions> + + /** + * Find zero or more CwMembers that matches the filter. + * Note, that providing `undefined` is treated as the value not being there. + * Read more here: https://pris.ly/d/null-undefined + * @param {CwMemberFindManyArgs} args - Arguments to filter and select certain fields only. + * @example + * // Get all CwMembers + * const cwMembers = await prisma.cwMember.findMany() + * + * // Get first 10 CwMembers + * const cwMembers = await prisma.cwMember.findMany({ take: 10 }) + * + * // Only select the `id` + * const cwMemberWithIdOnly = await prisma.cwMember.findMany({ select: { id: true } }) + * + */ + findMany(args?: Prisma.SelectSubset>): Prisma.PrismaPromise, T, "findMany", GlobalOmitOptions>> + + /** + * Create a CwMember. + * @param {CwMemberCreateArgs} args - Arguments to create a CwMember. + * @example + * // Create one CwMember + * const CwMember = await prisma.cwMember.create({ + * data: { + * // ... data to create a CwMember + * } + * }) + * + */ + create(args: Prisma.SelectSubset>): Prisma.Prisma__CwMemberClient, T, "create", GlobalOmitOptions>, never, ExtArgs, GlobalOmitOptions> + + /** + * Create many CwMembers. + * @param {CwMemberCreateManyArgs} args - Arguments to create many CwMembers. + * @example + * // Create many CwMembers + * const cwMember = await prisma.cwMember.createMany({ + * data: [ + * // ... provide data here + * ] + * }) + * + */ + createMany(args?: Prisma.SelectSubset>): Prisma.PrismaPromise + + /** + * Create many CwMembers and returns the data saved in the database. + * @param {CwMemberCreateManyAndReturnArgs} args - Arguments to create many CwMembers. + * @example + * // Create many CwMembers + * const cwMember = await prisma.cwMember.createManyAndReturn({ + * data: [ + * // ... provide data here + * ] + * }) + * + * // Create many CwMembers and only return the `id` + * const cwMemberWithIdOnly = await prisma.cwMember.createManyAndReturn({ + * select: { id: true }, + * data: [ + * // ... provide data here + * ] + * }) + * Note, that providing `undefined` is treated as the value not being there. + * Read more here: https://pris.ly/d/null-undefined + * + */ + createManyAndReturn(args?: Prisma.SelectSubset>): Prisma.PrismaPromise, T, "createManyAndReturn", GlobalOmitOptions>> + + /** + * Delete a CwMember. + * @param {CwMemberDeleteArgs} args - Arguments to delete one CwMember. + * @example + * // Delete one CwMember + * const CwMember = await prisma.cwMember.delete({ + * where: { + * // ... filter to delete one CwMember + * } + * }) + * + */ + delete(args: Prisma.SelectSubset>): Prisma.Prisma__CwMemberClient, T, "delete", GlobalOmitOptions>, never, ExtArgs, GlobalOmitOptions> + + /** + * Update one CwMember. + * @param {CwMemberUpdateArgs} args - Arguments to update one CwMember. + * @example + * // Update one CwMember + * const cwMember = await prisma.cwMember.update({ + * where: { + * // ... provide filter here + * }, + * data: { + * // ... provide data here + * } + * }) + * + */ + update(args: Prisma.SelectSubset>): Prisma.Prisma__CwMemberClient, T, "update", GlobalOmitOptions>, never, ExtArgs, GlobalOmitOptions> + + /** + * Delete zero or more CwMembers. + * @param {CwMemberDeleteManyArgs} args - Arguments to filter CwMembers to delete. + * @example + * // Delete a few CwMembers + * const { count } = await prisma.cwMember.deleteMany({ + * where: { + * // ... provide filter here + * } + * }) + * + */ + deleteMany(args?: Prisma.SelectSubset>): Prisma.PrismaPromise + + /** + * Update zero or more CwMembers. + * Note, that providing `undefined` is treated as the value not being there. + * Read more here: https://pris.ly/d/null-undefined + * @param {CwMemberUpdateManyArgs} args - Arguments to update one or more rows. + * @example + * // Update many CwMembers + * const cwMember = await prisma.cwMember.updateMany({ + * where: { + * // ... provide filter here + * }, + * data: { + * // ... provide data here + * } + * }) + * + */ + updateMany(args: Prisma.SelectSubset>): Prisma.PrismaPromise + + /** + * Update zero or more CwMembers and returns the data updated in the database. + * @param {CwMemberUpdateManyAndReturnArgs} args - Arguments to update many CwMembers. + * @example + * // Update many CwMembers + * const cwMember = await prisma.cwMember.updateManyAndReturn({ + * where: { + * // ... provide filter here + * }, + * data: [ + * // ... provide data here + * ] + * }) + * + * // Update zero or more CwMembers and only return the `id` + * const cwMemberWithIdOnly = await prisma.cwMember.updateManyAndReturn({ + * select: { id: true }, + * where: { + * // ... provide filter here + * }, + * data: [ + * // ... provide data here + * ] + * }) + * Note, that providing `undefined` is treated as the value not being there. + * Read more here: https://pris.ly/d/null-undefined + * + */ + updateManyAndReturn(args: Prisma.SelectSubset>): Prisma.PrismaPromise, T, "updateManyAndReturn", GlobalOmitOptions>> + + /** + * Create or update one CwMember. + * @param {CwMemberUpsertArgs} args - Arguments to update or create a CwMember. + * @example + * // Update or create a CwMember + * const cwMember = await prisma.cwMember.upsert({ + * create: { + * // ... data to create a CwMember + * }, + * update: { + * // ... in case it already exists, update + * }, + * where: { + * // ... the filter for the CwMember we want to update + * } + * }) + */ + upsert(args: Prisma.SelectSubset>): Prisma.Prisma__CwMemberClient, T, "upsert", GlobalOmitOptions>, never, ExtArgs, GlobalOmitOptions> + + + /** + * Count the number of CwMembers. + * Note, that providing `undefined` is treated as the value not being there. + * Read more here: https://pris.ly/d/null-undefined + * @param {CwMemberCountArgs} args - Arguments to filter CwMembers to count. + * @example + * // Count the number of CwMembers + * const count = await prisma.cwMember.count({ + * where: { + * // ... the filter for the CwMembers we want to count + * } + * }) + **/ + count( + args?: Prisma.Subset, + ): Prisma.PrismaPromise< + T extends runtime.Types.Utils.Record<'select', any> + ? T['select'] extends true + ? number + : Prisma.GetScalarType + : number + > + + /** + * Allows you to perform aggregations operations on a CwMember. + * Note, that providing `undefined` is treated as the value not being there. + * Read more here: https://pris.ly/d/null-undefined + * @param {CwMemberAggregateArgs} args - Select which aggregations you would like to apply and on what fields. + * @example + * // Ordered by age ascending + * // Where email contains prisma.io + * // Limited to the 10 users + * const aggregations = await prisma.user.aggregate({ + * _avg: { + * age: true, + * }, + * where: { + * email: { + * contains: "prisma.io", + * }, + * }, + * orderBy: { + * age: "asc", + * }, + * take: 10, + * }) + **/ + aggregate(args: Prisma.Subset): Prisma.PrismaPromise> + + /** + * Group by CwMember. + * Note, that providing `undefined` is treated as the value not being there. + * Read more here: https://pris.ly/d/null-undefined + * @param {CwMemberGroupByArgs} args - Group by arguments. + * @example + * // Group by city, order by createdAt, get count + * const result = await prisma.user.groupBy({ + * by: ['city', 'createdAt'], + * orderBy: { + * createdAt: true + * }, + * _count: { + * _all: true + * }, + * }) + * + **/ + groupBy< + T extends CwMemberGroupByArgs, + HasSelectOrTake extends Prisma.Or< + Prisma.Extends<'skip', Prisma.Keys>, + Prisma.Extends<'take', Prisma.Keys> + >, + OrderByArg extends Prisma.True extends HasSelectOrTake + ? { orderBy: CwMemberGroupByArgs['orderBy'] } + : { orderBy?: CwMemberGroupByArgs['orderBy'] }, + OrderFields extends Prisma.ExcludeUnderscoreKeys>>, + ByFields extends Prisma.MaybeTupleToUnion, + ByValid extends Prisma.Has, + HavingFields extends Prisma.GetHavingFields, + HavingValid extends Prisma.Has, + ByEmpty extends T['by'] extends never[] ? Prisma.True : Prisma.False, + InputErrors extends ByEmpty extends Prisma.True + ? `Error: "by" must not be empty.` + : HavingValid extends Prisma.False + ? { + [P in HavingFields]: P extends ByFields + ? never + : P extends string + ? `Error: Field "${P}" used in "having" needs to be provided in "by".` + : [ + Error, + 'Field ', + P, + ` in "having" needs to be provided in "by"`, + ] + }[HavingFields] + : 'take' extends Prisma.Keys + ? 'orderBy' extends Prisma.Keys + ? ByValid extends Prisma.True + ? {} + : { + [P in OrderFields]: P extends ByFields + ? never + : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` + }[OrderFields] + : 'Error: If you provide "take", you also need to provide "orderBy"' + : 'skip' extends Prisma.Keys + ? 'orderBy' extends Prisma.Keys + ? ByValid extends Prisma.True + ? {} + : { + [P in OrderFields]: P extends ByFields + ? never + : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` + }[OrderFields] + : 'Error: If you provide "skip", you also need to provide "orderBy"' + : ByValid extends Prisma.True + ? {} + : { + [P in OrderFields]: P extends ByFields + ? never + : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` + }[OrderFields] + >(args: Prisma.SubsetIntersection & InputErrors): {} extends InputErrors ? GetCwMemberGroupByPayload : Prisma.PrismaPromise +/** + * Fields of the CwMember model + */ +readonly fields: CwMemberFieldRefs; +} + +/** + * The delegate class that acts as a "Promise-like" for CwMember. + * Why is this prefixed with `Prisma__`? + * Because we want to prevent naming conflicts as mentioned in + * https://github.com/prisma/prisma-client-js/issues/707 + */ +export interface Prisma__CwMemberClient extends Prisma.PrismaPromise { + readonly [Symbol.toStringTag]: "PrismaPromise" + /** + * Attaches callbacks for the resolution and/or rejection of the Promise. + * @param onfulfilled The callback to execute when the Promise is resolved. + * @param onrejected The callback to execute when the Promise is rejected. + * @returns A Promise for the completion of which ever callback is executed. + */ + then(onfulfilled?: ((value: T) => TResult1 | PromiseLike) | undefined | null, onrejected?: ((reason: any) => TResult2 | PromiseLike) | undefined | null): runtime.Types.Utils.JsPromise + /** + * Attaches a callback for only the rejection of the Promise. + * @param onrejected The callback to execute when the Promise is rejected. + * @returns A Promise for the completion of the callback. + */ + catch(onrejected?: ((reason: any) => TResult | PromiseLike) | undefined | null): runtime.Types.Utils.JsPromise + /** + * Attaches a callback that is invoked when the Promise is settled (fulfilled or rejected). The + * resolved value cannot be modified from the callback. + * @param onfinally The callback to execute when the Promise is settled (fulfilled or rejected). + * @returns A Promise for the completion of the callback. + */ + finally(onfinally?: (() => void) | undefined | null): runtime.Types.Utils.JsPromise +} + + + + +/** + * Fields of the CwMember model + */ +export interface CwMemberFieldRefs { + readonly id: Prisma.FieldRef<"CwMember", 'String'> + readonly cwMemberId: Prisma.FieldRef<"CwMember", 'Int'> + readonly identifier: Prisma.FieldRef<"CwMember", 'String'> + readonly firstName: Prisma.FieldRef<"CwMember", 'String'> + readonly lastName: Prisma.FieldRef<"CwMember", 'String'> + readonly officeEmail: Prisma.FieldRef<"CwMember", 'String'> + readonly inactiveFlag: Prisma.FieldRef<"CwMember", 'Boolean'> + readonly apiKey: Prisma.FieldRef<"CwMember", 'String'> + readonly cwLastUpdated: Prisma.FieldRef<"CwMember", 'DateTime'> + readonly createdAt: Prisma.FieldRef<"CwMember", 'DateTime'> + readonly updatedAt: Prisma.FieldRef<"CwMember", 'DateTime'> +} + + +// Custom InputTypes +/** + * CwMember findUnique + */ +export type CwMemberFindUniqueArgs = { + /** + * Select specific fields to fetch from the CwMember + */ + select?: Prisma.CwMemberSelect | null + /** + * Omit specific fields from the CwMember + */ + omit?: Prisma.CwMemberOmit | null + /** + * Filter, which CwMember to fetch. + */ + where: Prisma.CwMemberWhereUniqueInput +} + +/** + * CwMember findUniqueOrThrow + */ +export type CwMemberFindUniqueOrThrowArgs = { + /** + * Select specific fields to fetch from the CwMember + */ + select?: Prisma.CwMemberSelect | null + /** + * Omit specific fields from the CwMember + */ + omit?: Prisma.CwMemberOmit | null + /** + * Filter, which CwMember to fetch. + */ + where: Prisma.CwMemberWhereUniqueInput +} + +/** + * CwMember findFirst + */ +export type CwMemberFindFirstArgs = { + /** + * Select specific fields to fetch from the CwMember + */ + select?: Prisma.CwMemberSelect | null + /** + * Omit specific fields from the CwMember + */ + omit?: Prisma.CwMemberOmit | null + /** + * Filter, which CwMember to fetch. + */ + where?: Prisma.CwMemberWhereInput + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/sorting Sorting Docs} + * + * Determine the order of CwMembers to fetch. + */ + orderBy?: Prisma.CwMemberOrderByWithRelationInput | Prisma.CwMemberOrderByWithRelationInput[] + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination#cursor-based-pagination Cursor Docs} + * + * Sets the position for searching for CwMembers. + */ + cursor?: Prisma.CwMemberWhereUniqueInput + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination Pagination Docs} + * + * Take `±n` CwMembers from the position of the cursor. + */ + take?: number + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination Pagination Docs} + * + * Skip the first `n` CwMembers. + */ + skip?: number + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/distinct Distinct Docs} + * + * Filter by unique combinations of CwMembers. + */ + distinct?: Prisma.CwMemberScalarFieldEnum | Prisma.CwMemberScalarFieldEnum[] +} + +/** + * CwMember findFirstOrThrow + */ +export type CwMemberFindFirstOrThrowArgs = { + /** + * Select specific fields to fetch from the CwMember + */ + select?: Prisma.CwMemberSelect | null + /** + * Omit specific fields from the CwMember + */ + omit?: Prisma.CwMemberOmit | null + /** + * Filter, which CwMember to fetch. + */ + where?: Prisma.CwMemberWhereInput + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/sorting Sorting Docs} + * + * Determine the order of CwMembers to fetch. + */ + orderBy?: Prisma.CwMemberOrderByWithRelationInput | Prisma.CwMemberOrderByWithRelationInput[] + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination#cursor-based-pagination Cursor Docs} + * + * Sets the position for searching for CwMembers. + */ + cursor?: Prisma.CwMemberWhereUniqueInput + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination Pagination Docs} + * + * Take `±n` CwMembers from the position of the cursor. + */ + take?: number + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination Pagination Docs} + * + * Skip the first `n` CwMembers. + */ + skip?: number + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/distinct Distinct Docs} + * + * Filter by unique combinations of CwMembers. + */ + distinct?: Prisma.CwMemberScalarFieldEnum | Prisma.CwMemberScalarFieldEnum[] +} + +/** + * CwMember findMany + */ +export type CwMemberFindManyArgs = { + /** + * Select specific fields to fetch from the CwMember + */ + select?: Prisma.CwMemberSelect | null + /** + * Omit specific fields from the CwMember + */ + omit?: Prisma.CwMemberOmit | null + /** + * Filter, which CwMembers to fetch. + */ + where?: Prisma.CwMemberWhereInput + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/sorting Sorting Docs} + * + * Determine the order of CwMembers to fetch. + */ + orderBy?: Prisma.CwMemberOrderByWithRelationInput | Prisma.CwMemberOrderByWithRelationInput[] + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination#cursor-based-pagination Cursor Docs} + * + * Sets the position for listing CwMembers. + */ + cursor?: Prisma.CwMemberWhereUniqueInput + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination Pagination Docs} + * + * Take `±n` CwMembers from the position of the cursor. + */ + take?: number + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination Pagination Docs} + * + * Skip the first `n` CwMembers. + */ + skip?: number + distinct?: Prisma.CwMemberScalarFieldEnum | Prisma.CwMemberScalarFieldEnum[] +} + +/** + * CwMember create + */ +export type CwMemberCreateArgs = { + /** + * Select specific fields to fetch from the CwMember + */ + select?: Prisma.CwMemberSelect | null + /** + * Omit specific fields from the CwMember + */ + omit?: Prisma.CwMemberOmit | null + /** + * The data needed to create a CwMember. + */ + data: Prisma.XOR +} + +/** + * CwMember createMany + */ +export type CwMemberCreateManyArgs = { + /** + * The data used to create many CwMembers. + */ + data: Prisma.CwMemberCreateManyInput | Prisma.CwMemberCreateManyInput[] + skipDuplicates?: boolean +} + +/** + * CwMember createManyAndReturn + */ +export type CwMemberCreateManyAndReturnArgs = { + /** + * Select specific fields to fetch from the CwMember + */ + select?: Prisma.CwMemberSelectCreateManyAndReturn | null + /** + * Omit specific fields from the CwMember + */ + omit?: Prisma.CwMemberOmit | null + /** + * The data used to create many CwMembers. + */ + data: Prisma.CwMemberCreateManyInput | Prisma.CwMemberCreateManyInput[] + skipDuplicates?: boolean +} + +/** + * CwMember update + */ +export type CwMemberUpdateArgs = { + /** + * Select specific fields to fetch from the CwMember + */ + select?: Prisma.CwMemberSelect | null + /** + * Omit specific fields from the CwMember + */ + omit?: Prisma.CwMemberOmit | null + /** + * The data needed to update a CwMember. + */ + data: Prisma.XOR + /** + * Choose, which CwMember to update. + */ + where: Prisma.CwMemberWhereUniqueInput +} + +/** + * CwMember updateMany + */ +export type CwMemberUpdateManyArgs = { + /** + * The data used to update CwMembers. + */ + data: Prisma.XOR + /** + * Filter which CwMembers to update + */ + where?: Prisma.CwMemberWhereInput + /** + * Limit how many CwMembers to update. + */ + limit?: number +} + +/** + * CwMember updateManyAndReturn + */ +export type CwMemberUpdateManyAndReturnArgs = { + /** + * Select specific fields to fetch from the CwMember + */ + select?: Prisma.CwMemberSelectUpdateManyAndReturn | null + /** + * Omit specific fields from the CwMember + */ + omit?: Prisma.CwMemberOmit | null + /** + * The data used to update CwMembers. + */ + data: Prisma.XOR + /** + * Filter which CwMembers to update + */ + where?: Prisma.CwMemberWhereInput + /** + * Limit how many CwMembers to update. + */ + limit?: number +} + +/** + * CwMember upsert + */ +export type CwMemberUpsertArgs = { + /** + * Select specific fields to fetch from the CwMember + */ + select?: Prisma.CwMemberSelect | null + /** + * Omit specific fields from the CwMember + */ + omit?: Prisma.CwMemberOmit | null + /** + * The filter to search for the CwMember to update in case it exists. + */ + where: Prisma.CwMemberWhereUniqueInput + /** + * In case the CwMember found by the `where` argument doesn't exist, create a new CwMember with this data. + */ + create: Prisma.XOR + /** + * In case the CwMember was found with the provided `where` argument, update it with this data. + */ + update: Prisma.XOR +} + +/** + * CwMember delete + */ +export type CwMemberDeleteArgs = { + /** + * Select specific fields to fetch from the CwMember + */ + select?: Prisma.CwMemberSelect | null + /** + * Omit specific fields from the CwMember + */ + omit?: Prisma.CwMemberOmit | null + /** + * Filter which CwMember to delete. + */ + where: Prisma.CwMemberWhereUniqueInput +} + +/** + * CwMember deleteMany + */ +export type CwMemberDeleteManyArgs = { + /** + * Filter which CwMembers to delete + */ + where?: Prisma.CwMemberWhereInput + /** + * Limit how many CwMembers to delete. + */ + limit?: number +} + +/** + * CwMember without action + */ +export type CwMemberDefaultArgs = { + /** + * Select specific fields to fetch from the CwMember + */ + select?: Prisma.CwMemberSelect | null + /** + * Omit specific fields from the CwMember + */ + omit?: Prisma.CwMemberOmit | null +} diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 45a2911..ffc0076 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -270,3 +270,20 @@ model GeneratedQuotes { createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } + +model CwMember { + id String @id @default(cuid()) + + cwMemberId Int @unique + identifier String @unique + firstName String + lastName String + officeEmail String? + inactiveFlag Boolean @default(false) + + apiKey String? + + cwLastUpdated DateTime? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} diff --git a/src/api/cw/fetchMembers.ts b/src/api/cw/fetchMembers.ts new file mode 100644 index 0000000..67db7cd --- /dev/null +++ b/src/api/cw/fetchMembers.ts @@ -0,0 +1,37 @@ +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 { getMemberCache } from "../../modules/cw-utils/members/memberCache"; + +/* GET /v1/cw/members */ +export default createRoute( + "get", + ["/members"], + async (c) => { + const cache = await getMemberCache(); + + const activeOnly = c.req.query("active") !== "false"; + + const members = cache + .filter((m) => (activeOnly ? !m.inactiveFlag : true)) + .map((m) => ({ + id: m.id, + identifier: m.identifier, + firstName: m.firstName, + lastName: m.lastName, + name: `${m.firstName} ${m.lastName}`.trim(), + officeEmail: m.officeEmail, + inactive: m.inactiveFlag, + })); + + const sorted = members.sort((a, b) => a.name.localeCompare(b.name)); + + const response = apiResponse.successful( + "CW members fetched successfully!", + sorted, + ); + return c.json(response, response.status as ContentfulStatusCode); + }, + authMiddleware(), +); diff --git a/src/api/cw/index.ts b/src/api/cw/index.ts index 924e894..5b0269c 100644 --- a/src/api/cw/index.ts +++ b/src/api/cw/index.ts @@ -1,3 +1,4 @@ import { default as callback } from "./callback"; +import { default as fetchMembers } from "./fetchMembers"; -export { callback }; +export { callback, fetchMembers }; diff --git a/src/api/sales/index.ts b/src/api/sales/index.ts index 9c17080..3e58663 100644 --- a/src/api/sales/index.ts +++ b/src/api/sales/index.ts @@ -1,8 +1,10 @@ import { default as fetchAll } from "./opportunities/fetchAll"; +import { default as createOpportunity } from "./opportunities/create"; import { default as fetchOpportunityTypes } from "./fetchOpportunityTypes"; import { default as count } from "./opportunities/count"; import { default as fetch } from "./opportunities/[id]/fetch"; import { default as refresh } from "./opportunities/[id]/refresh"; +import { default as updateOpportunity } from "./opportunities/[id]/update"; import { default as products } from "./opportunities/[id]/products/fetchAll"; import { default as addProduct } from "./opportunities/[id]/products/add"; import { default as addSpecialOrderProduct } from "./opportunities/[id]/products/addSpecialOrder"; @@ -29,6 +31,7 @@ export { laborOptions, addSpecialOrderProduct, count, + createOpportunity, fetch, fetchAll, fetchOpportunityTypes, @@ -48,4 +51,5 @@ export { downloadQuote, fetchDownloads, refresh, + updateOpportunity, }; diff --git a/src/api/sales/opportunities/[id]/update.ts b/src/api/sales/opportunities/[id]/update.ts new file mode 100644 index 0000000..2ffa570 --- /dev/null +++ b/src/api/sales/opportunities/[id]/update.ts @@ -0,0 +1,93 @@ +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 { z } from "zod"; + +const updateSchema = z + .object({ + name: z.string().min(1).optional(), + notes: z.string().optional(), + rating: z.object({ id: z.number() }).optional(), + type: z.object({ id: z.number() }).optional(), + stage: z.object({ id: z.number() }).optional(), + status: z.object({ id: z.number() }).optional(), + priority: z.object({ id: z.number() }).optional(), + campaign: z.object({ id: z.number() }).optional(), + primarySalesRep: z.object({ id: z.number() }).optional(), + secondarySalesRep: z.object({ id: z.number() }).nullable().optional(), + company: z.object({ id: z.number() }).optional(), + contact: z.object({ id: z.number() }).nullable().optional(), + site: z.object({ id: z.number() }).nullable().optional(), + expectedCloseDate: z + .string() + .optional() + .transform((v) => (v ? new Date(v).toISOString() : v)), + customerPO: z.string().nullable().optional(), + source: z.string().nullable().optional(), + locationId: z.number().optional(), + businessUnitId: z.number().optional(), + }) + .refine((d) => Object.values(d).some((v) => v !== undefined), { + message: "At least one field must be provided", + }); + +/* PATCH /v1/sales/opportunities/:identifier */ +export default createRoute( + "patch", + ["/opportunities/:identifier"], + async (c) => { + const identifier = c.req.param("identifier"); + const body = await c.req.json(); + const data = updateSchema.parse(body); + + const item = await opportunities.fetchRecord(identifier); + + try { + const updated = await item.updateOpportunity(data); + + const response = apiResponse.successful( + "Opportunity updated successfully!", + updated.toJson(), + ); + + return c.json(response, response.status as ContentfulStatusCode); + } catch (err) { + const isAxios = + err != null && typeof err === "object" && "isAxiosError" in err; + + if (isAxios) { + const axiosErr = err as any; + const cwStatus: number = axiosErr.response?.status ?? 502; + const cwData = axiosErr.response?.data; + const cwMessage: string = + cwData?.message ?? "Failed to update the opportunity in ConnectWise"; + const cwErrors: unknown[] | undefined = Array.isArray(cwData?.errors) + ? cwData.errors + : undefined; + + return c.json( + { + status: cwStatus, + message: cwMessage, + error: "ConnectWiseUpdateError", + successful: false, + errors: cwErrors, + meta: { timestamp: Date.now() }, + }, + cwStatus as ContentfulStatusCode, + ); + } + + throw new GenericError({ + status: 500, + name: "OpportunitySaveError", + message: "Failed to save opportunity data", + cause: err instanceof Error ? err.message : String(err), + }); + } + }, + authMiddleware({ permissions: ["sales.opportunity.update"] }), +); diff --git a/src/api/sales/opportunities/create.ts b/src/api/sales/opportunities/create.ts new file mode 100644 index 0000000..c58af19 --- /dev/null +++ b/src/api/sales/opportunities/create.ts @@ -0,0 +1,86 @@ +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 { z } from "zod"; + +const createSchema = z.object({ + name: z.string().min(1), + expectedCloseDate: z + .string() + .min(1) + .transform((v) => new Date(v).toISOString().replace(/\.\d{3}Z$/, "Z")), + notes: z.string().optional(), + rating: z.object({ id: z.number() }).optional(), + type: z.object({ id: z.number() }).optional(), + stage: z.object({ id: z.number() }).optional(), + status: z.object({ id: z.number() }).optional(), + priority: z.object({ id: z.number() }).optional(), + campaign: z.object({ id: z.number() }).optional(), + primarySalesRep: z.object({ id: z.number() }), + secondarySalesRep: z.object({ id: z.number() }).nullable().optional(), + company: z.object({ id: z.number() }), + contact: z.object({ id: z.number() }), + site: z.object({ id: z.number() }).nullable().optional(), + source: z.string().nullable().optional(), + customerPO: z.string().nullable().optional(), + locationId: z.number().optional(), + businessUnitId: z.number().optional(), +}); + +/* POST /v1/sales/opportunities */ +export default createRoute( + "post", + ["/opportunities"], + async (c) => { + const body = await c.req.json(); + const data = createSchema.parse(body); + + try { + const item = await opportunities.createItem(data); + + const response = apiResponse.created( + "Opportunity created successfully!", + item.toJson(), + ); + + return c.json(response, response.status as ContentfulStatusCode); + } catch (err) { + const isAxios = + err != null && typeof err === "object" && "isAxiosError" in err; + + if (isAxios) { + const axiosErr = err as any; + const cwStatus: number = axiosErr.response?.status ?? 502; + const cwData = axiosErr.response?.data; + const cwMessage: string = + cwData?.message ?? "Failed to create the opportunity in ConnectWise"; + const cwErrors: unknown[] | undefined = Array.isArray(cwData?.errors) + ? cwData.errors + : undefined; + + return c.json( + { + status: cwStatus, + message: cwMessage, + error: "ConnectWiseCreateError", + successful: false, + errors: cwErrors, + meta: { timestamp: Date.now() }, + }, + cwStatus as ContentfulStatusCode, + ); + } + + throw new GenericError({ + status: 500, + name: "OpportunityCreateError", + message: "Failed to create opportunity", + cause: err instanceof Error ? err.message : String(err), + }); + } + }, + authMiddleware({ permissions: ["sales.opportunity.create"] }), +); diff --git a/src/controllers/CwMemberController.ts b/src/controllers/CwMemberController.ts new file mode 100644 index 0000000..87d1835 --- /dev/null +++ b/src/controllers/CwMemberController.ts @@ -0,0 +1,86 @@ +import type { CwMember } from "../../generated/prisma/client"; +import type { CWMember } from "../modules/cw-utils/members/fetchAllMembers"; + +/** + * CW Member Controller + * + * Domain model class that encapsulates a ConnectWise Member entity, + * providing access to member data and serialization for the API. + */ +export class CwMemberController { + public readonly id: string; + public readonly cwMemberId: number; + public readonly identifier: string; + public firstName: string; + public lastName: string; + public officeEmail: string | null; + public inactiveFlag: boolean; + public apiKey: string | null; + public cwLastUpdated: Date | null; + public readonly createdAt: Date; + public readonly updatedAt: Date; + + constructor(data: CwMember) { + this.id = data.id; + this.cwMemberId = data.cwMemberId; + this.identifier = data.identifier; + this.firstName = data.firstName; + this.lastName = data.lastName; + this.officeEmail = data.officeEmail; + this.inactiveFlag = data.inactiveFlag; + this.apiKey = data.apiKey; + this.cwLastUpdated = data.cwLastUpdated; + this.createdAt = data.createdAt; + this.updatedAt = data.updatedAt; + } + + /** + * Full Name + * + * Returns the member's full name, falling back to the identifier. + */ + public get fullName(): string { + const name = `${this.firstName} ${this.lastName}`.trim(); + return name || this.identifier; + } + + /** + * Map CW Member → Prisma create/update payload + * + * Static helper used by both the controller and the refresh sync. + */ + public static mapCwToDb(item: CWMember) { + return { + identifier: item.identifier, + firstName: item.firstName ?? "", + lastName: item.lastName ?? "", + officeEmail: item.officeEmail ?? null, + inactiveFlag: item.inactiveFlag ?? false, + cwLastUpdated: item._info?.lastUpdated + ? new Date(item._info.lastUpdated) + : new Date(), + }; + } + + /** + * To JSON + * + * Serializes the member into a safe, API-friendly object. + */ + public toJson(): Record { + return { + id: this.id, + cwMemberId: this.cwMemberId, + identifier: this.identifier, + firstName: this.firstName, + lastName: this.lastName, + fullName: this.fullName, + officeEmail: this.officeEmail, + inactiveFlag: this.inactiveFlag, + apiKey: this.apiKey, + cwLastUpdated: this.cwLastUpdated, + createdAt: this.createdAt, + updatedAt: this.updatedAt, + }; + } +} diff --git a/src/controllers/OpportunityController.ts b/src/controllers/OpportunityController.ts index f74ae65..921de2b 100644 --- a/src/controllers/OpportunityController.ts +++ b/src/controllers/OpportunityController.ts @@ -14,6 +14,7 @@ import { CWForecastItemCreate, CWOpportunity, CWOpportunityNote, + CWOpportunityUpdate, CWProcurementProduct, CWProcurementProductCreate, } from "../modules/cw-utils/opportunities/opportunity.types"; @@ -292,6 +293,30 @@ export class OpportunityController { return new OpportunityController(updated); } + /** + * Update Opportunity + * + * Patches the opportunity in ConnectWise with the provided fields, + * then syncs the updated data back to the local database. + * + * @param data — Partial fields to update on the CW opportunity + * @returns A fresh OpportunityController with the updated data + */ + public async updateOpportunity( + data: CWOpportunityUpdate, + ): Promise { + const cwData = await opportunityCw.update(this.cwOpportunityId, data); + 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 * diff --git a/src/index.ts b/src/index.ts index f2c8f31..4e9f9ea 100644 --- a/src/index.ts +++ b/src/index.ts @@ -17,6 +17,7 @@ import { listenInventoryAdjustments } from "./modules/cw-utils/procurement/liste import { refreshOpportunities } from "./modules/cw-utils/opportunities/refreshOpportunities"; import { refreshOpportunityCache } from "./modules/cache/opportunityCache"; import { refreshCwIdentifiers } from "./modules/cw-utils/members/refreshCwIdentifiers"; +import { refreshCwMembers } from "./modules/cw-utils/members/refreshCwMembers"; import { userDefinedFieldsCw } from "./modules/cw-utils/userDefinedFields"; import { events, setupEventDebugger } from "./modules/globalEvents"; import { signPermissions } from "./modules/permission-utils/signPermissions"; @@ -188,6 +189,17 @@ setInterval( 30 * 60 * 1000, ); +// Refresh CW members DB table every hour +await safeStartup("refreshCwMembers", refreshCwMembers); +setInterval( + () => { + return refreshCwMembers().catch((err) => + console.error(`[interval] refreshCwMembers failed: ${briefErr(err)}`), + ); + }, + 60 * 60 * 1000, +); + await safeStartup("syncSites", () => unifiSites.syncSites()); setInterval(() => { return unifiSites diff --git a/src/managers/cwMembers.ts b/src/managers/cwMembers.ts new file mode 100644 index 0000000..84e6f30 --- /dev/null +++ b/src/managers/cwMembers.ts @@ -0,0 +1,77 @@ +import { prisma } from "../constants"; +import { CwMemberController } from "../controllers/CwMemberController"; +import GenericError from "../Errors/GenericError"; + +/** + * CW Members Manager + * + * Thin persistence layer wrapping Prisma calls for the CwMember model. + * Returns CwMemberController instances as domain objects. + */ +export const cwMembers = { + /** + * Fetch a single CW member by internal ID, CW member ID, or identifier. + */ + fetch: async (idOrIdentifier: string): Promise => { + const isNumeric = /^\d+$/.test(idOrIdentifier); + + const record = await prisma.cwMember.findFirst({ + where: isNumeric + ? { cwMemberId: Number(idOrIdentifier) } + : { + OR: [{ id: idOrIdentifier }, { identifier: idOrIdentifier }], + }, + }); + + if (!record) { + throw new GenericError({ + status: 404, + name: "CwMemberNotFound", + message: `CW Member "${idOrIdentifier}" not found`, + }); + } + + return new CwMemberController(record); + }, + + /** + * Fetch all CW members with optional filtering. + */ + fetchAll: async (opts?: { + includeInactive?: boolean; + }): Promise => { + const where = opts?.includeInactive ? {} : { inactiveFlag: false }; + + const records = await prisma.cwMember.findMany({ + where, + orderBy: { lastName: "asc" }, + }); + + return records.map((r) => new CwMemberController(r)); + }, + + /** + * Count CW members. + */ + count: async (opts?: { includeInactive?: boolean }): Promise => { + const where = opts?.includeInactive ? {} : { inactiveFlag: false }; + return prisma.cwMember.count({ where }); + }, + + /** + * Update the API key for a CW member. + */ + updateApiKey: async ( + idOrIdentifier: string, + apiKey: string | null, + ): Promise => { + const member = await cwMembers.fetch(idOrIdentifier); + + const updated = await prisma.cwMember.update({ + where: { id: member.id }, + data: { apiKey }, + }); + + return new CwMemberController(updated); + }, +}; diff --git a/src/managers/opportunities.ts b/src/managers/opportunities.ts index cc9e8bf..ea8dae7 100644 --- a/src/managers/opportunities.ts +++ b/src/managers/opportunities.ts @@ -6,6 +6,7 @@ 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 { CWOpportunityCreate } from "../modules/cw-utils/opportunities/opportunity.types"; import { computeCacheTTL } from "../modules/algorithms/computeCacheTTL"; import { getCachedActivities, @@ -127,6 +128,45 @@ async function buildActivities( } export const opportunities = { + /** + * Create Opportunity + * + * Creates a new opportunity in ConnectWise, then stores the resulting + * record in the local database and returns an OpportunityController. + * + * @param data — Fields required by the ConnectWise `POST /sales/opportunities` endpoint + * @returns {Promise} + */ + async createItem(data: CWOpportunityCreate): Promise { + const cwData = await opportunityCw.create(data); + const mapped = OpportunityController.mapCwToDb(cwData); + + // Resolve optional local company relation + const companyId = cwData.company?.id + ? (( + await prisma.company.findFirst({ + where: { cw_CompanyId: cwData.company.id }, + select: { id: true }, + }) + )?.id ?? null) + : null; + + const record = await prisma.opportunity.create({ + data: { + cwOpportunityId: cwData.id, + ...mapped, + companyId, + }, + include: { company: true }, + }); + + return new OpportunityController(record, { + company: record.company + ? new CompanyController(record.company) + : undefined, + }); + }, + /** * Fetch Record (lightweight) * diff --git a/src/modules/cw-utils/cwIntegratorInterceptor.ts b/src/modules/cw-utils/cwIntegratorInterceptor.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/modules/cw-utils/members/fetchAllMembers.ts b/src/modules/cw-utils/members/fetchAllMembers.ts index 585051f..dff5955 100644 --- a/src/modules/cw-utils/members/fetchAllMembers.ts +++ b/src/modules/cw-utils/members/fetchAllMembers.ts @@ -17,20 +17,26 @@ export interface CWMember { * Fetches every member from ConnectWise using pagination and returns them * in a Collection keyed by their identifier (e.g. "jroberts"). * + * @param opts.conditions - Optional CW conditions string to filter members * @returns {Promise>} Collection of CW members keyed by identifier */ -export const fetchAllCwMembers = async (): Promise< - Collection -> => { +export const fetchAllCwMembers = async (opts?: { + conditions?: string; +}): Promise> => { const members = new Collection(); const pageSize = 1000; + const conditionsParam = opts?.conditions + ? `&conditions=${encodeURIComponent(opts.conditions)}` + : ""; - const { data: countData } = await connectWiseApi.get("/system/members/count"); + const { data: countData } = await connectWiseApi.get( + `/system/members/count${conditionsParam ? `?${conditionsParam.slice(1)}` : ""}`, + ); const totalPages = Math.ceil(countData.count / pageSize); for (let page = 0; page < totalPages; page++) { const { data } = await connectWiseApi.get( - `/system/members?page=${page + 1}&pageSize=${pageSize}`, + `/system/members?page=${page + 1}&pageSize=${pageSize}${conditionsParam}`, ); for (const member of data) { diff --git a/src/modules/cw-utils/members/refreshCwMembers.ts b/src/modules/cw-utils/members/refreshCwMembers.ts new file mode 100644 index 0000000..4eb0fc2 --- /dev/null +++ b/src/modules/cw-utils/members/refreshCwMembers.ts @@ -0,0 +1,106 @@ +import { prisma } from "../../../constants"; +import { events } from "../../globalEvents"; +import { fetchAllCwMembers, type CWMember } from "./fetchAllMembers"; +import { setMemberCache } from "./memberCache"; +import { CwMemberController } from "../../../controllers/CwMemberController"; + +/** + * Is Regular User + * + * Returns true if the CW member looks like a real person rather than + * a service account (e.g. "labtech", "Admin"). A regular user must + * have a last name and an email address. + */ +const isRegularUser = (member: CWMember): boolean => + !member.inactiveFlag && + Boolean(member.lastName?.trim()) && + Boolean(member.officeEmail?.trim()); + +/** + * Refresh CW Members + * + * Syncs local CwMember records with ConnectWise using a stale-check + * pattern: + * 1. Fetch all members from CW + * 2. Filter to regular users (active, non-service accounts) + * 3. Compare against local cwLastUpdated timestamps + * 4. Upsert stale/new records + * 5. Also refreshes the in-memory member cache + */ +export const refreshCwMembers = async () => { + events.emit("cw:members:db:refresh:check"); + + // 1. Fetch all members from CW + const allCwMembers = await fetchAllCwMembers(); + + // Also refresh the in-memory cache with ALL members (used for name resolution) + await setMemberCache(allCwMembers); + + // 2. Filter to regular users only (active, has last name + email) + const cwMembers = allCwMembers.filter(isRegularUser); + + // 2. Fetch all DB records with their identifier and cwLastUpdated + const dbItems = await prisma.cwMember.findMany({ + select: { cwMemberId: true, cwLastUpdated: true }, + }); + const dbMap = new Map( + dbItems.map((item) => [item.cwMemberId, item.cwLastUpdated]), + ); + + // 3. Determine stale / new IDs + const staleIds: number[] = []; + + for (const [, member] of cwMembers) { + const cwLastUpdated = member._info?.lastUpdated + ? new Date(member._info.lastUpdated) + : null; + const dbLastUpdated = dbMap.get(member.id) ?? null; + + if (!dbLastUpdated || (cwLastUpdated && cwLastUpdated > dbLastUpdated)) { + staleIds.push(member.id); + } + } + + if (staleIds.length === 0) { + events.emit("cw:members:db:refresh:skipped", { + totalCw: cwMembers.size, + totalDb: dbItems.length, + staleCount: 0, + }); + return; + } + + events.emit("cw:members:db:refresh:started", { + totalCw: cwMembers.size, + totalDb: dbItems.length, + staleCount: staleIds.length, + }); + + // 4. Upsert stale/new items + const staleIdSet = new Set(staleIds); + const updatedCount = ( + await Promise.all( + [...cwMembers.values()] + .filter((m) => staleIdSet.has(m.id)) + .map(async (member) => { + const mapped = CwMemberController.mapCwToDb(member); + + return prisma.cwMember.upsert({ + where: { cwMemberId: member.id }, + create: { + cwMemberId: member.id, + ...mapped, + }, + update: mapped, + }); + }), + ) + ).filter(Boolean).length; + + events.emit("cw:members:db:refresh:completed", { + totalCw: cwMembers.size, + totalDb: dbItems.length, + staleCount: staleIds.length, + itemsUpdated: updatedCount, + }); +}; diff --git a/src/modules/cw-utils/opportunities/opportunities.ts b/src/modules/cw-utils/opportunities/opportunities.ts index 94d509a..c6e8b2c 100644 --- a/src/modules/cw-utils/opportunities/opportunities.ts +++ b/src/modules/cw-utils/opportunities/opportunities.ts @@ -2,6 +2,7 @@ import { Collection } from "@discordjs/collection"; import { connectWiseApi } from "../../../constants"; import { CWOpportunity, + CWOpportunityCreate, CWOpportunitySummary, CWForecast, CWForecastItem, @@ -12,6 +13,7 @@ import { CWOpportunityNoteCreate, CWOpportunityNoteUpdate, CWOpportunityContact, + CWOpportunityUpdate, } from "./opportunity.types"; export const opportunityCw = { @@ -100,6 +102,45 @@ export const opportunityCw = { return response.data; }, + /** + * Create Opportunity + * + * Creates a new opportunity in ConnectWise via POST. + * Strips null/undefined values from the payload — CW rejects + * null reference objects on create; omitting them lets CW apply + * its own defaults. + */ + create: async (data: CWOpportunityCreate): Promise => { + const cleaned = Object.fromEntries( + Object.entries(data).filter(([, v]) => v != null), + ); + const response = await connectWiseApi.post("/sales/opportunities", cleaned); + return response.data; + }, + + /** + * Update Opportunity + * + * Applies a JSON Patch update to an opportunity record in ConnectWise. + * Each key in `data` produces a replace operation. + */ + update: async ( + opportunityId: number, + data: CWOpportunityUpdate, + ): Promise => { + const operations = Object.entries(data).map(([key, value]) => ({ + op: "replace" as const, + path: key, + value, + })); + + const response = await connectWiseApi.patch( + `/sales/opportunities/${opportunityId}`, + operations, + ); + return response.data; + }, + /** * Fetch Opportunities by Company * diff --git a/src/modules/cw-utils/opportunities/opportunity.types.ts b/src/modules/cw-utils/opportunities/opportunity.types.ts index b17f9f8..d6b2091 100644 --- a/src/modules/cw-utils/opportunities/opportunity.types.ts +++ b/src/modules/cw-utils/opportunities/opportunity.types.ts @@ -263,6 +263,48 @@ export interface CWProcurementProduct { _info?: Record; } +export interface CWOpportunityUpdate { + name?: string; + notes?: string; + rating?: { id: number }; + type?: { id: number }; + stage?: { id: number }; + status?: { id: number }; + priority?: { id: number }; + campaign?: { id: number }; + primarySalesRep?: { id: number }; + secondarySalesRep?: { id: number } | null; + company?: { id: number }; + contact?: { id: number } | null; + site?: { id: number } | null; + expectedCloseDate?: string; + customerPO?: string | null; + source?: string | null; + locationId?: number; + businessUnitId?: number; +} + +export interface CWOpportunityCreate { + name: string; + expectedCloseDate: string; + primarySalesRep: { id: number }; + company: { id: number }; + contact: { id: number }; + type?: { id: number }; + stage?: { id: number }; + status?: { id: number }; + priority?: { id: number }; + campaign?: { id: number }; + secondarySalesRep?: { id: number } | null; + site?: { id: number } | null; + notes?: string; + rating?: { id: number }; + source?: string | null; + customerPO?: string | null; + locationId?: number; + businessUnitId?: number; +} + export interface CWOpportunitySummary { id: number; _info?: Record; diff --git a/src/modules/globalEvents.ts b/src/modules/globalEvents.ts index 6fc41c9..0bb7a54 100644 --- a/src/modules/globalEvents.ts +++ b/src/modules/globalEvents.ts @@ -205,6 +205,25 @@ interface EventTypes { totalUsers: number; usersUpdated: number; }) => void; + + // ConnectWise Members DB Sync Events + "cw:members:db:refresh:check": () => void; + "cw:members:db:refresh:started": (data: { + totalCw: number; + totalDb: number; + staleCount: number; + }) => void; + "cw:members:db:refresh:completed": (data: { + totalCw: number; + totalDb: number; + staleCount: number; + itemsUpdated: number; + }) => void; + "cw:members:db:refresh:skipped": (data: { + totalCw: number; + totalDb: number; + staleCount: number; + }) => void; } export const events = new Eventra(); diff --git a/src/types/PermissionNodes.ts b/src/types/PermissionNodes.ts index ac5f55a..59623a9 100644 --- a/src/types/PermissionNodes.ts +++ b/src/types/PermissionNodes.ts @@ -423,6 +423,18 @@ export const PERMISSION_NODES = { usedIn: ["src/api/sales/opportunities/[id]/refresh.ts"], dependencies: ["sales.opportunity.fetch"], }, + { + node: "sales.opportunity.update", + description: + "Update an opportunity's fields (rating, sales rep, company, contact, site, description, etc.) in ConnectWise", + usedIn: ["src/api/sales/opportunities/[id]/update.ts"], + dependencies: ["sales.opportunity.fetch"], + }, + { + node: "sales.opportunity.create", + description: "Create a new opportunity in ConnectWise", + usedIn: ["src/api/sales/opportunities/create.ts"], + }, { node: "sales.opportunity.note.create", description: "Create a new note on an opportunity", @@ -531,6 +543,20 @@ export const PERMISSION_NODES = { usedIn: ["src/api/sales/opportunities/[id]/quotes/fetchDownloads.ts"], dependencies: ["sales.opportunity.fetch"], }, + { + node: "sales.opportunity.view_margin", + description: + "View margin and markup data on opportunity products. Controls visibility of margin %, markup %, and related progress bars in the UI.", + usedIn: [], + dependencies: ["sales.opportunity.fetch"], + }, + { + node: "sales.opportunity.view_cost", + description: + "View cost data on opportunity products. Controls visibility of unit cost, total cost, and recurring cost in the UI.", + usedIn: [], + dependencies: ["sales.opportunity.fetch"], + }, ], },