all the haul

This commit is contained in:
2026-04-07 23:56:31 +00:00
parent 87cce83030
commit 24f303355b
244 changed files with 33743 additions and 11249 deletions
@@ -26,7 +26,7 @@ services:
image: adminer image: adminer
restart: unless-stopped restart: unless-stopped
ports: ports:
- 8080:8080 - 8081:8080
depends_on: depends_on:
- pgsql - pgsql
redisinsight: redisinsight:
-2
View File
@@ -1,2 +0,0 @@
node_modules
daemon
@@ -1,4 +1,4 @@
name: Build and Publish name: API - Build and Publish
on: on:
release: release:
@@ -8,6 +8,9 @@ jobs:
test: test:
name: Test name: Test
runs-on: ubuntu-latest runs-on: ubuntu-latest
defaults:
run:
working-directory: api
steps: steps:
- name: Checkout source code - name: Checkout source code
uses: actions/checkout@v4 uses: actions/checkout@v4
@@ -47,6 +50,7 @@ jobs:
- name: Build and push the Docker image - name: Build and push the Docker image
uses: docker/build-push-action@v6 uses: docker/build-push-action@v6
with: with:
context: ./api
push: true push: true
target: runtime target: runtime
tags: | tags: |
@@ -56,6 +60,7 @@ jobs:
- name: Build and push the migration image - name: Build and push the migration image
uses: docker/build-push-action@v6 uses: docker/build-push-action@v6
with: with:
context: ./api
push: true push: true
target: migration target: migration
tags: | tags: |
@@ -66,6 +71,9 @@ jobs:
name: Run Migrations name: Run Migrations
needs: [build] needs: [build]
runs-on: ubuntu-latest runs-on: ubuntu-latest
defaults:
run:
working-directory: api
steps: steps:
- name: Set the Kubernetes context - name: Set the Kubernetes context
uses: azure/k8s-set-context@v2 uses: azure/k8s-set-context@v2
@@ -108,8 +116,8 @@ jobs:
with: with:
lintType: dryrun lintType: dryrun
manifests: | manifests: |
kubernetes/deployment.yaml api/kubernetes/deployment.yaml
kubernetes/ingress.yaml api/kubernetes/ingress.yaml
namespace: optima namespace: optima
- name: Deploy to the Kubernetes cluster - name: Deploy to the Kubernetes cluster
@@ -119,7 +127,7 @@ jobs:
force: true force: true
skip-tls-verify: true skip-tls-verify: true
manifests: | manifests: |
kubernetes/deployment.yaml api/kubernetes/deployment.yaml
kubernetes/ingress.yaml api/kubernetes/ingress.yaml
images: | images: |
ghcr.io/project-optima/ttscm-api:${{ github.event.release.tag_name }} ghcr.io/project-optima/ttscm-api:${{ github.event.release.tag_name }}
@@ -1,4 +1,4 @@
name: Tests name: API - Tests
on: on:
push: push:
@@ -8,6 +8,9 @@ jobs:
test: test:
name: Test name: Test
runs-on: ubuntu-latest runs-on: ubuntu-latest
defaults:
run:
working-directory: api
steps: steps:
- name: Checkout source code - name: Checkout source code
uses: actions/checkout@v4 uses: actions/checkout@v4
@@ -1,4 +1,4 @@
name: Build and Publish name: UI - Build and Publish
on: on:
release: release:
@@ -28,7 +28,7 @@ jobs:
- name: Build and push the Docker image - name: Build and push the Docker image
uses: docker/build-push-action@v6 uses: docker/build-push-action@v6
with: with:
context: . context: ./ui
push: true push: true
build-args: | build-args: |
PUBLIC_API_URL=https://opt-api.osdci.net PUBLIC_API_URL=https://opt-api.osdci.net
@@ -41,6 +41,9 @@ jobs:
runs-on: macos-latest runs-on: macos-latest
permissions: permissions:
contents: write contents: write
defaults:
run:
working-directory: ui
steps: steps:
- name: Checkout source code - name: Checkout source code
uses: actions/checkout@v4 uses: actions/checkout@v4
@@ -68,14 +71,17 @@ jobs:
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v2
with: with:
files: | files: |
out/make/**/*.dmg ui/out/make/**/*.dmg
out/make/**/*.zip ui/out/make/**/*.zip
build-desktop-windows: build-desktop-windows:
name: Build Desktop (Windows) name: Build Desktop (Windows)
runs-on: windows-latest runs-on: windows-latest
permissions: permissions:
contents: write contents: write
defaults:
run:
working-directory: ui
steps: steps:
- name: Checkout source code - name: Checkout source code
uses: actions/checkout@v4 uses: actions/checkout@v4
@@ -97,41 +103,4 @@ jobs:
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v2
with: with:
files: | files: |
out/make/**/*.exe ui/out/make/**/*.exe
out/make/**/*.nupkg
out/make/**/*.msi
deploy:
name: Deploy
needs: [build-server]
runs-on: ubuntu-latest
steps:
- name: Set the Kubernetes context
uses: azure/k8s-set-context@v2
with:
method: kubeconfig
kubeconfig: ${{ secrets.KUBECONFIG }}
- name: Checkout source code
uses: actions/checkout@v4
- name: Lint Kubernetes manifests
uses: azure/k8s-lint@v3
with:
lintType: dryrun
manifests: |
kubernetes/deployment.yaml
kubernetes/ingress.yaml
namespace: optima
- name: Deploy to the Kubernetes cluster
uses: azure/k8s-deploy@v5
with:
namespace: optima
force: true
skip-tls-verify: true
manifests: |
kubernetes/deployment.yaml
kubernetes/ingress.yaml
images: |
ghcr.io/project-optima/ttscm-ui:${{ github.event.release.tag_name }}
+53 -123
View File
@@ -1,139 +1,69 @@
# Logs # macOS metadata
logs __MACOSX/
*.log .DS_Store
npm-debug.log* .AppleDouble
yarn-debug.log* .LSOverride
yarn-error.log*
lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html) # Windows
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json Thumbs.db
ehthumbs.db
Desktop.ini
# Runtime data # Dependencies
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/ node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/) # Environment variables
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env .env
.env.* .env.*
!.env.example !.env.example
!.env.test
# parcel-bundler cache (https://parceljs.org/) # Logs
.cache *.log
.parcel-cache *.jsonl
logs/
# Next.js build output # TypeScript
.next *.tsbuildinfo
out
# Nuxt.js build / generate output # Bun
.nuxt .bun/
dist
# Gatsby files # Build outputs
.cache/ dist/
# Comment in the public line in if your project uses Gatsby and not Next.js build/
# https://nextjs.org/blog/next-9-1#public-directory-support out/
# public .output/
# vuepress build output # SvelteKit
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
.cache
# Sveltekit cache directory
.svelte-kit/ .svelte-kit/
# vitepress build output # Vite
**/.vitepress/dist .vite/
# vitepress cache directory
**/.vitepress/cache
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# Firebase cache directory
.firebase/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v3
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions
# Vite logs files
vite.config.js.timestamp-* vite.config.js.timestamp-*
vite.config.ts.timestamp-* vite.config.ts.timestamp-*
# Test coverage
coverage/
.nyc_output/
# Local Docker data volumes
.docker/postgres/
.docker/redis/dump.rdb
# Generated Prisma client
api/generated/
dalpuri/generated/
# Secret key files
api/.permissions.key
api/.refreshToken.key
api/.secureKeys.key
api/.accessToken.key
api/.secureValues.key
api/production-keys/
api/public-keys/
# Temporary / scratch files
git-backup.zip
configurations-top-150-and-stats.json
-1
View File
@@ -1 +0,0 @@
_
-2
View File
@@ -1,2 +0,0 @@
node_modules/
daemon
-8
View File
@@ -1,8 +0,0 @@
module.exports = {
trailingComma: "es5",
tabWidth: 2,
semi: true,
singleQuote: false,
arrowParens: "always",
useTabs: false,
};
-9
View File
@@ -1,9 +0,0 @@
nodeLinker: node-modules
plugins:
- path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs
spec: "@yarnpkg/plugin-interactive-tools"
- path: .yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs
spec: "@yarnpkg/plugin-workspace-tools"
yarnPath: .yarn/releases/yarn-3.2.0.cjs
+165
View File
@@ -17,6 +17,16 @@ import * as Prisma from './internal/prismaNamespaceBrowser.ts'
export { Prisma } export { Prisma }
export * as $Enums from './enums.ts' export * as $Enums from './enums.ts'
export * from './enums.ts'; export * from './enums.ts';
/**
* Model SyncJobRun
*
*/
export type SyncJobRun = Prisma.SyncJobRunModel
/**
* Model SyncStepLog
*
*/
export type SyncStepLog = Prisma.SyncStepLogModel
/** /**
* Model Session * Model Session
* *
@@ -32,6 +42,16 @@ export type User = Prisma.UserModel
* *
*/ */
export type Role = Prisma.RoleModel export type Role = Prisma.RoleModel
/**
* Model CorporateLocation
*
*/
export type CorporateLocation = Prisma.CorporateLocationModel
/**
* Model InternalDepartment
*
*/
export type InternalDepartment = Prisma.InternalDepartmentModel
/** /**
* Model UnifiSite * Model UnifiSite
* *
@@ -42,16 +62,156 @@ export type UnifiSite = Prisma.UnifiSiteModel
* *
*/ */
export type Company = Prisma.CompanyModel export type Company = Prisma.CompanyModel
/**
* Model CompanyAddress
*
*/
export type CompanyAddress = Prisma.CompanyAddressModel
/**
* Model Contact
*
*/
export type Contact = Prisma.ContactModel
/**
* Model CatalogItemType
*
*/
export type CatalogItemType = Prisma.CatalogItemTypeModel
/**
* Model CatalogCategory
*
*/
export type CatalogCategory = Prisma.CatalogCategoryModel
/**
* Model CatalogSubcategory
*
*/
export type CatalogSubcategory = Prisma.CatalogSubcategoryModel
/**
* Model CatalogManufacturer
*
*/
export type CatalogManufacturer = Prisma.CatalogManufacturerModel
/**
* Model WarehouseBin
*
*/
export type WarehouseBin = Prisma.WarehouseBinModel
/**
* Model ProductInventory
*
*/
export type ProductInventory = Prisma.ProductInventoryModel
/**
* Model Warehouse
*
*/
export type Warehouse = Prisma.WarehouseModel
/**
* Model MinimumStockByWarehouse
*
*/
export type MinimumStockByWarehouse = Prisma.MinimumStockByWarehouseModel
/** /**
* Model CatalogItem * Model CatalogItem
* *
*/ */
export type CatalogItem = Prisma.CatalogItemModel export type CatalogItem = Prisma.CatalogItemModel
/**
* Model ProductData
*
*/
export type ProductData = Prisma.ProductDataModel
/**
* Model ServiceTicket
*
*/
export type ServiceTicket = Prisma.ServiceTicketModel
/**
* Model ServiceTicketNote
*
*/
export type ServiceTicketNote = Prisma.ServiceTicketNoteModel
/**
* Model ServiceTicketType
*
*/
export type ServiceTicketType = Prisma.ServiceTicketTypeModel
/**
* Model ServiceTicketBoard
*
*/
export type ServiceTicketBoard = Prisma.ServiceTicketBoardModel
/**
* Model ServiceTicketLocation
*
*/
export type ServiceTicketLocation = Prisma.ServiceTicketLocationModel
/**
* Model ServiceTicketSource
*
*/
export type ServiceTicketSource = Prisma.ServiceTicketSourceModel
/**
* Model ServiceTicketImpact
*
*/
export type ServiceTicketImpact = Prisma.ServiceTicketImpactModel
/**
* Model ServiceTicketPriority
*
*/
export type ServiceTicketPriority = Prisma.ServiceTicketPriorityModel
/**
* Model ServiceTicketSeverity
*
*/
export type ServiceTicketSeverity = Prisma.ServiceTicketSeverityModel
/**
* Model ServiceTicketFinalData
*
*/
export type ServiceTicketFinalData = Prisma.ServiceTicketFinalDataModel
/**
* Model OpportunityStage
*
*/
export type OpportunityStage = Prisma.OpportunityStageModel
/**
* Model OpportunityType
*
*/
export type OpportunityType = Prisma.OpportunityTypeModel
/**
* Model OpportunityStatus
*
*/
export type OpportunityStatus = Prisma.OpportunityStatusModel
/** /**
* Model Opportunity * Model Opportunity
* *
*/ */
export type Opportunity = Prisma.OpportunityModel export type Opportunity = Prisma.OpportunityModel
/**
* Model ScheduleStatus
*
*/
export type ScheduleStatus = Prisma.ScheduleStatusModel
/**
* Model ScheduleType
*
*/
export type ScheduleType = Prisma.ScheduleTypeModel
/**
* Model ScheduleSpan
*
*/
export type ScheduleSpan = Prisma.ScheduleSpanModel
/**
* Model Schedule
*
*/
export type Schedule = Prisma.ScheduleModel
/** /**
* Model CredentialType * Model CredentialType
* *
@@ -72,6 +232,11 @@ export type Credential = Prisma.CredentialModel
* *
*/ */
export type GeneratedQuotes = Prisma.GeneratedQuotesModel export type GeneratedQuotes = Prisma.GeneratedQuotesModel
/**
* Model TaxCode
*
*/
export type TaxCode = Prisma.TaxCodeModel
/** /**
* Model CwMember * Model CwMember
* *
+170 -3
View File
@@ -28,9 +28,11 @@ export * from "./enums.ts"
* Type-safe database client for TypeScript * Type-safe database client for TypeScript
* @example * @example
* ``` * ```
* const prisma = new PrismaClient() * const prisma = new PrismaClient({
* // Fetch zero or more Sessions * adapter: new PrismaPg({ connectionString: process.env.DATABASE_URL })
* const sessions = await prisma.session.findMany() * })
* // Fetch zero or more SyncJobRuns
* const syncJobRuns = await prisma.syncJobRun.findMany()
* ``` * ```
* *
* Read more in our [docs](https://pris.ly/d/client). * Read more in our [docs](https://pris.ly/d/client).
@@ -39,6 +41,16 @@ export const PrismaClient = $Class.getPrismaClientClass()
export type PrismaClient<LogOpts extends Prisma.LogLevel = never, OmitOpts extends Prisma.PrismaClientOptions["omit"] = Prisma.PrismaClientOptions["omit"], ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = $Class.PrismaClient<LogOpts, OmitOpts, ExtArgs> export type PrismaClient<LogOpts extends Prisma.LogLevel = never, OmitOpts extends Prisma.PrismaClientOptions["omit"] = Prisma.PrismaClientOptions["omit"], ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = $Class.PrismaClient<LogOpts, OmitOpts, ExtArgs>
export { Prisma } export { Prisma }
/**
* Model SyncJobRun
*
*/
export type SyncJobRun = Prisma.SyncJobRunModel
/**
* Model SyncStepLog
*
*/
export type SyncStepLog = Prisma.SyncStepLogModel
/** /**
* Model Session * Model Session
* *
@@ -54,6 +66,16 @@ export type User = Prisma.UserModel
* *
*/ */
export type Role = Prisma.RoleModel export type Role = Prisma.RoleModel
/**
* Model CorporateLocation
*
*/
export type CorporateLocation = Prisma.CorporateLocationModel
/**
* Model InternalDepartment
*
*/
export type InternalDepartment = Prisma.InternalDepartmentModel
/** /**
* Model UnifiSite * Model UnifiSite
* *
@@ -64,16 +86,156 @@ export type UnifiSite = Prisma.UnifiSiteModel
* *
*/ */
export type Company = Prisma.CompanyModel export type Company = Prisma.CompanyModel
/**
* Model CompanyAddress
*
*/
export type CompanyAddress = Prisma.CompanyAddressModel
/**
* Model Contact
*
*/
export type Contact = Prisma.ContactModel
/**
* Model CatalogItemType
*
*/
export type CatalogItemType = Prisma.CatalogItemTypeModel
/**
* Model CatalogCategory
*
*/
export type CatalogCategory = Prisma.CatalogCategoryModel
/**
* Model CatalogSubcategory
*
*/
export type CatalogSubcategory = Prisma.CatalogSubcategoryModel
/**
* Model CatalogManufacturer
*
*/
export type CatalogManufacturer = Prisma.CatalogManufacturerModel
/**
* Model WarehouseBin
*
*/
export type WarehouseBin = Prisma.WarehouseBinModel
/**
* Model ProductInventory
*
*/
export type ProductInventory = Prisma.ProductInventoryModel
/**
* Model Warehouse
*
*/
export type Warehouse = Prisma.WarehouseModel
/**
* Model MinimumStockByWarehouse
*
*/
export type MinimumStockByWarehouse = Prisma.MinimumStockByWarehouseModel
/** /**
* Model CatalogItem * Model CatalogItem
* *
*/ */
export type CatalogItem = Prisma.CatalogItemModel export type CatalogItem = Prisma.CatalogItemModel
/**
* Model ProductData
*
*/
export type ProductData = Prisma.ProductDataModel
/**
* Model ServiceTicket
*
*/
export type ServiceTicket = Prisma.ServiceTicketModel
/**
* Model ServiceTicketNote
*
*/
export type ServiceTicketNote = Prisma.ServiceTicketNoteModel
/**
* Model ServiceTicketType
*
*/
export type ServiceTicketType = Prisma.ServiceTicketTypeModel
/**
* Model ServiceTicketBoard
*
*/
export type ServiceTicketBoard = Prisma.ServiceTicketBoardModel
/**
* Model ServiceTicketLocation
*
*/
export type ServiceTicketLocation = Prisma.ServiceTicketLocationModel
/**
* Model ServiceTicketSource
*
*/
export type ServiceTicketSource = Prisma.ServiceTicketSourceModel
/**
* Model ServiceTicketImpact
*
*/
export type ServiceTicketImpact = Prisma.ServiceTicketImpactModel
/**
* Model ServiceTicketPriority
*
*/
export type ServiceTicketPriority = Prisma.ServiceTicketPriorityModel
/**
* Model ServiceTicketSeverity
*
*/
export type ServiceTicketSeverity = Prisma.ServiceTicketSeverityModel
/**
* Model ServiceTicketFinalData
*
*/
export type ServiceTicketFinalData = Prisma.ServiceTicketFinalDataModel
/**
* Model OpportunityStage
*
*/
export type OpportunityStage = Prisma.OpportunityStageModel
/**
* Model OpportunityType
*
*/
export type OpportunityType = Prisma.OpportunityTypeModel
/**
* Model OpportunityStatus
*
*/
export type OpportunityStatus = Prisma.OpportunityStatusModel
/** /**
* Model Opportunity * Model Opportunity
* *
*/ */
export type Opportunity = Prisma.OpportunityModel export type Opportunity = Prisma.OpportunityModel
/**
* Model ScheduleStatus
*
*/
export type ScheduleStatus = Prisma.ScheduleStatusModel
/**
* Model ScheduleType
*
*/
export type ScheduleType = Prisma.ScheduleTypeModel
/**
* Model ScheduleSpan
*
*/
export type ScheduleSpan = Prisma.ScheduleSpanModel
/**
* Model Schedule
*
*/
export type Schedule = Prisma.ScheduleModel
/** /**
* Model CredentialType * Model CredentialType
* *
@@ -94,6 +256,11 @@ export type Credential = Prisma.CredentialModel
* *
*/ */
export type GeneratedQuotes = Prisma.GeneratedQuotesModel export type GeneratedQuotes = Prisma.GeneratedQuotesModel
/**
* Model TaxCode
*
*/
export type TaxCode = Prisma.TaxCodeModel
/** /**
* Model CwMember * Model CwMember
* *
+525 -176
View File
@@ -29,20 +29,18 @@ export type StringFilter<$PrismaModel = never> = {
not?: Prisma.NestedStringFilter<$PrismaModel> | string not?: Prisma.NestedStringFilter<$PrismaModel> | string
} }
export type DateTimeFilter<$PrismaModel = never> = { export type EnumSyncJobTypeFilter<$PrismaModel = never> = {
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> equals?: $Enums.SyncJobType | Prisma.EnumSyncJobTypeFieldRefInput<$PrismaModel>
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> in?: $Enums.SyncJobType[] | Prisma.ListEnumSyncJobTypeFieldRefInput<$PrismaModel>
notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> notIn?: $Enums.SyncJobType[] | Prisma.ListEnumSyncJobTypeFieldRefInput<$PrismaModel>
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> not?: Prisma.NestedEnumSyncJobTypeFilter<$PrismaModel> | $Enums.SyncJobType
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
not?: Prisma.NestedDateTimeFilter<$PrismaModel> | Date | string
} }
export type BoolFilter<$PrismaModel = never> = { export type EnumSyncJobStatusFilter<$PrismaModel = never> = {
equals?: boolean | Prisma.BooleanFieldRefInput<$PrismaModel> equals?: $Enums.SyncJobStatus | Prisma.EnumSyncJobStatusFieldRefInput<$PrismaModel>
not?: Prisma.NestedBoolFilter<$PrismaModel> | boolean in?: $Enums.SyncJobStatus[] | Prisma.ListEnumSyncJobStatusFieldRefInput<$PrismaModel>
notIn?: $Enums.SyncJobStatus[] | Prisma.ListEnumSyncJobStatusFieldRefInput<$PrismaModel>
not?: Prisma.NestedEnumSyncJobStatusFilter<$PrismaModel> | $Enums.SyncJobStatus
} }
export type DateTimeNullableFilter<$PrismaModel = never> = { export type DateTimeNullableFilter<$PrismaModel = never> = {
@@ -56,6 +54,32 @@ export type DateTimeNullableFilter<$PrismaModel = never> = {
not?: Prisma.NestedDateTimeNullableFilter<$PrismaModel> | Date | string | null not?: Prisma.NestedDateTimeNullableFilter<$PrismaModel> | Date | string | null
} }
export type StringNullableFilter<$PrismaModel = never> = {
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
mode?: Prisma.QueryMode
not?: Prisma.NestedStringNullableFilter<$PrismaModel> | string | null
}
export type DateTimeFilter<$PrismaModel = never> = {
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel>
notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel>
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
not?: Prisma.NestedDateTimeFilter<$PrismaModel> | Date | string
}
export type SortOrderInput = { export type SortOrderInput = {
sort: Prisma.SortOrder sort: Prisma.SortOrder
nulls?: Prisma.NullsOrder nulls?: Prisma.NullsOrder
@@ -79,26 +103,24 @@ export type StringWithAggregatesFilter<$PrismaModel = never> = {
_max?: Prisma.NestedStringFilter<$PrismaModel> _max?: Prisma.NestedStringFilter<$PrismaModel>
} }
export type DateTimeWithAggregatesFilter<$PrismaModel = never> = { export type EnumSyncJobTypeWithAggregatesFilter<$PrismaModel = never> = {
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> equals?: $Enums.SyncJobType | Prisma.EnumSyncJobTypeFieldRefInput<$PrismaModel>
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> in?: $Enums.SyncJobType[] | Prisma.ListEnumSyncJobTypeFieldRefInput<$PrismaModel>
notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> notIn?: $Enums.SyncJobType[] | Prisma.ListEnumSyncJobTypeFieldRefInput<$PrismaModel>
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> not?: Prisma.NestedEnumSyncJobTypeWithAggregatesFilter<$PrismaModel> | $Enums.SyncJobType
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
not?: Prisma.NestedDateTimeWithAggregatesFilter<$PrismaModel> | Date | string
_count?: Prisma.NestedIntFilter<$PrismaModel> _count?: Prisma.NestedIntFilter<$PrismaModel>
_min?: Prisma.NestedDateTimeFilter<$PrismaModel> _min?: Prisma.NestedEnumSyncJobTypeFilter<$PrismaModel>
_max?: Prisma.NestedDateTimeFilter<$PrismaModel> _max?: Prisma.NestedEnumSyncJobTypeFilter<$PrismaModel>
} }
export type BoolWithAggregatesFilter<$PrismaModel = never> = { export type EnumSyncJobStatusWithAggregatesFilter<$PrismaModel = never> = {
equals?: boolean | Prisma.BooleanFieldRefInput<$PrismaModel> equals?: $Enums.SyncJobStatus | Prisma.EnumSyncJobStatusFieldRefInput<$PrismaModel>
not?: Prisma.NestedBoolWithAggregatesFilter<$PrismaModel> | boolean in?: $Enums.SyncJobStatus[] | Prisma.ListEnumSyncJobStatusFieldRefInput<$PrismaModel>
notIn?: $Enums.SyncJobStatus[] | Prisma.ListEnumSyncJobStatusFieldRefInput<$PrismaModel>
not?: Prisma.NestedEnumSyncJobStatusWithAggregatesFilter<$PrismaModel> | $Enums.SyncJobStatus
_count?: Prisma.NestedIntFilter<$PrismaModel> _count?: Prisma.NestedIntFilter<$PrismaModel>
_min?: Prisma.NestedBoolFilter<$PrismaModel> _min?: Prisma.NestedEnumSyncJobStatusFilter<$PrismaModel>
_max?: Prisma.NestedBoolFilter<$PrismaModel> _max?: Prisma.NestedEnumSyncJobStatusFilter<$PrismaModel>
} }
export type DateTimeNullableWithAggregatesFilter<$PrismaModel = never> = { export type DateTimeNullableWithAggregatesFilter<$PrismaModel = never> = {
@@ -115,21 +137,6 @@ export type DateTimeNullableWithAggregatesFilter<$PrismaModel = never> = {
_max?: Prisma.NestedDateTimeNullableFilter<$PrismaModel> _max?: Prisma.NestedDateTimeNullableFilter<$PrismaModel>
} }
export type StringNullableFilter<$PrismaModel = never> = {
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
mode?: Prisma.QueryMode
not?: Prisma.NestedStringNullableFilter<$PrismaModel> | string | null
}
export type StringNullableWithAggregatesFilter<$PrismaModel = never> = { export type StringNullableWithAggregatesFilter<$PrismaModel = never> = {
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
@@ -148,6 +155,20 @@ export type StringNullableWithAggregatesFilter<$PrismaModel = never> = {
_max?: Prisma.NestedStringNullableFilter<$PrismaModel> _max?: Prisma.NestedStringNullableFilter<$PrismaModel>
} }
export type DateTimeWithAggregatesFilter<$PrismaModel = never> = {
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel>
notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel>
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
not?: Prisma.NestedDateTimeWithAggregatesFilter<$PrismaModel> | Date | string
_count?: Prisma.NestedIntFilter<$PrismaModel>
_min?: Prisma.NestedDateTimeFilter<$PrismaModel>
_max?: Prisma.NestedDateTimeFilter<$PrismaModel>
}
export type IntFilter<$PrismaModel = never> = { export type IntFilter<$PrismaModel = never> = {
equals?: number | Prisma.IntFieldRefInput<$PrismaModel> equals?: number | Prisma.IntFieldRefInput<$PrismaModel>
in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel>
@@ -159,76 +180,6 @@ export type IntFilter<$PrismaModel = never> = {
not?: Prisma.NestedIntFilter<$PrismaModel> | number not?: Prisma.NestedIntFilter<$PrismaModel> | number
} }
export type IntWithAggregatesFilter<$PrismaModel = never> = {
equals?: number | Prisma.IntFieldRefInput<$PrismaModel>
in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel>
notIn?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel>
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
not?: Prisma.NestedIntWithAggregatesFilter<$PrismaModel> | number
_count?: Prisma.NestedIntFilter<$PrismaModel>
_avg?: Prisma.NestedFloatFilter<$PrismaModel>
_sum?: Prisma.NestedIntFilter<$PrismaModel>
_min?: Prisma.NestedIntFilter<$PrismaModel>
_max?: Prisma.NestedIntFilter<$PrismaModel>
}
export type IntNullableFilter<$PrismaModel = never> = {
equals?: number | Prisma.IntFieldRefInput<$PrismaModel> | null
in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> | null
notIn?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> | null
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
not?: Prisma.NestedIntNullableFilter<$PrismaModel> | number | null
}
export type FloatFilter<$PrismaModel = never> = {
equals?: number | Prisma.FloatFieldRefInput<$PrismaModel>
in?: number[] | Prisma.ListFloatFieldRefInput<$PrismaModel>
notIn?: number[] | Prisma.ListFloatFieldRefInput<$PrismaModel>
lt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
lte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
gt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
gte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
not?: Prisma.NestedFloatFilter<$PrismaModel> | number
}
export type IntNullableWithAggregatesFilter<$PrismaModel = never> = {
equals?: number | Prisma.IntFieldRefInput<$PrismaModel> | null
in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> | null
notIn?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> | null
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
not?: Prisma.NestedIntNullableWithAggregatesFilter<$PrismaModel> | number | null
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
_avg?: Prisma.NestedFloatNullableFilter<$PrismaModel>
_sum?: Prisma.NestedIntNullableFilter<$PrismaModel>
_min?: Prisma.NestedIntNullableFilter<$PrismaModel>
_max?: Prisma.NestedIntNullableFilter<$PrismaModel>
}
export type FloatWithAggregatesFilter<$PrismaModel = never> = {
equals?: number | Prisma.FloatFieldRefInput<$PrismaModel>
in?: number[] | Prisma.ListFloatFieldRefInput<$PrismaModel>
notIn?: number[] | Prisma.ListFloatFieldRefInput<$PrismaModel>
lt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
lte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
gt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
gte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
not?: Prisma.NestedFloatWithAggregatesFilter<$PrismaModel> | number
_count?: Prisma.NestedIntFilter<$PrismaModel>
_avg?: Prisma.NestedFloatFilter<$PrismaModel>
_sum?: Prisma.NestedFloatFilter<$PrismaModel>
_min?: Prisma.NestedFloatFilter<$PrismaModel>
_max?: Prisma.NestedFloatFilter<$PrismaModel>
}
export type JsonFilter<$PrismaModel = never> = export type JsonFilter<$PrismaModel = never> =
| Prisma.PatchUndefined< | Prisma.PatchUndefined<
Prisma.Either<Required<JsonFilterBase<$PrismaModel>>, Exclude<keyof Required<JsonFilterBase<$PrismaModel>>, 'path'>>, Prisma.Either<Required<JsonFilterBase<$PrismaModel>>, Exclude<keyof Required<JsonFilterBase<$PrismaModel>>, 'path'>>,
@@ -253,6 +204,22 @@ export type JsonFilterBase<$PrismaModel = never> = {
not?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter not?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter
} }
export type IntWithAggregatesFilter<$PrismaModel = never> = {
equals?: number | Prisma.IntFieldRefInput<$PrismaModel>
in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel>
notIn?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel>
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
not?: Prisma.NestedIntWithAggregatesFilter<$PrismaModel> | number
_count?: Prisma.NestedIntFilter<$PrismaModel>
_avg?: Prisma.NestedFloatFilter<$PrismaModel>
_sum?: Prisma.NestedIntFilter<$PrismaModel>
_min?: Prisma.NestedIntFilter<$PrismaModel>
_max?: Prisma.NestedIntFilter<$PrismaModel>
}
export type JsonWithAggregatesFilter<$PrismaModel = never> = export type JsonWithAggregatesFilter<$PrismaModel = never> =
| Prisma.PatchUndefined< | Prisma.PatchUndefined<
Prisma.Either<Required<JsonWithAggregatesFilterBase<$PrismaModel>>, Exclude<keyof Required<JsonWithAggregatesFilterBase<$PrismaModel>>, 'path'>>, Prisma.Either<Required<JsonWithAggregatesFilterBase<$PrismaModel>>, Exclude<keyof Required<JsonWithAggregatesFilterBase<$PrismaModel>>, 'path'>>,
@@ -280,6 +247,219 @@ export type JsonWithAggregatesFilterBase<$PrismaModel = never> = {
_max?: Prisma.NestedJsonFilter<$PrismaModel> _max?: Prisma.NestedJsonFilter<$PrismaModel>
} }
export type BoolFilter<$PrismaModel = never> = {
equals?: boolean | Prisma.BooleanFieldRefInput<$PrismaModel>
not?: Prisma.NestedBoolFilter<$PrismaModel> | boolean
}
export type BoolWithAggregatesFilter<$PrismaModel = never> = {
equals?: boolean | Prisma.BooleanFieldRefInput<$PrismaModel>
not?: Prisma.NestedBoolWithAggregatesFilter<$PrismaModel> | boolean
_count?: Prisma.NestedIntFilter<$PrismaModel>
_min?: Prisma.NestedBoolFilter<$PrismaModel>
_max?: Prisma.NestedBoolFilter<$PrismaModel>
}
export type IntNullableFilter<$PrismaModel = never> = {
equals?: number | Prisma.IntFieldRefInput<$PrismaModel> | null
in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> | null
notIn?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> | null
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
not?: Prisma.NestedIntNullableFilter<$PrismaModel> | number | null
}
export type IntNullableWithAggregatesFilter<$PrismaModel = never> = {
equals?: number | Prisma.IntFieldRefInput<$PrismaModel> | null
in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> | null
notIn?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> | null
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
not?: Prisma.NestedIntNullableWithAggregatesFilter<$PrismaModel> | number | null
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
_avg?: Prisma.NestedFloatNullableFilter<$PrismaModel>
_sum?: Prisma.NestedIntNullableFilter<$PrismaModel>
_min?: Prisma.NestedIntNullableFilter<$PrismaModel>
_max?: Prisma.NestedIntNullableFilter<$PrismaModel>
}
export type EnumUSStateNullableFilter<$PrismaModel = never> = {
equals?: $Enums.USState | Prisma.EnumUSStateFieldRefInput<$PrismaModel> | null
in?: $Enums.USState[] | Prisma.ListEnumUSStateFieldRefInput<$PrismaModel> | null
notIn?: $Enums.USState[] | Prisma.ListEnumUSStateFieldRefInput<$PrismaModel> | null
not?: Prisma.NestedEnumUSStateNullableFilter<$PrismaModel> | $Enums.USState | null
}
export type EnumCountryNullableFilter<$PrismaModel = never> = {
equals?: $Enums.Country | Prisma.EnumCountryFieldRefInput<$PrismaModel> | null
in?: $Enums.Country[] | Prisma.ListEnumCountryFieldRefInput<$PrismaModel> | null
notIn?: $Enums.Country[] | Prisma.ListEnumCountryFieldRefInput<$PrismaModel> | null
not?: Prisma.NestedEnumCountryNullableFilter<$PrismaModel> | $Enums.Country | null
}
export type EnumUSStateNullableWithAggregatesFilter<$PrismaModel = never> = {
equals?: $Enums.USState | Prisma.EnumUSStateFieldRefInput<$PrismaModel> | null
in?: $Enums.USState[] | Prisma.ListEnumUSStateFieldRefInput<$PrismaModel> | null
notIn?: $Enums.USState[] | Prisma.ListEnumUSStateFieldRefInput<$PrismaModel> | null
not?: Prisma.NestedEnumUSStateNullableWithAggregatesFilter<$PrismaModel> | $Enums.USState | null
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
_min?: Prisma.NestedEnumUSStateNullableFilter<$PrismaModel>
_max?: Prisma.NestedEnumUSStateNullableFilter<$PrismaModel>
}
export type EnumCountryNullableWithAggregatesFilter<$PrismaModel = never> = {
equals?: $Enums.Country | Prisma.EnumCountryFieldRefInput<$PrismaModel> | null
in?: $Enums.Country[] | Prisma.ListEnumCountryFieldRefInput<$PrismaModel> | null
notIn?: $Enums.Country[] | Prisma.ListEnumCountryFieldRefInput<$PrismaModel> | null
not?: Prisma.NestedEnumCountryNullableWithAggregatesFilter<$PrismaModel> | $Enums.Country | null
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
_min?: Prisma.NestedEnumCountryNullableFilter<$PrismaModel>
_max?: Prisma.NestedEnumCountryNullableFilter<$PrismaModel>
}
export type EnumGenderTypeNullableFilter<$PrismaModel = never> = {
equals?: $Enums.GenderType | Prisma.EnumGenderTypeFieldRefInput<$PrismaModel> | null
in?: $Enums.GenderType[] | Prisma.ListEnumGenderTypeFieldRefInput<$PrismaModel> | null
notIn?: $Enums.GenderType[] | Prisma.ListEnumGenderTypeFieldRefInput<$PrismaModel> | null
not?: Prisma.NestedEnumGenderTypeNullableFilter<$PrismaModel> | $Enums.GenderType | null
}
export type EnumPhoneTypeNullableFilter<$PrismaModel = never> = {
equals?: $Enums.PhoneType | Prisma.EnumPhoneTypeFieldRefInput<$PrismaModel> | null
in?: $Enums.PhoneType[] | Prisma.ListEnumPhoneTypeFieldRefInput<$PrismaModel> | null
notIn?: $Enums.PhoneType[] | Prisma.ListEnumPhoneTypeFieldRefInput<$PrismaModel> | null
not?: Prisma.NestedEnumPhoneTypeNullableFilter<$PrismaModel> | $Enums.PhoneType | null
}
export type EnumGenderTypeNullableWithAggregatesFilter<$PrismaModel = never> = {
equals?: $Enums.GenderType | Prisma.EnumGenderTypeFieldRefInput<$PrismaModel> | null
in?: $Enums.GenderType[] | Prisma.ListEnumGenderTypeFieldRefInput<$PrismaModel> | null
notIn?: $Enums.GenderType[] | Prisma.ListEnumGenderTypeFieldRefInput<$PrismaModel> | null
not?: Prisma.NestedEnumGenderTypeNullableWithAggregatesFilter<$PrismaModel> | $Enums.GenderType | null
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
_min?: Prisma.NestedEnumGenderTypeNullableFilter<$PrismaModel>
_max?: Prisma.NestedEnumGenderTypeNullableFilter<$PrismaModel>
}
export type EnumPhoneTypeNullableWithAggregatesFilter<$PrismaModel = never> = {
equals?: $Enums.PhoneType | Prisma.EnumPhoneTypeFieldRefInput<$PrismaModel> | null
in?: $Enums.PhoneType[] | Prisma.ListEnumPhoneTypeFieldRefInput<$PrismaModel> | null
notIn?: $Enums.PhoneType[] | Prisma.ListEnumPhoneTypeFieldRefInput<$PrismaModel> | null
not?: Prisma.NestedEnumPhoneTypeNullableWithAggregatesFilter<$PrismaModel> | $Enums.PhoneType | null
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
_min?: Prisma.NestedEnumPhoneTypeNullableFilter<$PrismaModel>
_max?: Prisma.NestedEnumPhoneTypeNullableFilter<$PrismaModel>
}
export type FloatFilter<$PrismaModel = never> = {
equals?: number | Prisma.FloatFieldRefInput<$PrismaModel>
in?: number[] | Prisma.ListFloatFieldRefInput<$PrismaModel>
notIn?: number[] | Prisma.ListFloatFieldRefInput<$PrismaModel>
lt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
lte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
gt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
gte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
not?: Prisma.NestedFloatFilter<$PrismaModel> | number
}
export type FloatWithAggregatesFilter<$PrismaModel = never> = {
equals?: number | Prisma.FloatFieldRefInput<$PrismaModel>
in?: number[] | Prisma.ListFloatFieldRefInput<$PrismaModel>
notIn?: number[] | Prisma.ListFloatFieldRefInput<$PrismaModel>
lt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
lte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
gt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
gte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
not?: Prisma.NestedFloatWithAggregatesFilter<$PrismaModel> | number
_count?: Prisma.NestedIntFilter<$PrismaModel>
_avg?: Prisma.NestedFloatFilter<$PrismaModel>
_sum?: Prisma.NestedFloatFilter<$PrismaModel>
_min?: Prisma.NestedFloatFilter<$PrismaModel>
_max?: Prisma.NestedFloatFilter<$PrismaModel>
}
export type FloatNullableFilter<$PrismaModel = never> = {
equals?: number | Prisma.FloatFieldRefInput<$PrismaModel> | null
in?: number[] | Prisma.ListFloatFieldRefInput<$PrismaModel> | null
notIn?: number[] | Prisma.ListFloatFieldRefInput<$PrismaModel> | null
lt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
lte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
gt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
gte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
not?: Prisma.NestedFloatNullableFilter<$PrismaModel> | number | null
}
export type FloatNullableWithAggregatesFilter<$PrismaModel = never> = {
equals?: number | Prisma.FloatFieldRefInput<$PrismaModel> | null
in?: number[] | Prisma.ListFloatFieldRefInput<$PrismaModel> | null
notIn?: number[] | Prisma.ListFloatFieldRefInput<$PrismaModel> | null
lt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
lte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
gt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
gte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
not?: Prisma.NestedFloatNullableWithAggregatesFilter<$PrismaModel> | number | null
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
_avg?: Prisma.NestedFloatNullableFilter<$PrismaModel>
_sum?: Prisma.NestedFloatNullableFilter<$PrismaModel>
_min?: Prisma.NestedFloatNullableFilter<$PrismaModel>
_max?: Prisma.NestedFloatNullableFilter<$PrismaModel>
}
export type EnumBillingMethodFilter<$PrismaModel = never> = {
equals?: $Enums.BillingMethod | Prisma.EnumBillingMethodFieldRefInput<$PrismaModel>
in?: $Enums.BillingMethod[] | Prisma.ListEnumBillingMethodFieldRefInput<$PrismaModel>
notIn?: $Enums.BillingMethod[] | Prisma.ListEnumBillingMethodFieldRefInput<$PrismaModel>
not?: Prisma.NestedEnumBillingMethodFilter<$PrismaModel> | $Enums.BillingMethod
}
export type EnumBillingTypeFilter<$PrismaModel = never> = {
equals?: $Enums.BillingType | Prisma.EnumBillingTypeFieldRefInput<$PrismaModel>
in?: $Enums.BillingType[] | Prisma.ListEnumBillingTypeFieldRefInput<$PrismaModel>
notIn?: $Enums.BillingType[] | Prisma.ListEnumBillingTypeFieldRefInput<$PrismaModel>
not?: Prisma.NestedEnumBillingTypeFilter<$PrismaModel> | $Enums.BillingType
}
export type EnumBillingMethodWithAggregatesFilter<$PrismaModel = never> = {
equals?: $Enums.BillingMethod | Prisma.EnumBillingMethodFieldRefInput<$PrismaModel>
in?: $Enums.BillingMethod[] | Prisma.ListEnumBillingMethodFieldRefInput<$PrismaModel>
notIn?: $Enums.BillingMethod[] | Prisma.ListEnumBillingMethodFieldRefInput<$PrismaModel>
not?: Prisma.NestedEnumBillingMethodWithAggregatesFilter<$PrismaModel> | $Enums.BillingMethod
_count?: Prisma.NestedIntFilter<$PrismaModel>
_min?: Prisma.NestedEnumBillingMethodFilter<$PrismaModel>
_max?: Prisma.NestedEnumBillingMethodFilter<$PrismaModel>
}
export type EnumBillingTypeWithAggregatesFilter<$PrismaModel = never> = {
equals?: $Enums.BillingType | Prisma.EnumBillingTypeFieldRefInput<$PrismaModel>
in?: $Enums.BillingType[] | Prisma.ListEnumBillingTypeFieldRefInput<$PrismaModel>
notIn?: $Enums.BillingType[] | Prisma.ListEnumBillingTypeFieldRefInput<$PrismaModel>
not?: Prisma.NestedEnumBillingTypeWithAggregatesFilter<$PrismaModel> | $Enums.BillingType
_count?: Prisma.NestedIntFilter<$PrismaModel>
_min?: Prisma.NestedEnumBillingTypeFilter<$PrismaModel>
_max?: Prisma.NestedEnumBillingTypeFilter<$PrismaModel>
}
export type EnumOpportunityInterestNullableFilter<$PrismaModel = never> = {
equals?: $Enums.OpportunityInterest | Prisma.EnumOpportunityInterestFieldRefInput<$PrismaModel> | null
in?: $Enums.OpportunityInterest[] | Prisma.ListEnumOpportunityInterestFieldRefInput<$PrismaModel> | null
notIn?: $Enums.OpportunityInterest[] | Prisma.ListEnumOpportunityInterestFieldRefInput<$PrismaModel> | null
not?: Prisma.NestedEnumOpportunityInterestNullableFilter<$PrismaModel> | $Enums.OpportunityInterest | null
}
export type EnumOpportunityInterestNullableWithAggregatesFilter<$PrismaModel = never> = {
equals?: $Enums.OpportunityInterest | Prisma.EnumOpportunityInterestFieldRefInput<$PrismaModel> | null
in?: $Enums.OpportunityInterest[] | Prisma.ListEnumOpportunityInterestFieldRefInput<$PrismaModel> | null
notIn?: $Enums.OpportunityInterest[] | Prisma.ListEnumOpportunityInterestFieldRefInput<$PrismaModel> | null
not?: Prisma.NestedEnumOpportunityInterestNullableWithAggregatesFilter<$PrismaModel> | $Enums.OpportunityInterest | null
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
_min?: Prisma.NestedEnumOpportunityInterestNullableFilter<$PrismaModel>
_max?: Prisma.NestedEnumOpportunityInterestNullableFilter<$PrismaModel>
}
export type BytesFilter<$PrismaModel = never> = { export type BytesFilter<$PrismaModel = never> = {
equals?: runtime.Bytes | Prisma.BytesFieldRefInput<$PrismaModel> equals?: runtime.Bytes | Prisma.BytesFieldRefInput<$PrismaModel>
in?: runtime.Bytes[] | Prisma.ListBytesFieldRefInput<$PrismaModel> in?: runtime.Bytes[] | Prisma.ListBytesFieldRefInput<$PrismaModel>
@@ -311,20 +491,18 @@ export type NestedStringFilter<$PrismaModel = never> = {
not?: Prisma.NestedStringFilter<$PrismaModel> | string not?: Prisma.NestedStringFilter<$PrismaModel> | string
} }
export type NestedDateTimeFilter<$PrismaModel = never> = { export type NestedEnumSyncJobTypeFilter<$PrismaModel = never> = {
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> equals?: $Enums.SyncJobType | Prisma.EnumSyncJobTypeFieldRefInput<$PrismaModel>
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> in?: $Enums.SyncJobType[] | Prisma.ListEnumSyncJobTypeFieldRefInput<$PrismaModel>
notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> notIn?: $Enums.SyncJobType[] | Prisma.ListEnumSyncJobTypeFieldRefInput<$PrismaModel>
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> not?: Prisma.NestedEnumSyncJobTypeFilter<$PrismaModel> | $Enums.SyncJobType
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
not?: Prisma.NestedDateTimeFilter<$PrismaModel> | Date | string
} }
export type NestedBoolFilter<$PrismaModel = never> = { export type NestedEnumSyncJobStatusFilter<$PrismaModel = never> = {
equals?: boolean | Prisma.BooleanFieldRefInput<$PrismaModel> equals?: $Enums.SyncJobStatus | Prisma.EnumSyncJobStatusFieldRefInput<$PrismaModel>
not?: Prisma.NestedBoolFilter<$PrismaModel> | boolean in?: $Enums.SyncJobStatus[] | Prisma.ListEnumSyncJobStatusFieldRefInput<$PrismaModel>
notIn?: $Enums.SyncJobStatus[] | Prisma.ListEnumSyncJobStatusFieldRefInput<$PrismaModel>
not?: Prisma.NestedEnumSyncJobStatusFilter<$PrismaModel> | $Enums.SyncJobStatus
} }
export type NestedDateTimeNullableFilter<$PrismaModel = never> = { export type NestedDateTimeNullableFilter<$PrismaModel = never> = {
@@ -338,6 +516,31 @@ export type NestedDateTimeNullableFilter<$PrismaModel = never> = {
not?: Prisma.NestedDateTimeNullableFilter<$PrismaModel> | Date | string | null not?: Prisma.NestedDateTimeNullableFilter<$PrismaModel> | Date | string | null
} }
export type NestedStringNullableFilter<$PrismaModel = never> = {
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
not?: Prisma.NestedStringNullableFilter<$PrismaModel> | string | null
}
export type NestedDateTimeFilter<$PrismaModel = never> = {
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel>
notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel>
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
not?: Prisma.NestedDateTimeFilter<$PrismaModel> | Date | string
}
export type NestedStringWithAggregatesFilter<$PrismaModel = never> = { export type NestedStringWithAggregatesFilter<$PrismaModel = never> = {
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> equals?: string | Prisma.StringFieldRefInput<$PrismaModel>
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel>
@@ -366,26 +569,24 @@ export type NestedIntFilter<$PrismaModel = never> = {
not?: Prisma.NestedIntFilter<$PrismaModel> | number not?: Prisma.NestedIntFilter<$PrismaModel> | number
} }
export type NestedDateTimeWithAggregatesFilter<$PrismaModel = never> = { export type NestedEnumSyncJobTypeWithAggregatesFilter<$PrismaModel = never> = {
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> equals?: $Enums.SyncJobType | Prisma.EnumSyncJobTypeFieldRefInput<$PrismaModel>
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> in?: $Enums.SyncJobType[] | Prisma.ListEnumSyncJobTypeFieldRefInput<$PrismaModel>
notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> notIn?: $Enums.SyncJobType[] | Prisma.ListEnumSyncJobTypeFieldRefInput<$PrismaModel>
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> not?: Prisma.NestedEnumSyncJobTypeWithAggregatesFilter<$PrismaModel> | $Enums.SyncJobType
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
not?: Prisma.NestedDateTimeWithAggregatesFilter<$PrismaModel> | Date | string
_count?: Prisma.NestedIntFilter<$PrismaModel> _count?: Prisma.NestedIntFilter<$PrismaModel>
_min?: Prisma.NestedDateTimeFilter<$PrismaModel> _min?: Prisma.NestedEnumSyncJobTypeFilter<$PrismaModel>
_max?: Prisma.NestedDateTimeFilter<$PrismaModel> _max?: Prisma.NestedEnumSyncJobTypeFilter<$PrismaModel>
} }
export type NestedBoolWithAggregatesFilter<$PrismaModel = never> = { export type NestedEnumSyncJobStatusWithAggregatesFilter<$PrismaModel = never> = {
equals?: boolean | Prisma.BooleanFieldRefInput<$PrismaModel> equals?: $Enums.SyncJobStatus | Prisma.EnumSyncJobStatusFieldRefInput<$PrismaModel>
not?: Prisma.NestedBoolWithAggregatesFilter<$PrismaModel> | boolean in?: $Enums.SyncJobStatus[] | Prisma.ListEnumSyncJobStatusFieldRefInput<$PrismaModel>
notIn?: $Enums.SyncJobStatus[] | Prisma.ListEnumSyncJobStatusFieldRefInput<$PrismaModel>
not?: Prisma.NestedEnumSyncJobStatusWithAggregatesFilter<$PrismaModel> | $Enums.SyncJobStatus
_count?: Prisma.NestedIntFilter<$PrismaModel> _count?: Prisma.NestedIntFilter<$PrismaModel>
_min?: Prisma.NestedBoolFilter<$PrismaModel> _min?: Prisma.NestedEnumSyncJobStatusFilter<$PrismaModel>
_max?: Prisma.NestedBoolFilter<$PrismaModel> _max?: Prisma.NestedEnumSyncJobStatusFilter<$PrismaModel>
} }
export type NestedDateTimeNullableWithAggregatesFilter<$PrismaModel = never> = { export type NestedDateTimeNullableWithAggregatesFilter<$PrismaModel = never> = {
@@ -413,20 +614,6 @@ export type NestedIntNullableFilter<$PrismaModel = never> = {
not?: Prisma.NestedIntNullableFilter<$PrismaModel> | number | null not?: Prisma.NestedIntNullableFilter<$PrismaModel> | number | null
} }
export type NestedStringNullableFilter<$PrismaModel = never> = {
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
not?: Prisma.NestedStringNullableFilter<$PrismaModel> | string | null
}
export type NestedStringNullableWithAggregatesFilter<$PrismaModel = never> = { export type NestedStringNullableWithAggregatesFilter<$PrismaModel = never> = {
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
@@ -444,6 +631,20 @@ export type NestedStringNullableWithAggregatesFilter<$PrismaModel = never> = {
_max?: Prisma.NestedStringNullableFilter<$PrismaModel> _max?: Prisma.NestedStringNullableFilter<$PrismaModel>
} }
export type NestedDateTimeWithAggregatesFilter<$PrismaModel = never> = {
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel>
notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel>
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
not?: Prisma.NestedDateTimeWithAggregatesFilter<$PrismaModel> | Date | string
_count?: Prisma.NestedIntFilter<$PrismaModel>
_min?: Prisma.NestedDateTimeFilter<$PrismaModel>
_max?: Prisma.NestedDateTimeFilter<$PrismaModel>
}
export type NestedIntWithAggregatesFilter<$PrismaModel = never> = { export type NestedIntWithAggregatesFilter<$PrismaModel = never> = {
equals?: number | Prisma.IntFieldRefInput<$PrismaModel> equals?: number | Prisma.IntFieldRefInput<$PrismaModel>
in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel>
@@ -471,6 +672,43 @@ export type NestedFloatFilter<$PrismaModel = never> = {
not?: Prisma.NestedFloatFilter<$PrismaModel> | number not?: Prisma.NestedFloatFilter<$PrismaModel> | number
} }
export type NestedJsonFilter<$PrismaModel = never> =
| Prisma.PatchUndefined<
Prisma.Either<Required<NestedJsonFilterBase<$PrismaModel>>, Exclude<keyof Required<NestedJsonFilterBase<$PrismaModel>>, 'path'>>,
Required<NestedJsonFilterBase<$PrismaModel>>
>
| Prisma.OptionalFlat<Omit<Required<NestedJsonFilterBase<$PrismaModel>>, 'path'>>
export type NestedJsonFilterBase<$PrismaModel = never> = {
equals?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter
path?: string[]
mode?: Prisma.QueryMode | Prisma.EnumQueryModeFieldRefInput<$PrismaModel>
string_contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
string_starts_with?: string | Prisma.StringFieldRefInput<$PrismaModel>
string_ends_with?: string | Prisma.StringFieldRefInput<$PrismaModel>
array_starts_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
array_ends_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
array_contains?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
lt?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
lte?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
gt?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
gte?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
not?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter
}
export type NestedBoolFilter<$PrismaModel = never> = {
equals?: boolean | Prisma.BooleanFieldRefInput<$PrismaModel>
not?: Prisma.NestedBoolFilter<$PrismaModel> | boolean
}
export type NestedBoolWithAggregatesFilter<$PrismaModel = never> = {
equals?: boolean | Prisma.BooleanFieldRefInput<$PrismaModel>
not?: Prisma.NestedBoolWithAggregatesFilter<$PrismaModel> | boolean
_count?: Prisma.NestedIntFilter<$PrismaModel>
_min?: Prisma.NestedBoolFilter<$PrismaModel>
_max?: Prisma.NestedBoolFilter<$PrismaModel>
}
export type NestedIntNullableWithAggregatesFilter<$PrismaModel = never> = { export type NestedIntNullableWithAggregatesFilter<$PrismaModel = never> = {
equals?: number | Prisma.IntFieldRefInput<$PrismaModel> | null equals?: number | Prisma.IntFieldRefInput<$PrismaModel> | null
in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> | null in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> | null
@@ -498,6 +736,74 @@ export type NestedFloatNullableFilter<$PrismaModel = never> = {
not?: Prisma.NestedFloatNullableFilter<$PrismaModel> | number | null not?: Prisma.NestedFloatNullableFilter<$PrismaModel> | number | null
} }
export type NestedEnumUSStateNullableFilter<$PrismaModel = never> = {
equals?: $Enums.USState | Prisma.EnumUSStateFieldRefInput<$PrismaModel> | null
in?: $Enums.USState[] | Prisma.ListEnumUSStateFieldRefInput<$PrismaModel> | null
notIn?: $Enums.USState[] | Prisma.ListEnumUSStateFieldRefInput<$PrismaModel> | null
not?: Prisma.NestedEnumUSStateNullableFilter<$PrismaModel> | $Enums.USState | null
}
export type NestedEnumCountryNullableFilter<$PrismaModel = never> = {
equals?: $Enums.Country | Prisma.EnumCountryFieldRefInput<$PrismaModel> | null
in?: $Enums.Country[] | Prisma.ListEnumCountryFieldRefInput<$PrismaModel> | null
notIn?: $Enums.Country[] | Prisma.ListEnumCountryFieldRefInput<$PrismaModel> | null
not?: Prisma.NestedEnumCountryNullableFilter<$PrismaModel> | $Enums.Country | null
}
export type NestedEnumUSStateNullableWithAggregatesFilter<$PrismaModel = never> = {
equals?: $Enums.USState | Prisma.EnumUSStateFieldRefInput<$PrismaModel> | null
in?: $Enums.USState[] | Prisma.ListEnumUSStateFieldRefInput<$PrismaModel> | null
notIn?: $Enums.USState[] | Prisma.ListEnumUSStateFieldRefInput<$PrismaModel> | null
not?: Prisma.NestedEnumUSStateNullableWithAggregatesFilter<$PrismaModel> | $Enums.USState | null
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
_min?: Prisma.NestedEnumUSStateNullableFilter<$PrismaModel>
_max?: Prisma.NestedEnumUSStateNullableFilter<$PrismaModel>
}
export type NestedEnumCountryNullableWithAggregatesFilter<$PrismaModel = never> = {
equals?: $Enums.Country | Prisma.EnumCountryFieldRefInput<$PrismaModel> | null
in?: $Enums.Country[] | Prisma.ListEnumCountryFieldRefInput<$PrismaModel> | null
notIn?: $Enums.Country[] | Prisma.ListEnumCountryFieldRefInput<$PrismaModel> | null
not?: Prisma.NestedEnumCountryNullableWithAggregatesFilter<$PrismaModel> | $Enums.Country | null
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
_min?: Prisma.NestedEnumCountryNullableFilter<$PrismaModel>
_max?: Prisma.NestedEnumCountryNullableFilter<$PrismaModel>
}
export type NestedEnumGenderTypeNullableFilter<$PrismaModel = never> = {
equals?: $Enums.GenderType | Prisma.EnumGenderTypeFieldRefInput<$PrismaModel> | null
in?: $Enums.GenderType[] | Prisma.ListEnumGenderTypeFieldRefInput<$PrismaModel> | null
notIn?: $Enums.GenderType[] | Prisma.ListEnumGenderTypeFieldRefInput<$PrismaModel> | null
not?: Prisma.NestedEnumGenderTypeNullableFilter<$PrismaModel> | $Enums.GenderType | null
}
export type NestedEnumPhoneTypeNullableFilter<$PrismaModel = never> = {
equals?: $Enums.PhoneType | Prisma.EnumPhoneTypeFieldRefInput<$PrismaModel> | null
in?: $Enums.PhoneType[] | Prisma.ListEnumPhoneTypeFieldRefInput<$PrismaModel> | null
notIn?: $Enums.PhoneType[] | Prisma.ListEnumPhoneTypeFieldRefInput<$PrismaModel> | null
not?: Prisma.NestedEnumPhoneTypeNullableFilter<$PrismaModel> | $Enums.PhoneType | null
}
export type NestedEnumGenderTypeNullableWithAggregatesFilter<$PrismaModel = never> = {
equals?: $Enums.GenderType | Prisma.EnumGenderTypeFieldRefInput<$PrismaModel> | null
in?: $Enums.GenderType[] | Prisma.ListEnumGenderTypeFieldRefInput<$PrismaModel> | null
notIn?: $Enums.GenderType[] | Prisma.ListEnumGenderTypeFieldRefInput<$PrismaModel> | null
not?: Prisma.NestedEnumGenderTypeNullableWithAggregatesFilter<$PrismaModel> | $Enums.GenderType | null
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
_min?: Prisma.NestedEnumGenderTypeNullableFilter<$PrismaModel>
_max?: Prisma.NestedEnumGenderTypeNullableFilter<$PrismaModel>
}
export type NestedEnumPhoneTypeNullableWithAggregatesFilter<$PrismaModel = never> = {
equals?: $Enums.PhoneType | Prisma.EnumPhoneTypeFieldRefInput<$PrismaModel> | null
in?: $Enums.PhoneType[] | Prisma.ListEnumPhoneTypeFieldRefInput<$PrismaModel> | null
notIn?: $Enums.PhoneType[] | Prisma.ListEnumPhoneTypeFieldRefInput<$PrismaModel> | null
not?: Prisma.NestedEnumPhoneTypeNullableWithAggregatesFilter<$PrismaModel> | $Enums.PhoneType | null
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
_min?: Prisma.NestedEnumPhoneTypeNullableFilter<$PrismaModel>
_max?: Prisma.NestedEnumPhoneTypeNullableFilter<$PrismaModel>
}
export type NestedFloatWithAggregatesFilter<$PrismaModel = never> = { export type NestedFloatWithAggregatesFilter<$PrismaModel = never> = {
equals?: number | Prisma.FloatFieldRefInput<$PrismaModel> equals?: number | Prisma.FloatFieldRefInput<$PrismaModel>
in?: number[] | Prisma.ListFloatFieldRefInput<$PrismaModel> in?: number[] | Prisma.ListFloatFieldRefInput<$PrismaModel>
@@ -514,28 +820,71 @@ export type NestedFloatWithAggregatesFilter<$PrismaModel = never> = {
_max?: Prisma.NestedFloatFilter<$PrismaModel> _max?: Prisma.NestedFloatFilter<$PrismaModel>
} }
export type NestedJsonFilter<$PrismaModel = never> = export type NestedFloatNullableWithAggregatesFilter<$PrismaModel = never> = {
| Prisma.PatchUndefined< equals?: number | Prisma.FloatFieldRefInput<$PrismaModel> | null
Prisma.Either<Required<NestedJsonFilterBase<$PrismaModel>>, Exclude<keyof Required<NestedJsonFilterBase<$PrismaModel>>, 'path'>>, in?: number[] | Prisma.ListFloatFieldRefInput<$PrismaModel> | null
Required<NestedJsonFilterBase<$PrismaModel>> notIn?: number[] | Prisma.ListFloatFieldRefInput<$PrismaModel> | null
> lt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
| Prisma.OptionalFlat<Omit<Required<NestedJsonFilterBase<$PrismaModel>>, 'path'>> lte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
gt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
gte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
not?: Prisma.NestedFloatNullableWithAggregatesFilter<$PrismaModel> | number | null
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
_avg?: Prisma.NestedFloatNullableFilter<$PrismaModel>
_sum?: Prisma.NestedFloatNullableFilter<$PrismaModel>
_min?: Prisma.NestedFloatNullableFilter<$PrismaModel>
_max?: Prisma.NestedFloatNullableFilter<$PrismaModel>
}
export type NestedJsonFilterBase<$PrismaModel = never> = { export type NestedEnumBillingMethodFilter<$PrismaModel = never> = {
equals?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter equals?: $Enums.BillingMethod | Prisma.EnumBillingMethodFieldRefInput<$PrismaModel>
path?: string[] in?: $Enums.BillingMethod[] | Prisma.ListEnumBillingMethodFieldRefInput<$PrismaModel>
mode?: Prisma.QueryMode | Prisma.EnumQueryModeFieldRefInput<$PrismaModel> notIn?: $Enums.BillingMethod[] | Prisma.ListEnumBillingMethodFieldRefInput<$PrismaModel>
string_contains?: string | Prisma.StringFieldRefInput<$PrismaModel> not?: Prisma.NestedEnumBillingMethodFilter<$PrismaModel> | $Enums.BillingMethod
string_starts_with?: string | Prisma.StringFieldRefInput<$PrismaModel> }
string_ends_with?: string | Prisma.StringFieldRefInput<$PrismaModel>
array_starts_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null export type NestedEnumBillingTypeFilter<$PrismaModel = never> = {
array_ends_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null equals?: $Enums.BillingType | Prisma.EnumBillingTypeFieldRefInput<$PrismaModel>
array_contains?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null in?: $Enums.BillingType[] | Prisma.ListEnumBillingTypeFieldRefInput<$PrismaModel>
lt?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> notIn?: $Enums.BillingType[] | Prisma.ListEnumBillingTypeFieldRefInput<$PrismaModel>
lte?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> not?: Prisma.NestedEnumBillingTypeFilter<$PrismaModel> | $Enums.BillingType
gt?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> }
gte?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
not?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter export type NestedEnumBillingMethodWithAggregatesFilter<$PrismaModel = never> = {
equals?: $Enums.BillingMethod | Prisma.EnumBillingMethodFieldRefInput<$PrismaModel>
in?: $Enums.BillingMethod[] | Prisma.ListEnumBillingMethodFieldRefInput<$PrismaModel>
notIn?: $Enums.BillingMethod[] | Prisma.ListEnumBillingMethodFieldRefInput<$PrismaModel>
not?: Prisma.NestedEnumBillingMethodWithAggregatesFilter<$PrismaModel> | $Enums.BillingMethod
_count?: Prisma.NestedIntFilter<$PrismaModel>
_min?: Prisma.NestedEnumBillingMethodFilter<$PrismaModel>
_max?: Prisma.NestedEnumBillingMethodFilter<$PrismaModel>
}
export type NestedEnumBillingTypeWithAggregatesFilter<$PrismaModel = never> = {
equals?: $Enums.BillingType | Prisma.EnumBillingTypeFieldRefInput<$PrismaModel>
in?: $Enums.BillingType[] | Prisma.ListEnumBillingTypeFieldRefInput<$PrismaModel>
notIn?: $Enums.BillingType[] | Prisma.ListEnumBillingTypeFieldRefInput<$PrismaModel>
not?: Prisma.NestedEnumBillingTypeWithAggregatesFilter<$PrismaModel> | $Enums.BillingType
_count?: Prisma.NestedIntFilter<$PrismaModel>
_min?: Prisma.NestedEnumBillingTypeFilter<$PrismaModel>
_max?: Prisma.NestedEnumBillingTypeFilter<$PrismaModel>
}
export type NestedEnumOpportunityInterestNullableFilter<$PrismaModel = never> = {
equals?: $Enums.OpportunityInterest | Prisma.EnumOpportunityInterestFieldRefInput<$PrismaModel> | null
in?: $Enums.OpportunityInterest[] | Prisma.ListEnumOpportunityInterestFieldRefInput<$PrismaModel> | null
notIn?: $Enums.OpportunityInterest[] | Prisma.ListEnumOpportunityInterestFieldRefInput<$PrismaModel> | null
not?: Prisma.NestedEnumOpportunityInterestNullableFilter<$PrismaModel> | $Enums.OpportunityInterest | null
}
export type NestedEnumOpportunityInterestNullableWithAggregatesFilter<$PrismaModel = never> = {
equals?: $Enums.OpportunityInterest | Prisma.EnumOpportunityInterestFieldRefInput<$PrismaModel> | null
in?: $Enums.OpportunityInterest[] | Prisma.ListEnumOpportunityInterestFieldRefInput<$PrismaModel> | null
notIn?: $Enums.OpportunityInterest[] | Prisma.ListEnumOpportunityInterestFieldRefInput<$PrismaModel> | null
not?: Prisma.NestedEnumOpportunityInterestNullableWithAggregatesFilter<$PrismaModel> | $Enums.OpportunityInterest | null
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
_min?: Prisma.NestedEnumOpportunityInterestNullableFilter<$PrismaModel>
_max?: Prisma.NestedEnumOpportunityInterestNullableFilter<$PrismaModel>
} }
export type NestedBytesFilter<$PrismaModel = never> = { export type NestedBytesFilter<$PrismaModel = never> = {
+133 -2
View File
@@ -9,7 +9,138 @@
* 🟢 You can import this file directly. * 🟢 You can import this file directly.
*/ */
export const PhoneType = {
DIRECT: 'DIRECT',
MOBILE: 'MOBILE',
HOME: 'HOME',
COMPANY: 'COMPANY',
SITE: 'SITE'
} as const
export type PhoneType = (typeof PhoneType)[keyof typeof PhoneType]
// This file is empty because there are no enums in the schema. export const FaxType = {
export {} FAX: 'FAX',
COMPANY: 'COMPANY',
SITE: 'SITE'
} as const
export type FaxType = (typeof FaxType)[keyof typeof FaxType]
export const BillingMethod = {
ACTUAL_RATES: 'ACTUAL_RATES',
FIXED_FEE: 'FIXED_FEE',
NOT_TO_EXCEED: 'NOT_TO_EXCEED',
OVERRIDE_RATE: 'OVERRIDE_RATE'
} as const
export type BillingMethod = (typeof BillingMethod)[keyof typeof BillingMethod]
export const BillingType = {
STANDARD: 'STANDARD',
PROJECT: 'PROJECT'
} as const
export type BillingType = (typeof BillingType)[keyof typeof BillingType]
export const GenderType = {
MALE: 'MALE',
FEMALE: 'FEMALE'
} as const
export type GenderType = (typeof GenderType)[keyof typeof GenderType]
export const USState = {
AL: 'AL',
AK: 'AK',
AZ: 'AZ',
AR: 'AR',
CA: 'CA',
CO: 'CO',
CT: 'CT',
DE: 'DE',
FL: 'FL',
GA: 'GA',
HI: 'HI',
ID: 'ID',
IL: 'IL',
IN: 'IN',
IA: 'IA',
KS: 'KS',
KY: 'KY',
LA: 'LA',
ME: 'ME',
MD: 'MD',
MA: 'MA',
MI: 'MI',
MN: 'MN',
MS: 'MS',
MO: 'MO',
MT: 'MT',
NE: 'NE',
NV: 'NV',
NH: 'NH',
NJ: 'NJ',
NM: 'NM',
NY: 'NY',
NC: 'NC',
ND: 'ND',
OH: 'OH',
OK: 'OK',
OR: 'OR',
PA: 'PA',
RI: 'RI',
SC: 'SC',
SD: 'SD',
TN: 'TN',
TX: 'TX',
UT: 'UT',
VT: 'VT',
VA: 'VA',
WA: 'WA',
WV: 'WV',
WI: 'WI',
WY: 'WY'
} as const
export type USState = (typeof USState)[keyof typeof USState]
export const Country = {
US: 'US'
} as const
export type Country = (typeof Country)[keyof typeof Country]
export const OpportunityInterest = {
HOT: 'HOT',
WARM: 'WARM',
COLD: 'COLD'
} as const
export type OpportunityInterest = (typeof OpportunityInterest)[keyof typeof OpportunityInterest]
export const SyncJobType = {
FULL_SYNC: 'FULL_SYNC',
INCREMENTAL_SYNC: 'INCREMENTAL_SYNC'
} as const
export type SyncJobType = (typeof SyncJobType)[keyof typeof SyncJobType]
export const SyncJobStatus = {
QUEUED: 'QUEUED',
RUNNING: 'RUNNING',
COMPLETED: 'COMPLETED',
FAILED: 'FAILED',
TIMED_OUT: 'TIMED_OUT'
} as const
export type SyncJobStatus = (typeof SyncJobStatus)[keyof typeof SyncJobStatus]
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
@@ -51,17 +51,50 @@ export const AnyNull = runtime.AnyNull
export const ModelName = { export const ModelName = {
SyncJobRun: 'SyncJobRun',
SyncStepLog: 'SyncStepLog',
Session: 'Session', Session: 'Session',
User: 'User', User: 'User',
Role: 'Role', Role: 'Role',
CorporateLocation: 'CorporateLocation',
InternalDepartment: 'InternalDepartment',
UnifiSite: 'UnifiSite', UnifiSite: 'UnifiSite',
Company: 'Company', Company: 'Company',
CompanyAddress: 'CompanyAddress',
Contact: 'Contact',
CatalogItemType: 'CatalogItemType',
CatalogCategory: 'CatalogCategory',
CatalogSubcategory: 'CatalogSubcategory',
CatalogManufacturer: 'CatalogManufacturer',
WarehouseBin: 'WarehouseBin',
ProductInventory: 'ProductInventory',
Warehouse: 'Warehouse',
MinimumStockByWarehouse: 'MinimumStockByWarehouse',
CatalogItem: 'CatalogItem', CatalogItem: 'CatalogItem',
ProductData: 'ProductData',
ServiceTicket: 'ServiceTicket',
ServiceTicketNote: 'ServiceTicketNote',
ServiceTicketType: 'ServiceTicketType',
ServiceTicketBoard: 'ServiceTicketBoard',
ServiceTicketLocation: 'ServiceTicketLocation',
ServiceTicketSource: 'ServiceTicketSource',
ServiceTicketImpact: 'ServiceTicketImpact',
ServiceTicketPriority: 'ServiceTicketPriority',
ServiceTicketSeverity: 'ServiceTicketSeverity',
ServiceTicketFinalData: 'ServiceTicketFinalData',
OpportunityStage: 'OpportunityStage',
OpportunityType: 'OpportunityType',
OpportunityStatus: 'OpportunityStatus',
Opportunity: 'Opportunity', Opportunity: 'Opportunity',
ScheduleStatus: 'ScheduleStatus',
ScheduleType: 'ScheduleType',
ScheduleSpan: 'ScheduleSpan',
Schedule: 'Schedule',
CredentialType: 'CredentialType', CredentialType: 'CredentialType',
SecureValue: 'SecureValue', SecureValue: 'SecureValue',
Credential: 'Credential', Credential: 'Credential',
GeneratedQuotes: 'GeneratedQuotes', GeneratedQuotes: 'GeneratedQuotes',
TaxCode: 'TaxCode',
CwMember: 'CwMember' CwMember: 'CwMember'
} as const } as const
@@ -81,6 +114,39 @@ export const TransactionIsolationLevel = runtime.makeStrictEnum({
export type TransactionIsolationLevel = (typeof TransactionIsolationLevel)[keyof typeof TransactionIsolationLevel] export type TransactionIsolationLevel = (typeof TransactionIsolationLevel)[keyof typeof TransactionIsolationLevel]
export const SyncJobRunScalarFieldEnum = {
id: 'id',
jobType: 'jobType',
status: 'status',
triggeredBy: 'triggeredBy',
startedAt: 'startedAt',
completedAt: 'completedAt',
errorSummary: 'errorSummary',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
} as const
export type SyncJobRunScalarFieldEnum = (typeof SyncJobRunScalarFieldEnum)[keyof typeof SyncJobRunScalarFieldEnum]
export const SyncStepLogScalarFieldEnum = {
id: 'id',
syncJobRunId: 'syncJobRunId',
tableName: 'tableName',
syncMode: 'syncMode',
recordsProcessed: 'recordsProcessed',
recordsInserted: 'recordsInserted',
recordsSkipped: 'recordsSkipped',
recordsFailed: 'recordsFailed',
recordsDeleted: 'recordsDeleted',
sampleErrors: 'sampleErrors',
durationMs: 'durationMs',
createdAt: 'createdAt'
} as const
export type SyncStepLogScalarFieldEnum = (typeof SyncStepLogScalarFieldEnum)[keyof typeof SyncStepLogScalarFieldEnum]
export const SessionScalarFieldEnum = { export const SessionScalarFieldEnum = {
id: 'id', id: 'id',
sessionKey: 'sessionKey', sessionKey: 'sessionKey',
@@ -98,13 +164,18 @@ export const UserScalarFieldEnum = {
id: 'id', id: 'id',
permissions: 'permissions', permissions: 'permissions',
login: 'login', login: 'login',
name: 'name', firstName: 'firstName',
lastName: 'lastName',
email: 'email', email: 'email',
emailVerified: 'emailVerified',
image: 'image', image: 'image',
title: 'title',
active: 'active',
hidden: 'hidden',
cwIdentifier: 'cwIdentifier', cwIdentifier: 'cwIdentifier',
cwMemberId: 'cwMemberId',
userId: 'userId', userId: 'userId',
token: 'token', token: 'token',
updatedBy: 'updatedBy',
createdAt: 'createdAt', createdAt: 'createdAt',
updatedAt: 'updatedAt' updatedAt: 'updatedAt'
} as const } as const
@@ -124,6 +195,40 @@ export const RoleScalarFieldEnum = {
export type RoleScalarFieldEnum = (typeof RoleScalarFieldEnum)[keyof typeof RoleScalarFieldEnum] export type RoleScalarFieldEnum = (typeof RoleScalarFieldEnum)[keyof typeof RoleScalarFieldEnum]
export const CorporateLocationScalarFieldEnum = {
id: 'id',
uid: 'uid',
name: 'name',
description: 'description',
updatedById: 'updatedById',
addressLine1: 'addressLine1',
addressLine2: 'addressLine2',
city: 'city',
state: 'state',
zipCode: 'zipCode',
country: 'country',
inactiveFlag: 'inactiveFlag',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
} as const
export type CorporateLocationScalarFieldEnum = (typeof CorporateLocationScalarFieldEnum)[keyof typeof CorporateLocationScalarFieldEnum]
export const InternalDepartmentScalarFieldEnum = {
id: 'id',
uid: 'uid',
name: 'name',
description: 'description',
createdById: 'createdById',
updatedById: 'updatedById',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
} as const
export type InternalDepartmentScalarFieldEnum = (typeof InternalDepartmentScalarFieldEnum)[keyof typeof InternalDepartmentScalarFieldEnum]
export const UnifiSiteScalarFieldEnum = { export const UnifiSiteScalarFieldEnum = {
id: 'id', id: 'id',
name: 'name', name: 'name',
@@ -138,9 +243,17 @@ export type UnifiSiteScalarFieldEnum = (typeof UnifiSiteScalarFieldEnum)[keyof t
export const CompanyScalarFieldEnum = { export const CompanyScalarFieldEnum = {
id: 'id', id: 'id',
uid: 'uid',
name: 'name', name: 'name',
cw_CompanyId: 'cw_CompanyId', phone: 'phone',
cw_Identifier: 'cw_Identifier', website: 'website',
deleteFlag: 'deleteFlag',
dateDeleted: 'dateDeleted',
taxId: 'taxId',
taxExempt: 'taxExempt',
enteredById: 'enteredById',
deletedById: 'deletedById',
deletedAt: 'deletedAt',
createdAt: 'createdAt', createdAt: 'createdAt',
updatedAt: 'updatedAt' updatedAt: 'updatedAt'
} as const } as const
@@ -148,20 +261,195 @@ export const CompanyScalarFieldEnum = {
export type CompanyScalarFieldEnum = (typeof CompanyScalarFieldEnum)[keyof typeof CompanyScalarFieldEnum] export type CompanyScalarFieldEnum = (typeof CompanyScalarFieldEnum)[keyof typeof CompanyScalarFieldEnum]
export const CompanyAddressScalarFieldEnum = {
id: 'id',
uid: 'uid',
name: 'name',
description: 'description',
addressLine1: 'addressLine1',
addressLine2: 'addressLine2',
city: 'city',
state: 'state',
zipCode: 'zipCode',
country: 'country',
phone: 'phone',
fax: 'fax',
inactiveFlag: 'inactiveFlag',
defaultFlag: 'defaultFlag',
defaultMailFlag: 'defaultMailFlag',
defaultBillFlag: 'defaultBillFlag',
defaultShipFlag: 'defaultShipFlag',
updatedById: 'updatedById',
companyId: 'companyId',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
} as const
export type CompanyAddressScalarFieldEnum = (typeof CompanyAddressScalarFieldEnum)[keyof typeof CompanyAddressScalarFieldEnum]
export const ContactScalarFieldEnum = {
id: 'id',
uid: 'uid',
active: 'active',
default: 'default',
firstName: 'firstName',
lastName: 'lastName',
nickname: 'nickname',
title: 'title',
gender: 'gender',
birthday: 'birthday',
email: 'email',
phone: 'phone',
phoneExtension: 'phoneExtension',
phoneType: 'phoneType',
companyAddressId: 'companyAddressId',
memberId: 'memberId',
companyId: 'companyId',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
} as const
export type ContactScalarFieldEnum = (typeof ContactScalarFieldEnum)[keyof typeof ContactScalarFieldEnum]
export const CatalogItemTypeScalarFieldEnum = {
id: 'id',
uid: 'uid',
name: 'name',
description: 'description',
inactiveFlag: 'inactiveFlag',
defaultFlag: 'defaultFlag',
updatedById: 'updatedById',
createdById: 'createdById',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
} as const
export type CatalogItemTypeScalarFieldEnum = (typeof CatalogItemTypeScalarFieldEnum)[keyof typeof CatalogItemTypeScalarFieldEnum]
export const CatalogCategoryScalarFieldEnum = {
id: 'id',
uid: 'uid',
name: 'name',
description: 'description',
inactiveFlag: 'inactiveFlag',
updatedById: 'updatedById',
createdById: 'createdById',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
} as const
export type CatalogCategoryScalarFieldEnum = (typeof CatalogCategoryScalarFieldEnum)[keyof typeof CatalogCategoryScalarFieldEnum]
export const CatalogSubcategoryScalarFieldEnum = {
id: 'id',
uid: 'uid',
name: 'name',
description: 'description',
categoryId: 'categoryId',
inactiveFlag: 'inactiveFlag',
updatedById: 'updatedById',
createdById: 'createdById',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
} as const
export type CatalogSubcategoryScalarFieldEnum = (typeof CatalogSubcategoryScalarFieldEnum)[keyof typeof CatalogSubcategoryScalarFieldEnum]
export const CatalogManufacturerScalarFieldEnum = {
id: 'id',
uid: 'uid',
name: 'name',
description: 'description',
inactiveFlag: 'inactiveFlag',
updatedById: 'updatedById',
createdById: 'createdById',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
} as const
export type CatalogManufacturerScalarFieldEnum = (typeof CatalogManufacturerScalarFieldEnum)[keyof typeof CatalogManufacturerScalarFieldEnum]
export const WarehouseBinScalarFieldEnum = {
id: 'id',
uid: 'uid',
name: 'name',
description: 'description',
minQuantity: 'minQuantity',
maxQuantity: 'maxQuantity',
inactiveFlag: 'inactiveFlag',
defaultFlag: 'defaultFlag',
warehouseId: 'warehouseId',
updatedById: 'updatedById',
createdById: 'createdById',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
} as const
export type WarehouseBinScalarFieldEnum = (typeof WarehouseBinScalarFieldEnum)[keyof typeof WarehouseBinScalarFieldEnum]
export const ProductInventoryScalarFieldEnum = {
id: 'id',
uid: 'uid',
qtyOnHand: 'qtyOnHand',
warehouseBinId: 'warehouseBinId',
itemId: 'itemId',
warehouseId: 'warehouseId',
updatedById: 'updatedById',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
} as const
export type ProductInventoryScalarFieldEnum = (typeof ProductInventoryScalarFieldEnum)[keyof typeof ProductInventoryScalarFieldEnum]
export const WarehouseScalarFieldEnum = {
id: 'id',
uid: 'uid',
name: 'name',
description: 'description',
inactiveFlag: 'inactiveFlag',
lockedFlag: 'lockedFlag',
updatedById: 'updatedById',
createdById: 'createdById',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
} as const
export type WarehouseScalarFieldEnum = (typeof WarehouseScalarFieldEnum)[keyof typeof WarehouseScalarFieldEnum]
export const MinimumStockByWarehouseScalarFieldEnum = {
id: 'id',
uid: 'uid',
minQty: 'minQty',
warehouseId: 'warehouseId',
itemId: 'itemId',
updatedById: 'updatedById',
enteredById: 'enteredById',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
} as const
export type MinimumStockByWarehouseScalarFieldEnum = (typeof MinimumStockByWarehouseScalarFieldEnum)[keyof typeof MinimumStockByWarehouseScalarFieldEnum]
export const CatalogItemScalarFieldEnum = { export const CatalogItemScalarFieldEnum = {
id: 'id', id: 'id',
cwCatalogId: 'cwCatalogId', uid: 'uid',
identifier: 'identifier', identifier: 'identifier',
name: 'name', name: 'name',
description: 'description', description: 'description',
customerDescription: 'customerDescription', customerDescription: 'customerDescription',
internalNotes: 'internalNotes', internalNotes: 'internalNotes',
category: 'category', subcategoryId: 'subcategoryId',
categoryCwId: 'categoryCwId', manufacturerId: 'manufacturerId',
subcategory: 'subcategory',
subcategoryCwId: 'subcategoryCwId',
manufacturer: 'manufacturer',
manufactureCwId: 'manufactureCwId',
partNumber: 'partNumber', partNumber: 'partNumber',
vendorName: 'vendorName', vendorName: 'vendorName',
vendorSku: 'vendorSku', vendorSku: 'vendorSku',
@@ -172,6 +460,7 @@ export const CatalogItemScalarFieldEnum = {
salesTaxable: 'salesTaxable', salesTaxable: 'salesTaxable',
onHand: 'onHand', onHand: 'onHand',
cwLastUpdated: 'cwLastUpdated', cwLastUpdated: 'cwLastUpdated',
classId: 'classId',
createdAt: 'createdAt', createdAt: 'createdAt',
updatedAt: 'updatedAt' updatedAt: 'updatedAt'
} as const } as const
@@ -179,54 +468,338 @@ export const CatalogItemScalarFieldEnum = {
export type CatalogItemScalarFieldEnum = (typeof CatalogItemScalarFieldEnum)[keyof typeof CatalogItemScalarFieldEnum] export type CatalogItemScalarFieldEnum = (typeof CatalogItemScalarFieldEnum)[keyof typeof CatalogItemScalarFieldEnum]
export const ProductDataScalarFieldEnum = {
id: 'id',
uid: 'uid',
qty: 'qty',
internalNote: 'internalNote',
shortDescription: 'shortDescription',
description: 'description',
sequenceNumber: 'sequenceNumber',
procurementNotes: 'procurementNotes',
productNarrative: 'productNarrative',
unitPrice: 'unitPrice',
unitCost: 'unitCost',
listPrice: 'listPrice',
discount: 'discount',
recurringRevenue: 'recurringRevenue',
recurringCost: 'recurringCost',
qtyPicked: 'qtyPicked',
qtyShipped: 'qtyShipped',
cancelReason: 'cancelReason',
cancelQty: 'cancelQty',
billableFlag: 'billableFlag',
taxableFlag: 'taxableFlag',
invoiceFlag: 'invoiceFlag',
recurringFlag: 'recurringFlag',
poApprovedFlag: 'poApprovedFlag',
calcPriceFlag: 'calcPriceFlag',
calcCostFlag: 'calcCostFlag',
cancelFlag: 'cancelFlag',
catalogItemId: 'catalogItemId',
corporateLocationId: 'corporateLocationId',
serviceTicketId: 'serviceTicketId',
opportunityId: 'opportunityId',
updatedById: 'updatedById',
createdById: 'createdById',
closedById: 'closedById',
cancelById: 'cancelById',
closedAt: 'closedAt',
cancelledAt: 'cancelledAt',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
} as const
export type ProductDataScalarFieldEnum = (typeof ProductDataScalarFieldEnum)[keyof typeof ProductDataScalarFieldEnum]
export const ServiceTicketScalarFieldEnum = {
id: 'id',
uid: 'uid',
summary: 'summary',
addressLine1: 'addressLine1',
addressLine2: 'addressLine2',
city: 'city',
state: 'state',
zipCode: 'zipCode',
country: 'country',
contactName: 'contactName',
phone: 'phone',
phoneExtension: 'phoneExtension',
phoneType: 'phoneType',
email: 'email',
poNumber: 'poNumber',
billCompleteFlag: 'billCompleteFlag',
billUnapprovedFlag: 'billUnapprovedFlag',
billingAmount: 'billingAmount',
billingMethod: 'billingMethod',
timeBillableFlag: 'timeBillableFlag',
expenseBillableFlag: 'expenseBillableFlag',
productBillableFlag: 'productBillableFlag',
timeInvoiceableFlag: 'timeInvoiceableFlag',
expenseInvoiceableFlag: 'expenseInvoiceableFlag',
productInvoiceableFlag: 'productInvoiceableFlag',
dateRequested: 'dateRequested',
billingType: 'billingType',
billingInstructions: 'billingInstructions',
rejectedFlag: 'rejectedFlag',
closedFlag: 'closedFlag',
redFlag: 'redFlag',
publishFlag: 'publishFlag',
ticketOwnerId: 'ticketOwnerId',
serviceTicketBoardId: 'serviceTicketBoardId',
severityId: 'severityId',
impactId: 'impactId',
priorityId: 'priorityId',
sourceId: 'sourceId',
locationId: 'locationId',
parentId: 'parentId',
companyId: 'companyId',
contactId: 'contactId',
companyAddressId: 'companyAddressId',
billingCompanyId: 'billingCompanyId',
billingAddressId: 'billingAddressId',
createdById: 'createdById',
updatedById: 'updatedById',
closedById: 'closedById',
rejectedAt: 'rejectedAt',
closedAt: 'closedAt',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
} as const
export type ServiceTicketScalarFieldEnum = (typeof ServiceTicketScalarFieldEnum)[keyof typeof ServiceTicketScalarFieldEnum]
export const ServiceTicketNoteScalarFieldEnum = {
id: 'id',
uid: 'uid',
notes: 'notes',
notesMd: 'notesMd',
authorId: 'authorId',
problemFlag: 'problemFlag',
resolutionFlag: 'resolutionFlag',
internalAnalysisFlag: 'internalAnalysisFlag',
internalMemberFlag: 'internalMemberFlag',
createdByParentFlag: 'createdByParentFlag',
mergedFlag: 'mergedFlag',
bundledFlag: 'bundledFlag',
serviceTicketId: 'serviceTicketId',
createdById: 'createdById',
updatedById: 'updatedById',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
} as const
export type ServiceTicketNoteScalarFieldEnum = (typeof ServiceTicketNoteScalarFieldEnum)[keyof typeof ServiceTicketNoteScalarFieldEnum]
export const ServiceTicketTypeScalarFieldEnum = {
id: 'id',
uid: 'uid',
name: 'name',
description: 'description',
inactiveFlag: 'inactiveFlag',
updatedById: 'updatedById',
createdById: 'createdById',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
} as const
export type ServiceTicketTypeScalarFieldEnum = (typeof ServiceTicketTypeScalarFieldEnum)[keyof typeof ServiceTicketTypeScalarFieldEnum]
export const ServiceTicketBoardScalarFieldEnum = {
id: 'id',
uid: 'uid',
name: 'name',
timeBillableFlag: 'timeBillableFlag',
expenseBillableFlag: 'expenseBillableFlag',
productBillableFlag: 'productBillableFlag',
timeInvoiceableFlag: 'timeInvoiceableFlag',
expenseInvoiceableFlag: 'expenseInvoiceableFlag',
productInvoiceableFlag: 'productInvoiceableFlag',
autoAssignNewFlag: 'autoAssignNewFlag',
autoAssignEmailCreatedFlag: 'autoAssignEmailCreatedFlag',
autoAssignPortalCreatedFlag: 'autoAssignPortalCreatedFlag',
projectFlag: 'projectFlag',
lockDescriptionFlag: 'lockDescriptionFlag',
emailContactFlag: 'emailContactFlag',
emailResourceFlag: 'emailResourceFlag',
resolutionSortOrder: 'resolutionSortOrder',
internalAnalysisSortOrder: 'internalAnalysisSortOrder',
locationId: 'locationId',
createdById: 'createdById',
updatedById: 'updatedById',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
} as const
export type ServiceTicketBoardScalarFieldEnum = (typeof ServiceTicketBoardScalarFieldEnum)[keyof typeof ServiceTicketBoardScalarFieldEnum]
export const ServiceTicketLocationScalarFieldEnum = {
id: 'id',
uid: 'uid',
name: 'name',
description: 'description',
defaultFlag: 'defaultFlag',
updatedById: 'updatedById',
createdById: 'createdById',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
} as const
export type ServiceTicketLocationScalarFieldEnum = (typeof ServiceTicketLocationScalarFieldEnum)[keyof typeof ServiceTicketLocationScalarFieldEnum]
export const ServiceTicketSourceScalarFieldEnum = {
id: 'id',
uid: 'uid',
name: 'name',
description: 'description',
inactiveFlag: 'inactiveFlag',
defaultFlag: 'defaultFlag',
updatedById: 'updatedById',
createdById: 'createdById',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
} as const
export type ServiceTicketSourceScalarFieldEnum = (typeof ServiceTicketSourceScalarFieldEnum)[keyof typeof ServiceTicketSourceScalarFieldEnum]
export const ServiceTicketImpactScalarFieldEnum = {
id: 'id',
uid: 'uid',
name: 'name',
description: 'description',
defaultFlag: 'defaultFlag',
updatedById: 'updatedById',
createdById: 'createdById',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
} as const
export type ServiceTicketImpactScalarFieldEnum = (typeof ServiceTicketImpactScalarFieldEnum)[keyof typeof ServiceTicketImpactScalarFieldEnum]
export const ServiceTicketPriorityScalarFieldEnum = {
id: 'id',
uid: 'uid',
name: 'name',
color: 'color',
description: 'description',
defaultFlag: 'defaultFlag',
updatedById: 'updatedById',
createdById: 'createdById',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
} as const
export type ServiceTicketPriorityScalarFieldEnum = (typeof ServiceTicketPriorityScalarFieldEnum)[keyof typeof ServiceTicketPriorityScalarFieldEnum]
export const ServiceTicketSeverityScalarFieldEnum = {
id: 'id',
uid: 'uid',
name: 'name',
description: 'description',
defaultFlag: 'defaultFlag',
updatedById: 'updatedById',
createdById: 'createdById',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
} as const
export type ServiceTicketSeverityScalarFieldEnum = (typeof ServiceTicketSeverityScalarFieldEnum)[keyof typeof ServiceTicketSeverityScalarFieldEnum]
export const ServiceTicketFinalDataScalarFieldEnum = {
id: 'id'
} as const
export type ServiceTicketFinalDataScalarFieldEnum = (typeof ServiceTicketFinalDataScalarFieldEnum)[keyof typeof ServiceTicketFinalDataScalarFieldEnum]
export const OpportunityStageScalarFieldEnum = {
id: 'id',
uid: 'uid',
name: 'name',
seqNbr: 'seqNbr',
funnelColor: 'funnelColor',
updatedById: 'updatedById',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
} as const
export type OpportunityStageScalarFieldEnum = (typeof OpportunityStageScalarFieldEnum)[keyof typeof OpportunityStageScalarFieldEnum]
export const OpportunityTypeScalarFieldEnum = {
id: 'id',
uid: 'uid',
name: 'name',
description: 'description',
inactiveFlag: 'inactiveFlag',
updatedById: 'updatedById',
createdById: 'createdById',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
} as const
export type OpportunityTypeScalarFieldEnum = (typeof OpportunityTypeScalarFieldEnum)[keyof typeof OpportunityTypeScalarFieldEnum]
export const OpportunityStatusScalarFieldEnum = {
id: 'id',
uid: 'uid',
name: 'name',
description: 'description',
inactiveFlag: 'inactiveFlag',
defaultFlag: 'defaultFlag',
wonFlag: 'wonFlag',
lostFlag: 'lostFlag',
closeFlag: 'closeFlag',
updatedById: 'updatedById',
createdById: 'createdById',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
} as const
export type OpportunityStatusScalarFieldEnum = (typeof OpportunityStatusScalarFieldEnum)[keyof typeof OpportunityStatusScalarFieldEnum]
export const OpportunityScalarFieldEnum = { export const OpportunityScalarFieldEnum = {
id: 'id', id: 'id',
cwOpportunityId: 'cwOpportunityId', uid: 'uid',
name: 'name', name: 'name',
notes: 'notes', notes: 'notes',
typeName: 'typeName', oppNarrative: 'oppNarrative',
typeCwId: 'typeCwId', typeId: 'typeId',
stageName: 'stageName', stageId: 'stageId',
stageCwId: 'stageCwId', statusId: 'statusId',
statusName: 'statusName', taxCodeId: 'taxCodeId',
statusCwId: 'statusCwId', interest: 'interest',
priorityName: 'priorityName',
priorityCwId: 'priorityCwId',
ratingName: 'ratingName',
ratingCwId: 'ratingCwId',
source: 'source',
campaignName: 'campaignName',
campaignCwId: 'campaignCwId',
primarySalesRepName: 'primarySalesRepName',
primarySalesRepIdentifier: 'primarySalesRepIdentifier',
primarySalesRepCwId: 'primarySalesRepCwId',
secondarySalesRepName: 'secondarySalesRepName',
secondarySalesRepIdentifier: 'secondarySalesRepIdentifier',
secondarySalesRepCwId: 'secondarySalesRepCwId',
companyCwId: 'companyCwId',
companyName: 'companyName',
contactCwId: 'contactCwId',
contactName: 'contactName',
siteCwId: 'siteCwId',
siteName: 'siteName',
customerPO: 'customerPO',
totalSalesTax: 'totalSalesTax',
probability: 'probability', probability: 'probability',
locationName: 'locationName', source: 'source',
locationCwId: 'locationCwId', primarySalesRepId: 'primarySalesRepId',
departmentName: 'departmentName', secondarySalesRepId: 'secondarySalesRepId',
departmentCwId: 'departmentCwId', companyId: 'companyId',
contactId: 'contactId',
siteId: 'siteId',
customerPO: 'customerPO',
locationId: 'locationId',
departmentId: 'departmentId',
expectedCloseDate: 'expectedCloseDate', expectedCloseDate: 'expectedCloseDate',
pipelineChangeDate: 'pipelineChangeDate', pipelineChangeDate: 'pipelineChangeDate',
dateBecameLead: 'dateBecameLead', dateBecameLead: 'dateBecameLead',
closedDate: 'closedDate', closedDate: 'closedDate',
closedFlag: 'closedFlag', closedFlag: 'closedFlag',
closedByName: 'closedByName', closedById: 'closedById',
closedByCwId: 'closedByCwId',
companyId: 'companyId',
productSequence: 'productSequence', productSequence: 'productSequence',
cwLastUpdated: 'cwLastUpdated', updatedBy: 'updatedBy',
cwDateEntered: 'cwDateEntered', eneteredBy: 'eneteredBy',
createdAt: 'createdAt', createdAt: 'createdAt',
updatedAt: 'updatedAt' updatedAt: 'updatedAt'
} as const } as const
@@ -234,6 +807,87 @@ export const OpportunityScalarFieldEnum = {
export type OpportunityScalarFieldEnum = (typeof OpportunityScalarFieldEnum)[keyof typeof OpportunityScalarFieldEnum] export type OpportunityScalarFieldEnum = (typeof OpportunityScalarFieldEnum)[keyof typeof OpportunityScalarFieldEnum]
export const ScheduleStatusScalarFieldEnum = {
id: 'id',
uid: 'uid',
name: 'name',
description: 'description',
color: 'color',
softFlag: 'softFlag',
defaultFlag: 'defaultFlag',
updatedById: 'updatedById',
createdById: 'createdById',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
} as const
export type ScheduleStatusScalarFieldEnum = (typeof ScheduleStatusScalarFieldEnum)[keyof typeof ScheduleStatusScalarFieldEnum]
export const ScheduleTypeScalarFieldEnum = {
id: 'id',
uid: 'uid',
name: 'name',
description: 'description',
displayColor: 'displayColor',
tableReference: 'tableReference',
moduleId: 'moduleId',
scheduleTypeId: 'scheduleTypeId',
systemFlag: 'systemFlag',
displayFlag: 'displayFlag',
updatedById: 'updatedById',
createdById: 'createdById',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
} as const
export type ScheduleTypeScalarFieldEnum = (typeof ScheduleTypeScalarFieldEnum)[keyof typeof ScheduleTypeScalarFieldEnum]
export const ScheduleSpanScalarFieldEnum = {
id: 'id',
scheduleSpanId: 'scheduleSpanId',
spanDesc: 'spanDesc'
} as const
export type ScheduleSpanScalarFieldEnum = (typeof ScheduleSpanScalarFieldEnum)[keyof typeof ScheduleSpanScalarFieldEnum]
export const ScheduleScalarFieldEnum = {
id: 'id',
uid: 'uid',
name: 'name',
description: 'description',
memberId: 'memberId',
closedFlag: 'closedFlag',
reminderFlag: 'reminderFlag',
allDayFlag: 'allDayFlag',
acknowledgementFlag: 'acknowledgementFlag',
meetingFlag: 'meetingFlag',
recurringFlag: 'recurringFlag',
billableFlag: 'billableFlag',
acknowledgedById: 'acknowledgedById',
acknowledgedAt: 'acknowledgedAt',
startDate: 'startDate',
endDate: 'endDate',
hoursScheduled: 'hoursScheduled',
duration: 'duration',
hoursPerDay: 'hoursPerDay',
reminderMinutes: 'reminderMinutes',
statusId: 'statusId',
typeId: 'typeId',
scheduleSpanId: 'scheduleSpanId',
updatedById: 'updatedById',
createdById: 'createdById',
closedById: 'closedById',
closedAt: 'closedAt',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
} as const
export type ScheduleScalarFieldEnum = (typeof ScheduleScalarFieldEnum)[keyof typeof ScheduleScalarFieldEnum]
export const CredentialTypeScalarFieldEnum = { export const CredentialTypeScalarFieldEnum = {
id: 'id', id: 'id',
name: 'name', name: 'name',
@@ -292,6 +946,23 @@ export const GeneratedQuotesScalarFieldEnum = {
export type GeneratedQuotesScalarFieldEnum = (typeof GeneratedQuotesScalarFieldEnum)[keyof typeof GeneratedQuotesScalarFieldEnum] export type GeneratedQuotesScalarFieldEnum = (typeof GeneratedQuotesScalarFieldEnum)[keyof typeof GeneratedQuotesScalarFieldEnum]
export const TaxCodeScalarFieldEnum = {
id: 'id',
uid: 'uid',
code: 'code',
codeCaption: 'codeCaption',
description: 'description',
rate: 'rate',
defaultFlag: 'defaultFlag',
createdBy: 'createdBy',
updatedBy: 'updatedBy',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
} as const
export type TaxCodeScalarFieldEnum = (typeof TaxCodeScalarFieldEnum)[keyof typeof TaxCodeScalarFieldEnum]
export const CwMemberScalarFieldEnum = { export const CwMemberScalarFieldEnum = {
id: 'id', id: 'id',
cwMemberId: 'cwMemberId', cwMemberId: 'cwMemberId',
+33
View File
@@ -8,16 +8,49 @@
* *
* 🟢 You can import this file directly. * 🟢 You can import this file directly.
*/ */
export type * from './models/SyncJobRun.ts'
export type * from './models/SyncStepLog.ts'
export type * from './models/Session.ts' export type * from './models/Session.ts'
export type * from './models/User.ts' export type * from './models/User.ts'
export type * from './models/Role.ts' export type * from './models/Role.ts'
export type * from './models/CorporateLocation.ts'
export type * from './models/InternalDepartment.ts'
export type * from './models/UnifiSite.ts' export type * from './models/UnifiSite.ts'
export type * from './models/Company.ts' export type * from './models/Company.ts'
export type * from './models/CompanyAddress.ts'
export type * from './models/Contact.ts'
export type * from './models/CatalogItemType.ts'
export type * from './models/CatalogCategory.ts'
export type * from './models/CatalogSubcategory.ts'
export type * from './models/CatalogManufacturer.ts'
export type * from './models/WarehouseBin.ts'
export type * from './models/ProductInventory.ts'
export type * from './models/Warehouse.ts'
export type * from './models/MinimumStockByWarehouse.ts'
export type * from './models/CatalogItem.ts' export type * from './models/CatalogItem.ts'
export type * from './models/ProductData.ts'
export type * from './models/ServiceTicket.ts'
export type * from './models/ServiceTicketNote.ts'
export type * from './models/ServiceTicketType.ts'
export type * from './models/ServiceTicketBoard.ts'
export type * from './models/ServiceTicketLocation.ts'
export type * from './models/ServiceTicketSource.ts'
export type * from './models/ServiceTicketImpact.ts'
export type * from './models/ServiceTicketPriority.ts'
export type * from './models/ServiceTicketSeverity.ts'
export type * from './models/ServiceTicketFinalData.ts'
export type * from './models/OpportunityStage.ts'
export type * from './models/OpportunityType.ts'
export type * from './models/OpportunityStatus.ts'
export type * from './models/Opportunity.ts' export type * from './models/Opportunity.ts'
export type * from './models/ScheduleStatus.ts'
export type * from './models/ScheduleType.ts'
export type * from './models/ScheduleSpan.ts'
export type * from './models/Schedule.ts'
export type * from './models/CredentialType.ts' export type * from './models/CredentialType.ts'
export type * from './models/SecureValue.ts' export type * from './models/SecureValue.ts'
export type * from './models/Credential.ts' export type * from './models/Credential.ts'
export type * from './models/GeneratedQuotes.ts' export type * from './models/GeneratedQuotes.ts'
export type * from './models/TaxCode.ts'
export type * from './models/CwMember.ts' export type * from './models/CwMember.ts'
export type * from './commonInputTypes.ts' export type * from './commonInputTypes.ts'
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -1806,6 +1806,11 @@ export type CredentialFindManyArgs<ExtArgs extends runtime.Types.Extensions.Inte
* Skip the first `n` Credentials. * Skip the first `n` Credentials.
*/ */
skip?: number skip?: number
/**
* {@link https://www.prisma.io/docs/concepts/components/prisma-client/distinct Distinct Docs}
*
* Filter by unique combinations of Credentials.
*/
distinct?: Prisma.CredentialScalarFieldEnum | Prisma.CredentialScalarFieldEnum[] distinct?: Prisma.CredentialScalarFieldEnum | Prisma.CredentialScalarFieldEnum[]
} }
@@ -1146,6 +1146,11 @@ export type CredentialTypeFindManyArgs<ExtArgs extends runtime.Types.Extensions.
* Skip the first `n` CredentialTypes. * Skip the first `n` CredentialTypes.
*/ */
skip?: number skip?: number
/**
* {@link https://www.prisma.io/docs/concepts/components/prisma-client/distinct Distinct Docs}
*
* Filter by unique combinations of CredentialTypes.
*/
distinct?: Prisma.CredentialTypeScalarFieldEnum | Prisma.CredentialTypeScalarFieldEnum[] distinct?: Prisma.CredentialTypeScalarFieldEnum | Prisma.CredentialTypeScalarFieldEnum[]
} }
+174 -1
View File
@@ -264,6 +264,7 @@ export type CwMemberWhereInput = {
cwLastUpdated?: Prisma.DateTimeNullableFilter<"CwMember"> | Date | string | null cwLastUpdated?: Prisma.DateTimeNullableFilter<"CwMember"> | Date | string | null
createdAt?: Prisma.DateTimeFilter<"CwMember"> | Date | string createdAt?: Prisma.DateTimeFilter<"CwMember"> | Date | string
updatedAt?: Prisma.DateTimeFilter<"CwMember"> | Date | string updatedAt?: Prisma.DateTimeFilter<"CwMember"> | Date | string
user?: Prisma.XOR<Prisma.UserNullableScalarRelationFilter, Prisma.UserWhereInput> | null
} }
export type CwMemberOrderByWithRelationInput = { export type CwMemberOrderByWithRelationInput = {
@@ -278,6 +279,7 @@ export type CwMemberOrderByWithRelationInput = {
cwLastUpdated?: Prisma.SortOrderInput | Prisma.SortOrder cwLastUpdated?: Prisma.SortOrderInput | Prisma.SortOrder
createdAt?: Prisma.SortOrder createdAt?: Prisma.SortOrder
updatedAt?: Prisma.SortOrder updatedAt?: Prisma.SortOrder
user?: Prisma.UserOrderByWithRelationInput
} }
export type CwMemberWhereUniqueInput = Prisma.AtLeast<{ export type CwMemberWhereUniqueInput = Prisma.AtLeast<{
@@ -295,6 +297,7 @@ export type CwMemberWhereUniqueInput = Prisma.AtLeast<{
cwLastUpdated?: Prisma.DateTimeNullableFilter<"CwMember"> | Date | string | null cwLastUpdated?: Prisma.DateTimeNullableFilter<"CwMember"> | Date | string | null
createdAt?: Prisma.DateTimeFilter<"CwMember"> | Date | string createdAt?: Prisma.DateTimeFilter<"CwMember"> | Date | string
updatedAt?: Prisma.DateTimeFilter<"CwMember"> | Date | string updatedAt?: Prisma.DateTimeFilter<"CwMember"> | Date | string
user?: Prisma.XOR<Prisma.UserNullableScalarRelationFilter, Prisma.UserWhereInput> | null
}, "id" | "cwMemberId" | "identifier"> }, "id" | "cwMemberId" | "identifier">
export type CwMemberOrderByWithAggregationInput = { export type CwMemberOrderByWithAggregationInput = {
@@ -345,6 +348,7 @@ export type CwMemberCreateInput = {
cwLastUpdated?: Date | string | null cwLastUpdated?: Date | string | null
createdAt?: Date | string createdAt?: Date | string
updatedAt?: Date | string updatedAt?: Date | string
user?: Prisma.UserCreateNestedOneWithoutCwMemberInput
} }
export type CwMemberUncheckedCreateInput = { export type CwMemberUncheckedCreateInput = {
@@ -359,6 +363,7 @@ export type CwMemberUncheckedCreateInput = {
cwLastUpdated?: Date | string | null cwLastUpdated?: Date | string | null
createdAt?: Date | string createdAt?: Date | string
updatedAt?: Date | string updatedAt?: Date | string
user?: Prisma.UserUncheckedCreateNestedOneWithoutCwMemberInput
} }
export type CwMemberUpdateInput = { export type CwMemberUpdateInput = {
@@ -373,6 +378,7 @@ export type CwMemberUpdateInput = {
cwLastUpdated?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null cwLastUpdated?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
user?: Prisma.UserUpdateOneWithoutCwMemberNestedInput
} }
export type CwMemberUncheckedUpdateInput = { export type CwMemberUncheckedUpdateInput = {
@@ -387,6 +393,7 @@ export type CwMemberUncheckedUpdateInput = {
cwLastUpdated?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null cwLastUpdated?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
user?: Prisma.UserUncheckedUpdateOneWithoutCwMemberNestedInput
} }
export type CwMemberCreateManyInput = { export type CwMemberCreateManyInput = {
@@ -431,6 +438,11 @@ export type CwMemberUncheckedUpdateManyInput = {
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
} }
export type CwMemberNullableScalarRelationFilter = {
is?: Prisma.CwMemberWhereInput | null
isNot?: Prisma.CwMemberWhereInput | null
}
export type CwMemberCountOrderByAggregateInput = { export type CwMemberCountOrderByAggregateInput = {
id?: Prisma.SortOrder id?: Prisma.SortOrder
cwMemberId?: Prisma.SortOrder cwMemberId?: Prisma.SortOrder
@@ -481,6 +493,94 @@ export type CwMemberSumOrderByAggregateInput = {
cwMemberId?: Prisma.SortOrder cwMemberId?: Prisma.SortOrder
} }
export type CwMemberCreateNestedOneWithoutUserInput = {
create?: Prisma.XOR<Prisma.CwMemberCreateWithoutUserInput, Prisma.CwMemberUncheckedCreateWithoutUserInput>
connectOrCreate?: Prisma.CwMemberCreateOrConnectWithoutUserInput
connect?: Prisma.CwMemberWhereUniqueInput
}
export type CwMemberUpdateOneWithoutUserNestedInput = {
create?: Prisma.XOR<Prisma.CwMemberCreateWithoutUserInput, Prisma.CwMemberUncheckedCreateWithoutUserInput>
connectOrCreate?: Prisma.CwMemberCreateOrConnectWithoutUserInput
upsert?: Prisma.CwMemberUpsertWithoutUserInput
disconnect?: Prisma.CwMemberWhereInput | boolean
delete?: Prisma.CwMemberWhereInput | boolean
connect?: Prisma.CwMemberWhereUniqueInput
update?: Prisma.XOR<Prisma.XOR<Prisma.CwMemberUpdateToOneWithWhereWithoutUserInput, Prisma.CwMemberUpdateWithoutUserInput>, Prisma.CwMemberUncheckedUpdateWithoutUserInput>
}
export type CwMemberCreateWithoutUserInput = {
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 CwMemberUncheckedCreateWithoutUserInput = {
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 CwMemberCreateOrConnectWithoutUserInput = {
where: Prisma.CwMemberWhereUniqueInput
create: Prisma.XOR<Prisma.CwMemberCreateWithoutUserInput, Prisma.CwMemberUncheckedCreateWithoutUserInput>
}
export type CwMemberUpsertWithoutUserInput = {
update: Prisma.XOR<Prisma.CwMemberUpdateWithoutUserInput, Prisma.CwMemberUncheckedUpdateWithoutUserInput>
create: Prisma.XOR<Prisma.CwMemberCreateWithoutUserInput, Prisma.CwMemberUncheckedCreateWithoutUserInput>
where?: Prisma.CwMemberWhereInput
}
export type CwMemberUpdateToOneWithWhereWithoutUserInput = {
where?: Prisma.CwMemberWhereInput
data: Prisma.XOR<Prisma.CwMemberUpdateWithoutUserInput, Prisma.CwMemberUncheckedUpdateWithoutUserInput>
}
export type CwMemberUpdateWithoutUserInput = {
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 CwMemberUncheckedUpdateWithoutUserInput = {
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 CwMemberSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{ export type CwMemberSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
@@ -495,6 +595,7 @@ export type CwMemberSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs
cwLastUpdated?: boolean cwLastUpdated?: boolean
createdAt?: boolean createdAt?: boolean
updatedAt?: boolean updatedAt?: boolean
user?: boolean | Prisma.CwMember$userArgs<ExtArgs>
}, ExtArgs["result"]["cwMember"]> }, ExtArgs["result"]["cwMember"]>
export type CwMemberSelectCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{ export type CwMemberSelectCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
@@ -540,10 +641,17 @@ export type CwMemberSelectScalar = {
} }
export type CwMemberOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "cwMemberId" | "identifier" | "firstName" | "lastName" | "officeEmail" | "inactiveFlag" | "apiKey" | "cwLastUpdated" | "createdAt" | "updatedAt", ExtArgs["result"]["cwMember"]> export type CwMemberOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "cwMemberId" | "identifier" | "firstName" | "lastName" | "officeEmail" | "inactiveFlag" | "apiKey" | "cwLastUpdated" | "createdAt" | "updatedAt", ExtArgs["result"]["cwMember"]>
export type CwMemberInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
user?: boolean | Prisma.CwMember$userArgs<ExtArgs>
}
export type CwMemberIncludeCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {}
export type CwMemberIncludeUpdateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {}
export type $CwMemberPayload<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = { export type $CwMemberPayload<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
name: "CwMember" name: "CwMember"
objects: {} objects: {
user: Prisma.$UserPayload<ExtArgs> | null
}
scalars: runtime.Types.Extensions.GetPayloadResult<{ scalars: runtime.Types.Extensions.GetPayloadResult<{
id: string id: string
cwMemberId: number cwMemberId: number
@@ -950,6 +1058,7 @@ readonly fields: CwMemberFieldRefs;
*/ */
export interface Prisma__CwMemberClient<T, Null = never, ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs, GlobalOmitOptions = {}> extends Prisma.PrismaPromise<T> { export interface Prisma__CwMemberClient<T, Null = never, ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs, GlobalOmitOptions = {}> extends Prisma.PrismaPromise<T> {
readonly [Symbol.toStringTag]: "PrismaPromise" readonly [Symbol.toStringTag]: "PrismaPromise"
user<T extends Prisma.CwMember$userArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.CwMember$userArgs<ExtArgs>>): Prisma.Prisma__UserClient<runtime.Types.Result.GetResult<Prisma.$UserPayload<ExtArgs>, T, "findUniqueOrThrow", GlobalOmitOptions> | null, null, ExtArgs, GlobalOmitOptions>
/** /**
* Attaches callbacks for the resolution and/or rejection of the Promise. * Attaches callbacks for the resolution and/or rejection of the Promise.
* @param onfulfilled The callback to execute when the Promise is resolved. * @param onfulfilled The callback to execute when the Promise is resolved.
@@ -1006,6 +1115,10 @@ export type CwMemberFindUniqueArgs<ExtArgs extends runtime.Types.Extensions.Inte
* Omit specific fields from the CwMember * Omit specific fields from the CwMember
*/ */
omit?: Prisma.CwMemberOmit<ExtArgs> | null omit?: Prisma.CwMemberOmit<ExtArgs> | null
/**
* Choose, which related nodes to fetch as well
*/
include?: Prisma.CwMemberInclude<ExtArgs> | null
/** /**
* Filter, which CwMember to fetch. * Filter, which CwMember to fetch.
*/ */
@@ -1024,6 +1137,10 @@ export type CwMemberFindUniqueOrThrowArgs<ExtArgs extends runtime.Types.Extensio
* Omit specific fields from the CwMember * Omit specific fields from the CwMember
*/ */
omit?: Prisma.CwMemberOmit<ExtArgs> | null omit?: Prisma.CwMemberOmit<ExtArgs> | null
/**
* Choose, which related nodes to fetch as well
*/
include?: Prisma.CwMemberInclude<ExtArgs> | null
/** /**
* Filter, which CwMember to fetch. * Filter, which CwMember to fetch.
*/ */
@@ -1042,6 +1159,10 @@ export type CwMemberFindFirstArgs<ExtArgs extends runtime.Types.Extensions.Inter
* Omit specific fields from the CwMember * Omit specific fields from the CwMember
*/ */
omit?: Prisma.CwMemberOmit<ExtArgs> | null omit?: Prisma.CwMemberOmit<ExtArgs> | null
/**
* Choose, which related nodes to fetch as well
*/
include?: Prisma.CwMemberInclude<ExtArgs> | null
/** /**
* Filter, which CwMember to fetch. * Filter, which CwMember to fetch.
*/ */
@@ -1090,6 +1211,10 @@ export type CwMemberFindFirstOrThrowArgs<ExtArgs extends runtime.Types.Extension
* Omit specific fields from the CwMember * Omit specific fields from the CwMember
*/ */
omit?: Prisma.CwMemberOmit<ExtArgs> | null omit?: Prisma.CwMemberOmit<ExtArgs> | null
/**
* Choose, which related nodes to fetch as well
*/
include?: Prisma.CwMemberInclude<ExtArgs> | null
/** /**
* Filter, which CwMember to fetch. * Filter, which CwMember to fetch.
*/ */
@@ -1138,6 +1263,10 @@ export type CwMemberFindManyArgs<ExtArgs extends runtime.Types.Extensions.Intern
* Omit specific fields from the CwMember * Omit specific fields from the CwMember
*/ */
omit?: Prisma.CwMemberOmit<ExtArgs> | null omit?: Prisma.CwMemberOmit<ExtArgs> | null
/**
* Choose, which related nodes to fetch as well
*/
include?: Prisma.CwMemberInclude<ExtArgs> | null
/** /**
* Filter, which CwMembers to fetch. * Filter, which CwMembers to fetch.
*/ */
@@ -1166,6 +1295,11 @@ export type CwMemberFindManyArgs<ExtArgs extends runtime.Types.Extensions.Intern
* Skip the first `n` CwMembers. * Skip the first `n` CwMembers.
*/ */
skip?: number 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[] distinct?: Prisma.CwMemberScalarFieldEnum | Prisma.CwMemberScalarFieldEnum[]
} }
@@ -1181,6 +1315,10 @@ export type CwMemberCreateArgs<ExtArgs extends runtime.Types.Extensions.Internal
* Omit specific fields from the CwMember * Omit specific fields from the CwMember
*/ */
omit?: Prisma.CwMemberOmit<ExtArgs> | null omit?: Prisma.CwMemberOmit<ExtArgs> | null
/**
* Choose, which related nodes to fetch as well
*/
include?: Prisma.CwMemberInclude<ExtArgs> | null
/** /**
* The data needed to create a CwMember. * The data needed to create a CwMember.
*/ */
@@ -1229,6 +1367,10 @@ export type CwMemberUpdateArgs<ExtArgs extends runtime.Types.Extensions.Internal
* Omit specific fields from the CwMember * Omit specific fields from the CwMember
*/ */
omit?: Prisma.CwMemberOmit<ExtArgs> | null omit?: Prisma.CwMemberOmit<ExtArgs> | null
/**
* Choose, which related nodes to fetch as well
*/
include?: Prisma.CwMemberInclude<ExtArgs> | null
/** /**
* The data needed to update a CwMember. * The data needed to update a CwMember.
*/ */
@@ -1295,6 +1437,10 @@ export type CwMemberUpsertArgs<ExtArgs extends runtime.Types.Extensions.Internal
* Omit specific fields from the CwMember * Omit specific fields from the CwMember
*/ */
omit?: Prisma.CwMemberOmit<ExtArgs> | null omit?: Prisma.CwMemberOmit<ExtArgs> | null
/**
* Choose, which related nodes to fetch as well
*/
include?: Prisma.CwMemberInclude<ExtArgs> | null
/** /**
* The filter to search for the CwMember to update in case it exists. * The filter to search for the CwMember to update in case it exists.
*/ */
@@ -1321,6 +1467,10 @@ export type CwMemberDeleteArgs<ExtArgs extends runtime.Types.Extensions.Internal
* Omit specific fields from the CwMember * Omit specific fields from the CwMember
*/ */
omit?: Prisma.CwMemberOmit<ExtArgs> | null omit?: Prisma.CwMemberOmit<ExtArgs> | null
/**
* Choose, which related nodes to fetch as well
*/
include?: Prisma.CwMemberInclude<ExtArgs> | null
/** /**
* Filter which CwMember to delete. * Filter which CwMember to delete.
*/ */
@@ -1341,6 +1491,25 @@ export type CwMemberDeleteManyArgs<ExtArgs extends runtime.Types.Extensions.Inte
limit?: number limit?: number
} }
/**
* CwMember.user
*/
export type CwMember$userArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
/**
* Select specific fields to fetch from the User
*/
select?: Prisma.UserSelect<ExtArgs> | null
/**
* Omit specific fields from the User
*/
omit?: Prisma.UserOmit<ExtArgs> | null
/**
* Choose, which related nodes to fetch as well
*/
include?: Prisma.UserInclude<ExtArgs> | null
where?: Prisma.UserWhereInput
}
/** /**
* CwMember without action * CwMember without action
*/ */
@@ -1353,4 +1522,8 @@ export type CwMemberDefaultArgs<ExtArgs extends runtime.Types.Extensions.Interna
* Omit specific fields from the CwMember * Omit specific fields from the CwMember
*/ */
omit?: Prisma.CwMemberOmit<ExtArgs> | null omit?: Prisma.CwMemberOmit<ExtArgs> | null
/**
* Choose, which related nodes to fetch as well
*/
include?: Prisma.CwMemberInclude<ExtArgs> | null
} }
@@ -1474,6 +1474,11 @@ export type GeneratedQuotesFindManyArgs<ExtArgs extends runtime.Types.Extensions
* Skip the first `n` GeneratedQuotes. * Skip the first `n` GeneratedQuotes.
*/ */
skip?: number skip?: number
/**
* {@link https://www.prisma.io/docs/concepts/components/prisma-client/distinct Distinct Docs}
*
* Filter by unique combinations of GeneratedQuotes.
*/
distinct?: Prisma.GeneratedQuotesScalarFieldEnum | Prisma.GeneratedQuotesScalarFieldEnum[] distinct?: Prisma.GeneratedQuotesScalarFieldEnum | Prisma.GeneratedQuotesScalarFieldEnum[]
} }
File diff suppressed because it is too large Load Diff
+5
View File
@@ -1175,6 +1175,11 @@ export type RoleFindManyArgs<ExtArgs extends runtime.Types.Extensions.InternalAr
* Skip the first `n` Roles. * Skip the first `n` Roles.
*/ */
skip?: number skip?: number
/**
* {@link https://www.prisma.io/docs/concepts/components/prisma-client/distinct Distinct Docs}
*
* Filter by unique combinations of Roles.
*/
distinct?: Prisma.RoleScalarFieldEnum | Prisma.RoleScalarFieldEnum[] distinct?: Prisma.RoleScalarFieldEnum | Prisma.RoleScalarFieldEnum[]
} }
@@ -1192,6 +1192,11 @@ export type SecureValueFindManyArgs<ExtArgs extends runtime.Types.Extensions.Int
* Skip the first `n` SecureValues. * Skip the first `n` SecureValues.
*/ */
skip?: number skip?: number
/**
* {@link https://www.prisma.io/docs/concepts/components/prisma-client/distinct Distinct Docs}
*
* Filter by unique combinations of SecureValues.
*/
distinct?: Prisma.SecureValueScalarFieldEnum | Prisma.SecureValueScalarFieldEnum[] distinct?: Prisma.SecureValueScalarFieldEnum | Prisma.SecureValueScalarFieldEnum[]
} }
+5 -12
View File
@@ -361,22 +361,10 @@ export type SessionOrderByRelationAggregateInput = {
_count?: Prisma.SortOrder _count?: Prisma.SortOrder
} }
export type StringFieldUpdateOperationsInput = {
set?: string
}
export type DateTimeFieldUpdateOperationsInput = {
set?: Date | string
}
export type BoolFieldUpdateOperationsInput = { export type BoolFieldUpdateOperationsInput = {
set?: boolean set?: boolean
} }
export type NullableDateTimeFieldUpdateOperationsInput = {
set?: Date | string | null
}
export type SessionCreateNestedManyWithoutUserInput = { export type SessionCreateNestedManyWithoutUserInput = {
create?: Prisma.XOR<Prisma.SessionCreateWithoutUserInput, Prisma.SessionUncheckedCreateWithoutUserInput> | Prisma.SessionCreateWithoutUserInput[] | Prisma.SessionUncheckedCreateWithoutUserInput[] create?: Prisma.XOR<Prisma.SessionCreateWithoutUserInput, Prisma.SessionUncheckedCreateWithoutUserInput> | Prisma.SessionCreateWithoutUserInput[] | Prisma.SessionUncheckedCreateWithoutUserInput[]
connectOrCreate?: Prisma.SessionCreateOrConnectWithoutUserInput | Prisma.SessionCreateOrConnectWithoutUserInput[] connectOrCreate?: Prisma.SessionCreateOrConnectWithoutUserInput | Prisma.SessionCreateOrConnectWithoutUserInput[]
@@ -1208,6 +1196,11 @@ export type SessionFindManyArgs<ExtArgs extends runtime.Types.Extensions.Interna
* Skip the first `n` Sessions. * Skip the first `n` Sessions.
*/ */
skip?: number skip?: number
/**
* {@link https://www.prisma.io/docs/concepts/components/prisma-client/distinct Distinct Docs}
*
* Filter by unique combinations of Sessions.
*/
distinct?: Prisma.SessionScalarFieldEnum | Prisma.SessionScalarFieldEnum[] distinct?: Prisma.SessionScalarFieldEnum | Prisma.SessionScalarFieldEnum[]
} }
+62 -13
View File
@@ -20,15 +20,25 @@ export type UnifiSiteModel = runtime.Types.Result.DefaultSelection<Prisma.$Unifi
export type AggregateUnifiSite = { export type AggregateUnifiSite = {
_count: UnifiSiteCountAggregateOutputType | null _count: UnifiSiteCountAggregateOutputType | null
_avg: UnifiSiteAvgAggregateOutputType | null
_sum: UnifiSiteSumAggregateOutputType | null
_min: UnifiSiteMinAggregateOutputType | null _min: UnifiSiteMinAggregateOutputType | null
_max: UnifiSiteMaxAggregateOutputType | null _max: UnifiSiteMaxAggregateOutputType | null
} }
export type UnifiSiteAvgAggregateOutputType = {
companyId: number | null
}
export type UnifiSiteSumAggregateOutputType = {
companyId: number | null
}
export type UnifiSiteMinAggregateOutputType = { export type UnifiSiteMinAggregateOutputType = {
id: string | null id: string | null
name: string | null name: string | null
siteId: string | null siteId: string | null
companyId: string | null companyId: number | null
createdAt: Date | null createdAt: Date | null
updatedAt: Date | null updatedAt: Date | null
} }
@@ -37,7 +47,7 @@ export type UnifiSiteMaxAggregateOutputType = {
id: string | null id: string | null
name: string | null name: string | null
siteId: string | null siteId: string | null
companyId: string | null companyId: number | null
createdAt: Date | null createdAt: Date | null
updatedAt: Date | null updatedAt: Date | null
} }
@@ -53,6 +63,14 @@ export type UnifiSiteCountAggregateOutputType = {
} }
export type UnifiSiteAvgAggregateInputType = {
companyId?: true
}
export type UnifiSiteSumAggregateInputType = {
companyId?: true
}
export type UnifiSiteMinAggregateInputType = { export type UnifiSiteMinAggregateInputType = {
id?: true id?: true
name?: true name?: true
@@ -116,6 +134,18 @@ export type UnifiSiteAggregateArgs<ExtArgs extends runtime.Types.Extensions.Inte
* Count returned UnifiSites * Count returned UnifiSites
**/ **/
_count?: true | UnifiSiteCountAggregateInputType _count?: true | UnifiSiteCountAggregateInputType
/**
* {@link https://www.prisma.io/docs/concepts/components/prisma-client/aggregations Aggregation Docs}
*
* Select which fields to average
**/
_avg?: UnifiSiteAvgAggregateInputType
/**
* {@link https://www.prisma.io/docs/concepts/components/prisma-client/aggregations Aggregation Docs}
*
* Select which fields to sum
**/
_sum?: UnifiSiteSumAggregateInputType
/** /**
* {@link https://www.prisma.io/docs/concepts/components/prisma-client/aggregations Aggregation Docs} * {@link https://www.prisma.io/docs/concepts/components/prisma-client/aggregations Aggregation Docs}
* *
@@ -149,6 +179,8 @@ export type UnifiSiteGroupByArgs<ExtArgs extends runtime.Types.Extensions.Intern
take?: number take?: number
skip?: number skip?: number
_count?: UnifiSiteCountAggregateInputType | true _count?: UnifiSiteCountAggregateInputType | true
_avg?: UnifiSiteAvgAggregateInputType
_sum?: UnifiSiteSumAggregateInputType
_min?: UnifiSiteMinAggregateInputType _min?: UnifiSiteMinAggregateInputType
_max?: UnifiSiteMaxAggregateInputType _max?: UnifiSiteMaxAggregateInputType
} }
@@ -157,10 +189,12 @@ export type UnifiSiteGroupByOutputType = {
id: string id: string
name: string name: string
siteId: string siteId: string
companyId: string | null companyId: number | null
createdAt: Date createdAt: Date
updatedAt: Date updatedAt: Date
_count: UnifiSiteCountAggregateOutputType | null _count: UnifiSiteCountAggregateOutputType | null
_avg: UnifiSiteAvgAggregateOutputType | null
_sum: UnifiSiteSumAggregateOutputType | null
_min: UnifiSiteMinAggregateOutputType | null _min: UnifiSiteMinAggregateOutputType | null
_max: UnifiSiteMaxAggregateOutputType | null _max: UnifiSiteMaxAggregateOutputType | null
} }
@@ -187,7 +221,7 @@ export type UnifiSiteWhereInput = {
id?: Prisma.StringFilter<"UnifiSite"> | string id?: Prisma.StringFilter<"UnifiSite"> | string
name?: Prisma.StringFilter<"UnifiSite"> | string name?: Prisma.StringFilter<"UnifiSite"> | string
siteId?: Prisma.StringFilter<"UnifiSite"> | string siteId?: Prisma.StringFilter<"UnifiSite"> | string
companyId?: Prisma.StringNullableFilter<"UnifiSite"> | string | null companyId?: Prisma.IntNullableFilter<"UnifiSite"> | number | null
createdAt?: Prisma.DateTimeFilter<"UnifiSite"> | Date | string createdAt?: Prisma.DateTimeFilter<"UnifiSite"> | Date | string
updatedAt?: Prisma.DateTimeFilter<"UnifiSite"> | Date | string updatedAt?: Prisma.DateTimeFilter<"UnifiSite"> | Date | string
company?: Prisma.XOR<Prisma.CompanyNullableScalarRelationFilter, Prisma.CompanyWhereInput> | null company?: Prisma.XOR<Prisma.CompanyNullableScalarRelationFilter, Prisma.CompanyWhereInput> | null
@@ -210,7 +244,7 @@ export type UnifiSiteWhereUniqueInput = Prisma.AtLeast<{
OR?: Prisma.UnifiSiteWhereInput[] OR?: Prisma.UnifiSiteWhereInput[]
NOT?: Prisma.UnifiSiteWhereInput | Prisma.UnifiSiteWhereInput[] NOT?: Prisma.UnifiSiteWhereInput | Prisma.UnifiSiteWhereInput[]
name?: Prisma.StringFilter<"UnifiSite"> | string name?: Prisma.StringFilter<"UnifiSite"> | string
companyId?: Prisma.StringNullableFilter<"UnifiSite"> | string | null companyId?: Prisma.IntNullableFilter<"UnifiSite"> | number | null
createdAt?: Prisma.DateTimeFilter<"UnifiSite"> | Date | string createdAt?: Prisma.DateTimeFilter<"UnifiSite"> | Date | string
updatedAt?: Prisma.DateTimeFilter<"UnifiSite"> | Date | string updatedAt?: Prisma.DateTimeFilter<"UnifiSite"> | Date | string
company?: Prisma.XOR<Prisma.CompanyNullableScalarRelationFilter, Prisma.CompanyWhereInput> | null company?: Prisma.XOR<Prisma.CompanyNullableScalarRelationFilter, Prisma.CompanyWhereInput> | null
@@ -224,8 +258,10 @@ export type UnifiSiteOrderByWithAggregationInput = {
createdAt?: Prisma.SortOrder createdAt?: Prisma.SortOrder
updatedAt?: Prisma.SortOrder updatedAt?: Prisma.SortOrder
_count?: Prisma.UnifiSiteCountOrderByAggregateInput _count?: Prisma.UnifiSiteCountOrderByAggregateInput
_avg?: Prisma.UnifiSiteAvgOrderByAggregateInput
_max?: Prisma.UnifiSiteMaxOrderByAggregateInput _max?: Prisma.UnifiSiteMaxOrderByAggregateInput
_min?: Prisma.UnifiSiteMinOrderByAggregateInput _min?: Prisma.UnifiSiteMinOrderByAggregateInput
_sum?: Prisma.UnifiSiteSumOrderByAggregateInput
} }
export type UnifiSiteScalarWhereWithAggregatesInput = { export type UnifiSiteScalarWhereWithAggregatesInput = {
@@ -235,7 +271,7 @@ export type UnifiSiteScalarWhereWithAggregatesInput = {
id?: Prisma.StringWithAggregatesFilter<"UnifiSite"> | string id?: Prisma.StringWithAggregatesFilter<"UnifiSite"> | string
name?: Prisma.StringWithAggregatesFilter<"UnifiSite"> | string name?: Prisma.StringWithAggregatesFilter<"UnifiSite"> | string
siteId?: Prisma.StringWithAggregatesFilter<"UnifiSite"> | string siteId?: Prisma.StringWithAggregatesFilter<"UnifiSite"> | string
companyId?: Prisma.StringNullableWithAggregatesFilter<"UnifiSite"> | string | null companyId?: Prisma.IntNullableWithAggregatesFilter<"UnifiSite"> | number | null
createdAt?: Prisma.DateTimeWithAggregatesFilter<"UnifiSite"> | Date | string createdAt?: Prisma.DateTimeWithAggregatesFilter<"UnifiSite"> | Date | string
updatedAt?: Prisma.DateTimeWithAggregatesFilter<"UnifiSite"> | Date | string updatedAt?: Prisma.DateTimeWithAggregatesFilter<"UnifiSite"> | Date | string
} }
@@ -253,7 +289,7 @@ export type UnifiSiteUncheckedCreateInput = {
id?: string id?: string
name: string name: string
siteId: string siteId: string
companyId?: string | null companyId?: number | null
createdAt?: Date | string createdAt?: Date | string
updatedAt?: Date | string updatedAt?: Date | string
} }
@@ -271,7 +307,7 @@ export type UnifiSiteUncheckedUpdateInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string id?: Prisma.StringFieldUpdateOperationsInput | string
name?: Prisma.StringFieldUpdateOperationsInput | string name?: Prisma.StringFieldUpdateOperationsInput | string
siteId?: Prisma.StringFieldUpdateOperationsInput | string siteId?: Prisma.StringFieldUpdateOperationsInput | string
companyId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null companyId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
} }
@@ -280,7 +316,7 @@ export type UnifiSiteCreateManyInput = {
id?: string id?: string
name: string name: string
siteId: string siteId: string
companyId?: string | null companyId?: number | null
createdAt?: Date | string createdAt?: Date | string
updatedAt?: Date | string updatedAt?: Date | string
} }
@@ -297,7 +333,7 @@ export type UnifiSiteUncheckedUpdateManyInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string id?: Prisma.StringFieldUpdateOperationsInput | string
name?: Prisma.StringFieldUpdateOperationsInput | string name?: Prisma.StringFieldUpdateOperationsInput | string
siteId?: Prisma.StringFieldUpdateOperationsInput | string siteId?: Prisma.StringFieldUpdateOperationsInput | string
companyId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null companyId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
} }
@@ -311,6 +347,10 @@ export type UnifiSiteCountOrderByAggregateInput = {
updatedAt?: Prisma.SortOrder updatedAt?: Prisma.SortOrder
} }
export type UnifiSiteAvgOrderByAggregateInput = {
companyId?: Prisma.SortOrder
}
export type UnifiSiteMaxOrderByAggregateInput = { export type UnifiSiteMaxOrderByAggregateInput = {
id?: Prisma.SortOrder id?: Prisma.SortOrder
name?: Prisma.SortOrder name?: Prisma.SortOrder
@@ -329,6 +369,10 @@ export type UnifiSiteMinOrderByAggregateInput = {
updatedAt?: Prisma.SortOrder updatedAt?: Prisma.SortOrder
} }
export type UnifiSiteSumOrderByAggregateInput = {
companyId?: Prisma.SortOrder
}
export type UnifiSiteListRelationFilter = { export type UnifiSiteListRelationFilter = {
every?: Prisma.UnifiSiteWhereInput every?: Prisma.UnifiSiteWhereInput
some?: Prisma.UnifiSiteWhereInput some?: Prisma.UnifiSiteWhereInput
@@ -430,7 +474,7 @@ export type UnifiSiteScalarWhereInput = {
id?: Prisma.StringFilter<"UnifiSite"> | string id?: Prisma.StringFilter<"UnifiSite"> | string
name?: Prisma.StringFilter<"UnifiSite"> | string name?: Prisma.StringFilter<"UnifiSite"> | string
siteId?: Prisma.StringFilter<"UnifiSite"> | string siteId?: Prisma.StringFilter<"UnifiSite"> | string
companyId?: Prisma.StringNullableFilter<"UnifiSite"> | string | null companyId?: Prisma.IntNullableFilter<"UnifiSite"> | number | null
createdAt?: Prisma.DateTimeFilter<"UnifiSite"> | Date | string createdAt?: Prisma.DateTimeFilter<"UnifiSite"> | Date | string
updatedAt?: Prisma.DateTimeFilter<"UnifiSite"> | Date | string updatedAt?: Prisma.DateTimeFilter<"UnifiSite"> | Date | string
} }
@@ -528,7 +572,7 @@ export type $UnifiSitePayload<ExtArgs extends runtime.Types.Extensions.InternalA
id: string id: string
name: string name: string
siteId: string siteId: string
companyId: string | null companyId: number | null
createdAt: Date createdAt: Date
updatedAt: Date updatedAt: Date
}, ExtArgs["result"]["unifiSite"]> }, ExtArgs["result"]["unifiSite"]>
@@ -958,7 +1002,7 @@ export interface UnifiSiteFieldRefs {
readonly id: Prisma.FieldRef<"UnifiSite", 'String'> readonly id: Prisma.FieldRef<"UnifiSite", 'String'>
readonly name: Prisma.FieldRef<"UnifiSite", 'String'> readonly name: Prisma.FieldRef<"UnifiSite", 'String'>
readonly siteId: Prisma.FieldRef<"UnifiSite", 'String'> readonly siteId: Prisma.FieldRef<"UnifiSite", 'String'>
readonly companyId: Prisma.FieldRef<"UnifiSite", 'String'> readonly companyId: Prisma.FieldRef<"UnifiSite", 'Int'>
readonly createdAt: Prisma.FieldRef<"UnifiSite", 'DateTime'> readonly createdAt: Prisma.FieldRef<"UnifiSite", 'DateTime'>
readonly updatedAt: Prisma.FieldRef<"UnifiSite", 'DateTime'> readonly updatedAt: Prisma.FieldRef<"UnifiSite", 'DateTime'>
} }
@@ -1157,6 +1201,11 @@ export type UnifiSiteFindManyArgs<ExtArgs extends runtime.Types.Extensions.Inter
* Skip the first `n` UnifiSites. * Skip the first `n` UnifiSites.
*/ */
skip?: number skip?: number
/**
* {@link https://www.prisma.io/docs/concepts/components/prisma-client/distinct Distinct Docs}
*
* Filter by unique combinations of UnifiSites.
*/
distinct?: Prisma.UnifiSiteScalarFieldEnum | Prisma.UnifiSiteScalarFieldEnum[] distinct?: Prisma.UnifiSiteScalarFieldEnum | Prisma.UnifiSiteScalarFieldEnum[]
} }
File diff suppressed because it is too large Load Diff
+2 -1
View File
@@ -29,6 +29,7 @@
"utils:gen_private_keys": "bun ./utils/genPrivateKeys", "utils:gen_private_keys": "bun ./utils/genPrivateKeys",
"utils:create_admin_role": "bun ./utils/createAdminRole", "utils:create_admin_role": "bun ./utils/createAdminRole",
"utils:assign_user_role": "bun ./utils/assignUserRole", "utils:assign_user_role": "bun ./utils/assignUserRole",
"utils:gen_access_token": "bun ./utils/generate24HourAccessToken.ts",
"utils:test_webserver": "bun ./utils/testWebserver.ts", "utils:test_webserver": "bun ./utils/testWebserver.ts",
"utils:test_adjustments_poll": "bun ./utils/testAdjustmentsPoll.ts", "utils:test_adjustments_poll": "bun ./utils/testAdjustmentsPoll.ts",
"utils:analyze_cw": "python3 debug-scripts/analyze-cw-calls.py", "utils:analyze_cw": "python3 debug-scripts/analyze-cw-calls.py",
@@ -45,6 +46,7 @@
"blakets": "^0.1.12", "blakets": "^0.1.12",
"cors": "^2.8.6", "cors": "^2.8.6",
"cuid": "^3.0.0", "cuid": "^3.0.0",
"dalpuri": "workspace:*",
"hono": "^4.11.5", "hono": "^4.11.5",
"ioredis": "^5.10.0", "ioredis": "^5.10.0",
"jsonwebtoken": "^9.0.3", "jsonwebtoken": "^9.0.3",
@@ -54,7 +56,6 @@
"pg-boss": "^12.14.0", "pg-boss": "^12.14.0",
"prisma": "^7.3.0", "prisma": "^7.3.0",
"socket.io": "^4.8.3", "socket.io": "^4.8.3",
"socket.io-client": "^4.8.3",
"zod": "^4.3.6", "zod": "^4.3.6",
"zon": "^1.0.3" "zon": "^1.0.3"
} }
@@ -0,0 +1,2 @@
-- Rename the misspelled column serverityId -> severityId on ServiceTicket
ALTER TABLE "ServiceTicket" RENAME COLUMN "serverityId" TO "severityId";
@@ -0,0 +1,41 @@
-- CreateEnum
CREATE TYPE "SyncJobType" AS ENUM ('FULL_SYNC', 'INCREMENTAL_SYNC');
-- CreateEnum
CREATE TYPE "SyncJobStatus" AS ENUM ('QUEUED', 'RUNNING', 'COMPLETED', 'FAILED', 'TIMED_OUT');
-- CreateTable
CREATE TABLE "SyncJobRun" (
"id" TEXT NOT NULL,
"jobType" "SyncJobType" NOT NULL,
"status" "SyncJobStatus" NOT NULL DEFAULT 'QUEUED',
"triggeredBy" TEXT NOT NULL DEFAULT 'system',
"startedAt" TIMESTAMP(3),
"completedAt" TIMESTAMP(3),
"errorSummary" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "SyncJobRun_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "SyncStepLog" (
"id" TEXT NOT NULL,
"syncJobRunId" TEXT NOT NULL,
"tableName" TEXT NOT NULL,
"syncMode" TEXT NOT NULL,
"recordsProcessed" INTEGER NOT NULL DEFAULT 0,
"recordsInserted" INTEGER NOT NULL DEFAULT 0,
"recordsSkipped" INTEGER NOT NULL DEFAULT 0,
"recordsFailed" INTEGER NOT NULL DEFAULT 0,
"recordsDeleted" INTEGER NOT NULL DEFAULT 0,
"sampleErrors" JSONB NOT NULL DEFAULT '[]',
"durationMs" INTEGER NOT NULL DEFAULT 0,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "SyncStepLog_pkey" PRIMARY KEY ("id")
);
-- AddForeignKey
ALTER TABLE "SyncStepLog" ADD CONSTRAINT "SyncStepLog_syncJobRunId_fkey" FOREIGN KEY ("syncJobRunId") REFERENCES "SyncJobRun"("id") ON DELETE CASCADE ON UPDATE CASCADE;
@@ -0,0 +1,47 @@
-- CreateTable (idempotent)
CREATE TABLE IF NOT EXISTS "OpportunityStage" (
"uid" TEXT NOT NULL,
"id" INTEGER NOT NULL,
"name" TEXT NOT NULL,
"seqNbr" INTEGER,
"funnelColor" TEXT,
"updatedById" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "OpportunityStage_pkey" PRIMARY KEY ("uid")
);
-- CreateIndex (idempotent)
CREATE UNIQUE INDEX IF NOT EXISTS "OpportunityStage_id_key" ON "OpportunityStage"("id");
-- AlterTable: drop old columns if they exist, add stageId if it doesn't
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'Opportunity' AND column_name = 'stageName') THEN
ALTER TABLE "Opportunity" DROP COLUMN "stageName";
END IF;
IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'Opportunity' AND column_name = 'stageCwId') THEN
ALTER TABLE "Opportunity" DROP COLUMN "stageCwId";
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'Opportunity' AND column_name = 'stageId') THEN
ALTER TABLE "Opportunity" ADD COLUMN "stageId" INTEGER;
END IF;
END $$;
-- Nullify any stageId values that reference non-existent OpportunityStage rows
UPDATE "Opportunity" SET "stageId" = NULL
WHERE "stageId" IS NOT NULL
AND "stageId" NOT IN (SELECT "id" FROM "OpportunityStage");
-- AddForeignKey (skip if already exists)
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.table_constraints
WHERE constraint_name = 'Opportunity_stageId_fkey'
) THEN
ALTER TABLE "Opportunity" ADD CONSTRAINT "Opportunity_stageId_fkey"
FOREIGN KEY ("stageId") REFERENCES "OpportunityStage"("id") ON DELETE SET NULL ON UPDATE CASCADE;
END IF;
END $$;
+783 -15
View File
@@ -27,6 +27,18 @@ enum FaxType {
SITE // Main site fax line, not direct to contact SITE // Main site fax line, not direct to contact
} }
enum BillingMethod {
ACTUAL_RATES
FIXED_FEE
NOT_TO_EXCEED // Requires "Bill Ticket Seperately"
OVERRIDE_RATE // Shows hourly rate field
}
enum BillingType {
STANDARD
PROJECT
}
// By human nature, there are only two genders. // By human nature, there are only two genders.
enum GenderType { enum GenderType {
MALE MALE
@@ -96,6 +108,59 @@ enum OpportunityInterest {
COLD COLD
} }
// ---- Sync Job Tracking ----
enum SyncJobType {
FULL_SYNC
INCREMENTAL_SYNC
}
enum SyncJobStatus {
QUEUED
RUNNING
COMPLETED
FAILED
TIMED_OUT
}
model SyncJobRun {
id String @id @default(uuid())
jobType SyncJobType
status SyncJobStatus @default(QUEUED)
triggeredBy String @default("system")
startedAt DateTime?
completedAt DateTime?
errorSummary String?
steps SyncStepLog[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model SyncStepLog {
id String @id @default(uuid())
syncJobRunId String
syncJobRun SyncJobRun @relation(fields: [syncJobRunId], references: [id], onDelete: Cascade)
tableName String
syncMode String // "full" or "incremental"
recordsProcessed Int @default(0)
recordsInserted Int @default(0)
recordsSkipped Int @default(0)
recordsFailed Int @default(0)
recordsDeleted Int @default(0)
sampleErrors Json @default("[]")
durationMs Int @default(0)
createdAt DateTime @default(now())
}
model Session { model Session {
id String @id @default(uuid()) id String @id @default(uuid())
sessionKey String @unique @default(cuid()) sessionKey String @unique @default(cuid())
@@ -108,21 +173,25 @@ model Session {
} }
model User { model User {
id String @id @default(cuid()) id String @id @default(uuid())
roles Role[] roles Role[]
permissions String? permissions String?
login String @unique login String @unique
name String? firstName String?
lastName String?
email String @unique email String @unique
emailVerified DateTime?
image String? image String?
title String?
active Boolean @default(true) active Boolean @default(true)
hidden Boolean @default(false) hidden Boolean @default(false)
cwIdentifier String? @unique cwIdentifier String? @unique
cwMemberId Int? @unique
cwMember CwMember? @relation(fields: [cwMemberId], references: [cwMemberId])
userId String @unique userId String? @unique
token String? token String?
sessions Session[] sessions Session[]
@@ -136,8 +205,16 @@ model User {
generatedQuotes GeneratedQuotes[] generatedQuotes GeneratedQuotes[]
companyAddresses CompanyAddress[] companyAddresses CompanyAddress[]
updatedBy String?
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
serviceTicketsOwned ServiceTicket[] @relation("ServiceTicketOwner")
serviceTicketsClosed ServiceTicket[] @relation("ServiceTicketClosedBy")
serviceTicketsCreated ServiceTicket[] @relation("ServiceTicketCreatedBy")
serviceTicketsUpdated ServiceTicket[] @relation("ServiceTicketUpdatedBy")
serviceTicketNotes ServiceTicketNote[] @relation("ServiceTicketNoteAuthor")
} }
model Role { model Role {
@@ -171,6 +248,8 @@ model CorporateLocation {
inactiveFlag Boolean @default(false) // Optima Only field, not synced to CW inactiveFlag Boolean @default(false) // Optima Only field, not synced to CW
opportunities Opportunity[] opportunities Opportunity[]
serviceBoards ServiceTicketBoard[]
productDataRecords ProductData[]
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
@@ -232,11 +311,13 @@ model Company {
deletedBy User? @relation("DeletedBy", fields: [deletedById], references: [cwIdentifier]) deletedBy User? @relation("DeletedBy", fields: [deletedById], references: [cwIdentifier])
enteredBy User? @relation("EnteredBy", fields: [enteredById], references: [cwIdentifier]) enteredBy User? @relation("EnteredBy", fields: [enteredById], references: [cwIdentifier])
enteredAt DateTime @default(now())
deletedAt DateTime? deletedAt DateTime?
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
serviceTickets ServiceTicket[]
billingServiceTickets ServiceTicket[] @relation("BillingCompany")
} }
model CompanyAddress { model CompanyAddress {
@@ -270,6 +351,8 @@ model CompanyAddress {
contacts Contact[] contacts Contact[]
oppportunities Opportunity[] oppportunities Opportunity[]
serviceTickets ServiceTicket[]
billingTickets ServiceTicket[] @relation("BillingAddress")
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
@@ -304,29 +387,190 @@ model Contact {
company Company? @relation(fields: [companyId], references: [id]) company Company? @relation(fields: [companyId], references: [id])
opportunities Opportunity[] opportunities Opportunity[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
serviceTickets ServiceTicket[]
}
model CatalogItemType {
id Int @unique
uid String @id @default(uuid())
name String
description String? // Optima Only field, not synced to CW
inactiveFlag Boolean @default(false)
defaultFlag Boolean @default(false)
updatedById String?
createdById String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model CatalogCategory {
id Int @unique
uid String @id @default(uuid())
name String
description String? // Optima Only field, not synced to CW
inactiveFlag Boolean @default(false)
catalogSubcategories CatalogSubcategory[]
updatedById String?
createdById String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model CatalogSubcategory {
id Int @unique
uid String @id @default(uuid())
name String
description String? // Optima Only field, not synced to CW
categoryId Int
category CatalogCategory @relation(fields: [categoryId], references: [id])
items CatalogItem[]
inactiveFlag Boolean @default(false)
updatedById String?
createdById String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model CatalogManufacturer {
id Int @unique
uid String @id @default(uuid())
name String
description String? // Optima Only field, not synced to CW
inactiveFlag Boolean @default(false)
items CatalogItem[]
updatedById String?
createdById String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model WarehouseBin {
id Int @unique
uid String @id @default(uuid())
name String
description String? // Optima Only field, not synced to CW
minQuantity Int
maxQuantity Int
inactiveFlag Boolean @default(false)
defaultFlag Boolean @default(false)
proeductInventories ProductInventory[]
warehouse Warehouse? @relation(fields: [warehouseId], references: [id])
warehouseId Int?
updatedById String?
createdById String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model ProductInventory {
id Int @unique
uid String @id @default(uuid())
qtyOnHand Int @default(0)
warehouseBinId Int
warehouseBin WarehouseBin @relation(fields: [warehouseBinId], references: [id])
itemId Int?
item CatalogItem? @relation(fields: [itemId], references: [id])
warehouse Warehouse? @relation(fields: [warehouseId], references: [id])
warehouseId Int?
updatedById String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Warehouse {
id Int @unique
uid String @id @default(uuid())
name String
description String? // Optima Only field, not synced to CW
inactiveFlag Boolean @default(false)
lockedFlag Boolean @default(false)
minimumStock MinimumStockByWarehouse[]
inventory ProductInventory[]
bins WarehouseBin[]
updatedById String?
createdById String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model MinimumStockByWarehouse {
id Int @unique
uid String @id @default(uuid())
minQty Int @default(0)
warehouseId Int
warehouse Warehouse @relation(fields: [warehouseId], references: [id])
itemId Int?
item CatalogItem? @relation(fields: [itemId], references: [id])
updatedById String?
enteredById String?
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
} }
model CatalogItem { model CatalogItem {
id String @id @default(cuid()) id Int @unique
cwCatalogId Int @unique uid String @id @default(uuid())
identifier String? @unique identifier String? @unique
name String name String
description String? description String?
customerDescription String? customerDescription String?
internalNotes String? internalNotes String?
linkedItems CatalogItem[] @relation("LinkedItems") linkedItems CatalogItem[] @relation("LinkedItems")
linkedTo CatalogItem[] @relation("LinkedItems") linkedTo CatalogItem[] @relation("LinkedItems")
category String? subcategoryId Int
categoryCwId Int? subcategory CatalogSubcategory @relation(fields: [subcategoryId], references: [id])
subcategory String?
subcategoryCwId Int?
manufacturer String? manufacturerId Int?
manufactureCwId Int? manufacturer CatalogManufacturer? @relation(fields: [manufacturerId], references: [id])
partNumber String? partNumber String?
@@ -337,12 +581,403 @@ model CatalogItem {
price Float price Float
cost Float cost Float
inventory ProductInventory[]
minimumStockByWarehouses MinimumStockByWarehouse[]
productDataRecords ProductData[]
inactive Boolean @default(false) inactive Boolean @default(false)
salesTaxable Boolean @default(true) salesTaxable Boolean @default(true)
onHand Int @default(0) onHand Int @default(0)
cwLastUpdated DateTime? cwLastUpdated DateTime?
// IV_Class_ID from CW: 'S' = Service/Labor, 'I' = Inventory, 'N' = Non-Inventory
classId String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model ProductData {
id Int @unique
uid String @id @default(uuid())
qty Float @default(1)
internalNote String?
shortDescription String?
description String?
sequenceNumber Int? // This is the sequence number of the product on the ticket, which may be different from the sequence number of the product in the catalog, and is important for maintaining the order of products on the ticket as they were added.
procurementNotes String?
productNarrative String?
unitPrice Float @default(0)
unitCost Float @default(0)
listPrice Float @default(0) // The original price of the product before any discounts or promotions are applied, which may be different from the unit price if there are any overrides or promotions applied at the ticket level.
discount Float @default(0) // The discount amount applied to this product, which may be different from the discount on the catalog item if there are any overrides or promotions applied at the ticket level.
recurringRevenue Float @default(0) // For subscription products, the recurring revenue amount, which may be different from the unit price if there are any discounts or promotions applied.
recurringCost Float @default(0) // For subscription products, the recurring cost amount, which may be different from the unit cost if there are any discounts or promotions applied.
qtyPicked Int @default(0) // How many of this product have been taken out of inventory
qtyShipped Int @default(0) // How many of this product have been recieved by the customer
cancelReason String? // If this product was canceled or removed from the ticket after being added, what was the reason for that?
cancelQty Float? // If this product was canceled or removed from the ticket after being added, how many were canceled or removed?
// ------ Flag Fields ------
billableFlag Boolean @default(true)
taxableFlag Boolean @default(true)
invoiceFlag Boolean @default(true)
recurringFlag Boolean @default(false) // Is this product a subscription or recurring revenue product?
poApprovedFlag Boolean @default(false) // Was the purchase of this product approved as part of a purchase order process?
calcPriceFlag Boolean @default(true) // Should the price of this product be calculated based on the catalog item price and any overrides, or should it use the unitPrice as is?
calcCostFlag Boolean @default(true) // Should the cost of this product be calculated based on the catalog item cost and any overrides, or should it use the unitCost as is?
cancelFlag Boolean @default(false) // Has this product been canceled or removed from the ticket after being added, but we want to keep the record of it for historical and reporting purposes?
// ------ Relational Fields ------
catalogItemId Int
corporateLocationId Int
serviceTicketId Int?
opportunityId Int?
serviceTicket ServiceTicket? @relation(fields: [serviceTicketId], references: [id])
opportunity Opportunity? @relation(fields: [opportunityId], references: [id])
catalogItem CatalogItem @relation(fields: [catalogItemId], references: [id])
corporateLocation CorporateLocation @relation(fields: [corporateLocationId], references: [id])
updatedById String?
createdById String?
closedById String?
cancelById String
closedAt DateTime?
cancelledAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
//
// SERVICE TICKETS
//
model ServiceTicket {
id Int @unique
uid String @id @default(uuid())
// ------ Core ticket fields ------
summary String
notes ServiceTicketNote[]
addressLine1 String?
addressLine2 String?
city String?
state USState?
zipCode String?
country Country?
contactName String?
phone String?
phoneExtension String?
phoneType PhoneType?
email String?
// ------ Billing and invoicing fields ------
products ProductData[] // The products used on this ticket, which may be important for billing and invoicing, as well as reporting and analytics.
poNumber String?
billCompleteFlag Boolean @default(false) // Bill after ticket is closed, not allowing any billing while open.
billUnapprovedFlag Boolean @default(false) // Can this ticket bill unapproved work or expenses?
billingAmount Float @default(0.00)
billingMethod BillingMethod @default(ACTUAL_RATES)
timeBillableFlag Boolean @default(true) // Is the time spent on this ticket billable?
expenseBillableFlag Boolean @default(true) // Are the expenses incurred on this ticket billable?
productBillableFlag Boolean @default(true) // Are the products used on this ticket billable?
timeInvoiceableFlag Boolean @default(true) // Should the billable time on this ticket be included on invoices?
expenseInvoiceableFlag Boolean @default(true) // Should the billable expenses on this ticket be included on invoices?
productInvoiceableFlag Boolean @default(true) // Should the billable products on this ticket be included on invoices?
dateRequested DateTime? // The date the customer requested service, which may be different from the date the ticket was created in the system.
billingType BillingType @default(STANDARD) // (CUSTOM FIELD) Standard billing or project billing, which may have different rules for how the ticket is billed.
billingInstructions String? // (CUSTOM FIELD) Any special instructions for billing this ticket, which may be important for project billing or non-standard billing arrangements.
// ------ Flag Fields ------
rejectedFlag Boolean @default(false) // More used for denoting if a ticket was rejected by some automation.
closedFlag Boolean @default(false)
redFlag Boolean @default(false) // Carry over from CW, used for visibility and filtering.
publishFlag Boolean @default(false) // Should this ticket be visible to the customer in the portal or any other means?
// ------ Relational Fields ------
ticketOwnerId String?
serviceTicketBoardId Int?
severityId Int
impactId Int
priorityId Int
sourceId Int
locationId Int
parentId Int?
companyId Int?
contactId Int?
companyAddressId Int?
billingCompanyId Int?
billingAddressId Int?
severity ServiceTicketSeverity @relation(fields: [severityId], references: [id])
impact ServiceTicketImpact @relation(fields: [impactId], references: [id])
priority ServiceTicketPriority @relation(fields: [priorityId], references: [id])
source ServiceTicketSource @relation(fields: [sourceId], references: [id])
location ServiceTicketLocation @relation(fields: [locationId], references: [id])
serviceTicketBoard ServiceTicketBoard? @relation(fields: [serviceTicketBoardId], references: [id])
ticketOwner User? @relation("ServiceTicketOwner", fields: [ticketOwnerId], references: [cwIdentifier])
company Company? @relation(fields: [companyId], references: [id])
contact Contact? @relation(fields: [contactId], references: [id])
companyAddress CompanyAddress? @relation(fields: [companyAddressId], references: [id])
billingCompany Company? @relation("BillingCompany", fields: [billingCompanyId], references: [id])
billingAddress CompanyAddress? @relation("BillingAddress", fields: [billingAddressId], references: [id])
// ------ Audit Fields ------
createdById String?
updatedById String?
closedById String?
closedBy User? @relation("ServiceTicketClosedBy", fields: [closedById], references: [id])
createdBy User? @relation("ServiceTicketCreatedBy", fields: [createdById], references: [id])
updatedBy User? @relation("ServiceTicketUpdatedBy", fields: [updatedById], references: [id])
rejectedAt DateTime?
closedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model ServiceTicketNote {
id Int @unique
uid String @id @default(uuid())
notes String
notesMd String
authorId String
author User @relation("ServiceTicketNoteAuthor", fields: [authorId], references: [id])
problemFlag Boolean @default(false) // Is this note describing the problem?
resolutionFlag Boolean @default(false) // Is this note describing the resolution?
internalAnalysisFlag Boolean @default(false) // Is this note describing the internal analysis of the issue, such as root cause analysis or technical details that may not be relevant to the customer?
internalMemberFlag Boolean @default(false) // Is this note meant to be seen by internal team members only, not visible to the customer?
createdByParentFlag Boolean @default(false) // Is this note created by the parent entity.
mergedFlag Boolean @default(false)
bundledFlag Boolean @default(false)
serviceTicketId Int
serviceTicket ServiceTicket @relation(fields: [serviceTicketId], references: [id])
createdById String?
updatedById String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model ServiceTicketType {
id Int @unique
uid String @id @default(uuid())
name String
description String? // Optima Only field, not synced to CW
inactiveFlag Boolean @default(false)
updatedById String?
createdById String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model ServiceTicketBoard {
id Int @unique
uid String @id @default(uuid())
name String
// Does this generate revenue for the company?
timeBillableFlag Boolean @default(false)
expenseBillableFlag Boolean @default(false)
productBillableFlag Boolean @default(false)
// Can the customer see this on their invoice?
timeInvoiceableFlag Boolean @default(false)
expenseInvoiceableFlag Boolean @default(false)
productInvoiceableFlag Boolean @default(false)
// These are auto assignment rule flags
// If/When I implement a system for auto assignement,
// these flags will determine which fields are considered
// for auto assignment when a ticket is created without an assignee.
// These are a carry over from CW.
autoAssignNewFlag Boolean @default(false)
autoAssignEmailCreatedFlag Boolean @default(false)
autoAssignPortalCreatedFlag Boolean @default(false)
projectFlag Boolean @default(false)
lockDescriptionFlag Boolean @default(false) // Should we lock the description field on tickets in this board after creation?
emailContactFlag Boolean @default(false) // Should we email the contact when a ticket is updated?
emailResourceFlag Boolean @default(false) // Should we email the assigned resource(s) when a ticket is updated?
resolutionSortOrder String @default("D") @db.Char(1)
internalAnalysisSortOrder String @default("D") @db.Char(1)
locationId Int
location CorporateLocation @relation(fields: [locationId], references: [id])
serviceTickets ServiceTicket[]
createdById String?
updatedById String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model ServiceTicketLocation {
id Int @unique
uid String @id @default(uuid())
name String
description String? // Optima Only field, not synced to CW
defaultFlag Boolean @default(false)
serviceTickets ServiceTicket[]
updatedById String?
createdById String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model ServiceTicketSource {
id Int @unique
uid String @id @default(uuid())
name String
description String? // Optima Only field, not synced to CW
inactiveFlag Boolean @default(false)
defaultFlag Boolean @default(false)
serviceTickets ServiceTicket[]
updatedById String?
createdById String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
// How much does the issue affect the customer or their business operations
model ServiceTicketImpact {
id Int @unique
uid String @id @default(uuid())
name String
description String?
defaultFlag Boolean @default(false)
serviceTickets ServiceTicket[]
updatedById String?
createdById String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
// How soon does the issue need to be addressed/resolved
model ServiceTicketPriority {
id Int @unique
uid String @id @default(uuid())
name String
color String? // Hex color code for priority, e.g. "#FF0000" for red
description String? // Optima Only field, not synced to CW
defaultFlag Boolean @default(false)
serviceTickets ServiceTicket[]
updatedById String?
createdById String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
// How bad is the Technical Issue
model ServiceTicketSeverity {
id Int @unique
uid String @id @default(uuid())
name String
description String?
defaultFlag Boolean @default(false)
serviceTickets ServiceTicket[]
updatedById String?
createdById String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@map("ServiceTicketServerity")
}
// This data is populated asynchronously after a ticket is closed,
// so we can keep the critical path of closing a ticket fast and not
// dependent on any additional data processing. This will also allow
// us to be able to store the data AS IT was at the time of closing,
// without worrying about any additional updates that may come in after the fact.
model ServiceTicketFinalData {
id String @id @default(uuid())
}
model OpportunityStage {
id Int @unique
uid String @id @default(uuid())
name String
seqNbr Int?
funnelColor String?
updatedById String?
opportunities Opportunity[]
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
} }
@@ -393,17 +1028,22 @@ model Opportunity {
name String name String
notes String? notes String?
oppNarrative String? // A long form text field for the story of the opportunity, which may include details about the customer's needs, the proposed solution, and any other relevant information that doesn't fit into the structured fields.
generatedQuotes GeneratedQuotes[] generatedQuotes GeneratedQuotes[]
typeId Int typeId Int
type OpportunityType @relation(fields: [typeId], references: [id]) type OpportunityType @relation(fields: [typeId], references: [id])
stageName String? stageId Int?
stageCwId Int? stage OpportunityStage? @relation(fields: [stageId], references: [id])
statusId Int? statusId Int?
status OpportunityStatus? @relation(fields: [statusId], references: [id]) status OpportunityStatus? @relation(fields: [statusId], references: [id])
taxCodeId Int?
taxCode TaxCode? @relation(fields: [taxCodeId], references: [id])
interest OpportunityInterest? interest OpportunityInterest?
probability Float @default(0) probability Float @default(0)
@@ -447,6 +1087,111 @@ model Opportunity {
// When present, fetchProducts() uses this order instead of CW sequenceNumber. // When present, fetchProducts() uses this order instead of CW sequenceNumber.
productSequence Int[] @default([]) productSequence Int[] @default([])
products ProductData[]
updatedBy String
eneteredBy String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
// ------ Schedule / Calendar --------
model ScheduleStatus {
id Int @unique
uid String @id @default(uuid())
name String
description String? // Optima Only field, not synced to CW
color String?
softFlag Boolean @default(false)
defaultFlag Boolean @default(false)
schedules Schedule[]
updatedById String?
createdById String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model ScheduleType {
id Int @unique
uid String @id @default(uuid())
name String
description String? // Optima Only field, not synced to CW
displayColor String?
tableReference String?
moduleId String? @db.Char(2)
scheduleTypeId String? @db.Char(1)
systemFlag Boolean @default(false)
displayFlag Boolean @default(false)
schedules Schedule[]
updatedById String?
createdById String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model ScheduleSpan {
id Int @id @default(autoincrement())
scheduleSpanId String? @db.Char(1)
spanDesc String? @db.Char(20)
schedules Schedule[]
}
model Schedule {
id Int @unique
uid String @id @default(uuid())
name String
description String?
memberId String?
closedFlag Boolean @default(false)
reminderFlag Boolean @default(false)
allDayFlag Boolean @default(false)
acknowledgementFlag Boolean @default(false)
meetingFlag Boolean @default(false)
recurringFlag Boolean @default(false)
billableFlag Boolean @default(false)
acknowledgedById String?
acknowledgedAt DateTime?
startDate DateTime?
endDate DateTime?
hoursScheduled Float?
duration Int? // The number of days in between the start and end date.
hoursPerDay Float?
reminderMinutes Int? @default(15)
statusId Int?
status ScheduleStatus? @relation(fields: [statusId], references: [id])
typeId Int?
type ScheduleType? @relation(fields: [typeId], references: [id])
scheduleSpanId Int?
scheduleSpan ScheduleSpan? @relation(fields: [scheduleSpanId], references: [id])
updatedById String?
createdById String?
closedById String?
closedAt DateTime?
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
} }
@@ -523,6 +1268,27 @@ model GeneratedQuotes {
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
} }
model TaxCode {
id Int @unique
uid String @id @default(uuid())
opportunities Opportunity[]
code String @unique
codeCaption String
description String?
rate Float?
defaultFlag Boolean @default(false)
createdBy String
updatedBy String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model CwMember { model CwMember {
id String @id @default(cuid()) id String @id @default(cuid())
@@ -535,6 +1301,8 @@ model CwMember {
apiKey String? apiKey String?
user User?
cwLastUpdated DateTime? cwLastUpdated DateTime?
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
+24
View File
@@ -0,0 +1,24 @@
import { createRoute } from "../../modules/api-utils/createRoute";
import { redis } from "../../constants";
/* /v1/auth/callback/:callbackKey */
export default createRoute("get", ["/callback/:callbackKey"], async (c) => {
const callbackKey = c.req.param("callbackKey");
if (!callbackKey) {
c.status(400);
return c.json({ status: 400, message: "Missing callbackKey", successful: false });
}
const redisKey = `auth:cb:${callbackKey}`;
const raw = await redis.get(redisKey);
if (!raw) {
c.status(202);
return c.json({ status: 202, message: "Pending", successful: false });
}
// Delete immediately so tokens can't be replayed
await redis.del(redisKey);
const tokens = JSON.parse(raw) as { accessToken: string; refreshToken: string };
return c.json({ status: 200, message: "Auth callback resolved", data: tokens, successful: true });
});
+1
View File
@@ -1,3 +1,4 @@
export { default as redirect } from "./redirect"; export { default as redirect } from "./redirect";
export { default as refresh } from "./refresh"; export { default as refresh } from "./refresh";
export { default as uri } from "./uri"; export { default as uri } from "./uri";
export { default as callback } from "./callback";
+10 -12
View File
@@ -1,9 +1,11 @@
import { Hono } from "hono/tiny"; import { Hono } from "hono/tiny";
import { createRoute } from "../../modules/api-utils/createRoute"; import { createRoute } from "../../modules/api-utils/createRoute";
import * as msal from "@azure/msal-node"; import * as msal from "@azure/msal-node";
import { API_BASE_URL, io, msalClient } from "../../constants"; import { API_BASE_URL, msalClient, redis } from "../../constants";
import { users } from "../../managers/users"; import { users } from "../../managers/users";
const AUTH_CALLBACK_TTL_SECONDS = 300; // 5 minutes
/* /v1/auth/redirect */ /* /v1/auth/redirect */
export default createRoute("get", ["/redirect"], async (c) => { export default createRoute("get", ["/redirect"], async (c) => {
c.status(200); c.status(200);
@@ -18,10 +20,13 @@ export default createRoute("get", ["/redirect"], async (c) => {
const callbackKey = c.req.query().state as string; const callbackKey = c.req.query().state as string;
const tokens = await users.authenticate(authResult); const tokens = await users.authenticate(authResult);
io.of(`/auth_callback`).emit(`auth:login:callback:${callbackKey}`, { // Store tokens in Redis so the UI can poll for them
accessToken: tokens.accessToken, await redis.set(
refreshToken: tokens.refreshToken, `auth:cb:${callbackKey}`,
}); JSON.stringify({ accessToken: tokens.accessToken, refreshToken: tokens.refreshToken }),
"EX",
AUTH_CALLBACK_TTL_SECONDS,
);
// Close the window because duh // Close the window because duh
return c.html( return c.html(
@@ -29,11 +34,4 @@ export default createRoute("get", ["/redirect"], async (c) => {
window.close(); window.close();
</script>`, </script>`,
); );
return c.json({
status: 200,
message: "Auth Redirect Endpoint",
data: authResult,
successful: true,
});
}); });
+16 -27
View File
@@ -4,7 +4,6 @@ import { companies } from "../../../managers/companies";
import { apiResponse } from "../../../modules/api-utils/apiResponse"; import { apiResponse } from "../../../modules/api-utils/apiResponse";
import { ContentfulStatusCode } from "hono/utils/http-status"; import { ContentfulStatusCode } from "hono/utils/http-status";
import { authMiddleware } from "../../middleware/authorization"; import { authMiddleware } from "../../middleware/authorization";
import GenericError from "../../../Errors/GenericError";
import { processObjectValuePerms } from "../../../modules/permission-utils/processObjectPermissions"; import { processObjectValuePerms } from "../../../modules/permission-utils/processObjectPermissions";
/* /v1/company/companies/[id] */ /* /v1/company/companies/[id] */
@@ -14,34 +13,17 @@ export default createRoute(
async (c) => { async (c) => {
const company = await companies.fetch(c.req.param("identifier")); const company = await companies.fetch(c.req.param("identifier"));
const includeAddress = c.req.query("includeAddress") === "true"; const user = c.get("user");
const includeAddress =
c.req.query("includeAddress") === "true" &&
!!user &&
(await user.hasPermission("company.fetch.address"));
const includePrimaryContact = const includePrimaryContact =
c.req.query("includePrimaryContact") === "true"; c.req.query("includePrimaryContact") === "true";
const includeAllContacts = c.req.query("includeAllContacts") === "true"; const includeAllContacts =
c.req.query("includeAllContacts") === "true" &&
// Check for address-specific permission if includeAddress is requested !!user &&
if (includeAddress) { (await user.hasPermission("company.fetch.contacts"));
const user = c.get("user");
if (!user || !(await user.hasPermission("company.fetch.address"))) {
throw new GenericError({
name: "InsufficientPermission",
message: "You do not have permission to view company addresses.",
status: 403,
});
}
}
// Check for contacts permission if includeAllContacts is requested
if (includeAllContacts) {
const user = c.get("user");
if (!user || !(await user.hasPermission("company.fetch.contacts"))) {
throw new GenericError({
name: "InsufficientPermission",
message: "You do not have permission to view company contacts.",
status: 403,
});
}
}
const companyData = company.toJson({ const companyData = company.toJson({
includeAddress, includeAddress,
@@ -54,6 +36,13 @@ export default createRoute(
c.get("user"), c.get("user"),
); );
// cw_Data fields were already gated by the explicit permission checks above
// (company.fetch.contacts / company.fetch.address). Re-attach them so they
// are not silently dropped by field-level gating on obj.company.cw_Data.
if (companyData.cw_Data && Object.keys(companyData.cw_Data).length > 0) {
(gatedData as any).cw_Data = companyData.cw_Data;
}
const response = apiResponse.successful( const response = apiResponse.successful(
"Company Fetched Successfully!", "Company Fetched Successfully!",
gatedData, gatedData,
+10 -11
View File
@@ -2,21 +2,22 @@ import { createRoute } from "../../modules/api-utils/createRoute";
import { apiResponse } from "../../modules/api-utils/apiResponse"; import { apiResponse } from "../../modules/api-utils/apiResponse";
import { ContentfulStatusCode } from "hono/utils/http-status"; import { ContentfulStatusCode } from "hono/utils/http-status";
import { authMiddleware } from "../middleware/authorization"; import { authMiddleware } from "../middleware/authorization";
import { getMemberCache } from "../../modules/cw-utils/members/memberCache"; import { prisma } from "../../constants";
/* GET /v1/cw/members */ /* GET /v1/cw/members */
export default createRoute( export default createRoute(
"get", "get",
["/members"], ["/members"],
async (c) => { async (c) => {
const cache = await getMemberCache();
const activeOnly = c.req.query("active") !== "false"; const activeOnly = c.req.query("active") !== "false";
const members = cache const dbMembers = await prisma.cwMember.findMany({
.filter((m) => (activeOnly ? !m.inactiveFlag : true)) where: activeOnly ? { inactiveFlag: false } : undefined,
.map((m) => ({ orderBy: [{ firstName: "asc" }, { lastName: "asc" }],
id: m.id, });
const members = dbMembers.map((m) => ({
id: m.cwMemberId,
identifier: m.identifier, identifier: m.identifier,
firstName: m.firstName, firstName: m.firstName,
lastName: m.lastName, lastName: m.lastName,
@@ -25,13 +26,11 @@ export default createRoute(
inactive: m.inactiveFlag, inactive: m.inactiveFlag,
})); }));
const sorted = members.sort((a, b) => a.name.localeCompare(b.name));
const response = apiResponse.successful( const response = apiResponse.successful(
"CW members fetched successfully!", "CW members fetched successfully!",
sorted, members
); );
return c.json(response, response.status as ContentfulStatusCode); return c.json(response, response.status as ContentfulStatusCode);
}, },
authMiddleware(), authMiddleware()
); );
+3 -1
View File
@@ -1,4 +1,6 @@
import { default as callback } from "./callback"; import { default as callback } from "./callback";
import { default as fetchMembers } from "./fetchMembers"; import { default as fetchMembers } from "./fetchMembers";
import { default as syncFull } from "./sync";
import { syncStatus, syncStatusById, syncHistory } from "./sync-status";
export { callback, fetchMembers }; export { callback, fetchMembers, syncFull, syncStatus, syncStatusById, syncHistory };
+84
View File
@@ -0,0 +1,84 @@
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 { prisma } from "../../constants";
/* GET /v1/cw/sync/status — latest sync job run with step logs */
export const syncStatus = createRoute(
"get",
["/sync/status"],
async (c) => {
const latest = await prisma.syncJobRun.findFirst({
orderBy: { createdAt: "desc" },
include: {
steps: {
orderBy: { createdAt: "asc" },
},
},
});
if (!latest) {
const response = apiResponse.successful("No sync runs found", null);
return c.json(response, response.status as ContentfulStatusCode);
}
const response = apiResponse.successful("Latest sync run", latest);
return c.json(response, response.status as ContentfulStatusCode);
},
authMiddleware()
);
/* GET /v1/cw/sync/status/:jobId — specific job details */
export const syncStatusById = createRoute(
"get",
["/sync/status/:jobId"],
async (c) => {
const jobId = c.req.param("jobId");
const run = await prisma.syncJobRun.findUnique({
where: { id: jobId },
include: {
steps: {
orderBy: { createdAt: "asc" },
},
},
});
if (!run) {
const response = apiResponse.notFound("Sync run not found");
return c.json(response, response.status as ContentfulStatusCode);
}
const response = apiResponse.successful("Sync run details", run);
return c.json(response, response.status as ContentfulStatusCode);
},
authMiddleware()
);
/* GET /v1/cw/sync/history — last N sync runs */
export const syncHistory = createRoute(
"get",
["/sync/history"],
async (c) => {
const limitParam = c.req.query("limit");
const limit = Math.min(
Math.max(Number.parseInt(limitParam || "20", 10) || 20, 1),
100
);
const runs = await prisma.syncJobRun.findMany({
orderBy: { createdAt: "desc" },
take: limit,
include: {
_count: {
select: { steps: true },
},
},
});
const response = apiResponse.successful("Sync history", runs);
return c.json(response, response.status as ContentfulStatusCode);
},
authMiddleware()
);
+26
View File
@@ -0,0 +1,26 @@
import { createRoute } from "../../modules/api-utils/createRoute";
import { apiResponse } from "../../modules/api-utils/apiResponse";
import { ContentfulStatusCode } from "hono/utils/http-status";
import { authMiddleware } from "../middleware/authorization";
import { getBoss } from "../../workert";
import { WorkerQueue } from "../../modules/workers/queues";
/* POST /v1/cw/sync/full */
export default createRoute(
"post",
["/sync/full"],
async (c) => {
const jobId = await getBoss().send(WorkerQueue.DALPURI_FULL_SYNC, {});
if (!jobId) {
throw new Error("Failed to enqueue dalpuri full sync job");
}
const response = apiResponse.successful(
"Full sync enqueued successfully",
{ jobId }
);
return c.json(response, response.status as ContentfulStatusCode);
},
authMiddleware()
);
+7
View File
@@ -0,0 +1,7 @@
import { Hono } from "hono";
import * as scheduleRoutes from "../schedules";
const scheduleRouter = new Hono();
Object.values(scheduleRoutes).map((r) => scheduleRouter.route("/", r));
export default scheduleRouter;
+7 -110
View File
@@ -4,20 +4,6 @@ import { apiResponse } from "../../../modules/api-utils/apiResponse";
import { ContentfulStatusCode } from "hono/utils/http-status"; import { ContentfulStatusCode } from "hono/utils/http-status";
import { authMiddleware } from "../../middleware/authorization"; import { authMiddleware } from "../../middleware/authorization";
import { processObjectValuePerms } from "../../../modules/permission-utils/processObjectPermissions"; import { processObjectValuePerms } from "../../../modules/permission-utils/processObjectPermissions";
import GenericError from "../../../Errors/GenericError";
import { prisma } from "../../../constants";
import { computeSubResourceCacheTTL } from "../../../modules/algorithms/computeSubResourceCacheTTL";
import { computeProductsCacheTTL } from "../../../modules/algorithms/computeProductsCacheTTL";
import {
getCachedSite,
getCachedNotes,
getCachedContacts,
getCachedProducts,
fetchAndCacheNotes,
fetchAndCacheContacts,
fetchAndCacheProducts,
fetchAndCacheSite,
} from "../../../modules/cache/opportunityCache";
import { generatedQuotes } from "../../../managers/generatedQuotes"; import { generatedQuotes } from "../../../managers/generatedQuotes";
/* GET /v1/sales/opportunities/opportunity/:identifier?include=notes,contacts,products,quotes */ /* GET /v1/sales/opportunities/opportunity/:identifier?include=notes,contacts,products,quotes */
@@ -31,102 +17,13 @@ export default createRoute(
includeParam includeParam
.split(",") .split(",")
.map((s) => s.trim().toLowerCase()) .map((s) => s.trim().toLowerCase())
.filter(Boolean), .filter(Boolean)
); );
// ── Quick DB lookup (≈3ms) to get cwOpportunityId for pre-warming ── // Fetch the opportunity from local DB
const isNumeric = /^\d+$/.test(identifier); const item = await opportunities.fetchItem(identifier);
const dbRecord = await prisma.opportunity.findFirst({
where: isNumeric
? { cwOpportunityId: Number(identifier) }
: { id: identifier },
select: {
cwOpportunityId: true,
companyCwId: true,
siteCwId: true,
closedFlag: true,
closedDate: true,
expectedCloseDate: true,
cwLastUpdated: true,
statusCwId: true,
},
});
if (!dbRecord) { // Fetch sub-resources as requested
throw new GenericError({
message: "Opportunity not found",
name: "OpportunityNotFound",
cause: `No opportunity exists with identifier '${identifier}'`,
status: 404,
});
}
// Compute TTLs from DB state
const subTtl = computeSubResourceCacheTTL({
closedFlag: dbRecord.closedFlag,
closedDate: dbRecord.closedDate,
expectedCloseDate: dbRecord.expectedCloseDate,
lastUpdated: dbRecord.cwLastUpdated,
});
const prodTtl = computeProductsCacheTTL({
closedFlag: dbRecord.closedFlag,
closedDate: dbRecord.closedDate,
expectedCloseDate: dbRecord.expectedCloseDate,
lastUpdated: dbRecord.cwLastUpdated,
statusCwId: dbRecord.statusCwId,
});
// ── Pre-warm sub-resources only on cache miss ───────────────────────
// Check Redis first — if the background refresh has kept the keys warm,
// skip the CW calls entirely. Only fetch-and-cache on a miss.
const cwOppId = dbRecord.cwOpportunityId;
const _ignoreErrors = (p: Promise<any>) => p.catch(() => {});
const prewarmPromises: Promise<any>[] = [];
if (dbRecord.companyCwId && dbRecord.siteCwId) {
const compId = dbRecord.companyCwId,
siteId = dbRecord.siteCwId;
prewarmPromises.push(
_ignoreErrors(
getCachedSite(compId, siteId).then(
(c) => c ?? fetchAndCacheSite(compId, siteId),
),
),
);
}
if (includes.has("notes") && subTtl)
prewarmPromises.push(
_ignoreErrors(
getCachedNotes(cwOppId).then(
(c) => c ?? fetchAndCacheNotes(cwOppId, subTtl),
),
),
);
if (includes.has("contacts") && subTtl)
prewarmPromises.push(
_ignoreErrors(
getCachedContacts(cwOppId).then(
(c) => c ?? fetchAndCacheContacts(cwOppId, subTtl),
),
),
);
if (includes.has("products") && prodTtl)
prewarmPromises.push(
_ignoreErrors(
getCachedProducts(cwOppId).then(
(c) => c ?? fetchAndCacheProducts(cwOppId, prodTtl),
),
),
);
// fetchItem runs its own CW calls (opp, activities, company) —
// these execute concurrently with the sub-resource pre-warming above.
const [item] = await Promise.all([
opportunities.fetchItem(identifier),
...prewarmPromises,
]);
// Sub-resources now hit warm Redis cache (near-instant)
const subResourcePromises: Record<string, Promise<any>> = { const subResourcePromises: Record<string, Promise<any>> = {
_site: item.fetchSite(), _site: item.fetchSite(),
}; };
@@ -154,7 +51,7 @@ export default createRoute(
const gatedData = await processObjectValuePerms( const gatedData = await processObjectValuePerms(
item.toJson(), item.toJson(),
"obj.opportunity", "obj.opportunity",
c.get("user"), c.get("user")
); );
const originalOpportunityNoteText = (gatedData as any).notes; const originalOpportunityNoteText = (gatedData as any).notes;
@@ -175,9 +72,9 @@ export default createRoute(
const response = apiResponse.successful( const response = apiResponse.successful(
"Opportunity fetched successfully!", "Opportunity fetched successfully!",
gatedData, gatedData
); );
return c.json(response, response.status as ContentfulStatusCode); return c.json(response, response.status as ContentfulStatusCode);
}, },
authMiddleware({ permissions: ["sales.opportunity.fetch"] }), authMiddleware({ permissions: ["sales.opportunity.fetch"] })
); );
+7 -2
View File
@@ -2,7 +2,7 @@ import { createRoute } from "../../modules/api-utils/createRoute";
import { apiResponse } from "../../modules/api-utils/apiResponse"; import { apiResponse } from "../../modules/api-utils/apiResponse";
import { ContentfulStatusCode } from "hono/utils/http-status"; import { ContentfulStatusCode } from "hono/utils/http-status";
import { authMiddleware } from "../middleware/authorization"; import { authMiddleware } from "../middleware/authorization";
import { QUOTE_STATUSES } from "../../types/QuoteStatuses"; import { prisma } from "../../constants";
/* GET /v1/sales/opportunity-types */ /* GET /v1/sales/opportunity-types */
export default createRoute( export default createRoute(
@@ -10,9 +10,14 @@ export default createRoute(
["/opportunity-types"], ["/opportunity-types"],
async (c) => { async (c) => {
const types = await prisma.opportunityType.findMany({
where: { inactiveFlag: false },
orderBy: { name: "asc" },
select: { id: true, name: true, inactiveFlag: true },
});
const response = apiResponse.successful( const response = apiResponse.successful(
"Opportunity Types Fetched Successfully!", "Opportunity Types Fetched Successfully!",
QUOTE_STATUSES, types,
); );
return c.json(response, response.status as ContentfulStatusCode); return c.json(response, response.status as ContentfulStatusCode);
}, },
+4
View File
@@ -27,6 +27,8 @@ import { default as fetchQuotes } from "./opportunities/[id]/quotes/fetchAll";
import { default as previewQuote } from "./opportunities/[id]/quotes/preview"; import { default as previewQuote } from "./opportunities/[id]/quotes/preview";
import { default as downloadQuote } from "./opportunities/[id]/quotes/download"; import { default as downloadQuote } from "./opportunities/[id]/quotes/download";
import { default as fetchDownloads } from "./opportunities/[id]/quotes/fetchDownloads"; import { default as fetchDownloads } from "./opportunities/[id]/quotes/fetchDownloads";
import { default as fetchNarrative } from "./opportunities/[id]/quotes/fetchNarrative";
import { default as updateNarrative } from "./opportunities/[id]/quotes/updateNarrative";
import { default as fetchByUser } from "./opportunities/fetchByUser"; import { default as fetchByUser } from "./opportunities/fetchByUser";
import { default as fetchByUserId } from "./opportunities/fetchByUserId"; import { default as fetchByUserId } from "./opportunities/fetchByUserId";
import { default as workflowDispatch } from "./opportunities/[id]/workflow/dispatch"; import { default as workflowDispatch } from "./opportunities/[id]/workflow/dispatch";
@@ -63,6 +65,8 @@ export {
previewQuote, previewQuote,
downloadQuote, downloadQuote,
fetchDownloads, fetchDownloads,
fetchNarrative,
updateNarrative,
refresh, refresh,
updateOpportunity, updateOpportunity,
workflowDispatch, workflowDispatch,
+9 -109
View File
@@ -5,19 +5,6 @@ import { ContentfulStatusCode } from "hono/utils/http-status";
import { authMiddleware } from "../../../middleware/authorization"; import { authMiddleware } from "../../../middleware/authorization";
import { processObjectValuePerms } from "../../../../modules/permission-utils/processObjectPermissions"; import { processObjectValuePerms } from "../../../../modules/permission-utils/processObjectPermissions";
import GenericError from "../../../../Errors/GenericError"; import GenericError from "../../../../Errors/GenericError";
import { prisma } from "../../../../constants";
import { computeSubResourceCacheTTL } from "../../../../modules/algorithms/computeSubResourceCacheTTL";
import { computeProductsCacheTTL } from "../../../../modules/algorithms/computeProductsCacheTTL";
import {
getCachedSite,
getCachedNotes,
getCachedContacts,
getCachedProducts,
fetchAndCacheNotes,
fetchAndCacheContacts,
fetchAndCacheProducts,
fetchAndCacheSite,
} from "../../../../modules/cache/opportunityCache";
import { generatedQuotes } from "../../../../managers/generatedQuotes"; import { generatedQuotes } from "../../../../managers/generatedQuotes";
/* GET /v1/sales/opportunities/opportunity/:identifier?include=notes,contacts,products,quotes */ /* GET /v1/sales/opportunities/opportunity/:identifier?include=notes,contacts,products,quotes */
@@ -31,102 +18,15 @@ export default createRoute(
includeParam includeParam
.split(",") .split(",")
.map((s) => s.trim().toLowerCase()) .map((s) => s.trim().toLowerCase())
.filter(Boolean), .filter(Boolean)
); );
// ── Quick DB lookup (≈3ms) to get cwOpportunityId for pre-warming ── // Fetch the opportunity from local DB
const isNumeric = /^\d+$/.test(identifier); const item = await opportunities.fetchItem(identifier);
const dbRecord = await prisma.opportunity.findFirst({
where: isNumeric
? { cwOpportunityId: Number(identifier) }
: { id: identifier },
select: {
cwOpportunityId: true,
companyCwId: true,
siteCwId: true,
closedFlag: true,
closedDate: true,
expectedCloseDate: true,
cwLastUpdated: true,
statusCwId: true,
},
});
if (!dbRecord) { console.log("Fetched opportunity:", item ? item.toJson() : null);
throw new GenericError({
message: "Opportunity not found",
name: "OpportunityNotFound",
cause: `No opportunity exists with identifier '${identifier}'`,
status: 404,
});
}
// Compute TTLs from DB state // Fetch sub-resources as requested
const subTtl = computeSubResourceCacheTTL({
closedFlag: dbRecord.closedFlag,
closedDate: dbRecord.closedDate,
expectedCloseDate: dbRecord.expectedCloseDate,
lastUpdated: dbRecord.cwLastUpdated,
});
const prodTtl = computeProductsCacheTTL({
closedFlag: dbRecord.closedFlag,
closedDate: dbRecord.closedDate,
expectedCloseDate: dbRecord.expectedCloseDate,
lastUpdated: dbRecord.cwLastUpdated,
statusCwId: dbRecord.statusCwId,
});
// ── Pre-warm sub-resources only on cache miss ───────────────────────
// Check Redis first — if the background refresh has kept the keys warm,
// skip the CW calls entirely. Only fetch-and-cache on a miss.
const cwOppId = dbRecord.cwOpportunityId;
const _ignoreErrors = (p: Promise<any>) => p.catch(() => {});
const prewarmPromises: Promise<any>[] = [];
if (dbRecord.companyCwId && dbRecord.siteCwId) {
const compId = dbRecord.companyCwId,
siteId = dbRecord.siteCwId;
prewarmPromises.push(
_ignoreErrors(
getCachedSite(compId, siteId).then(
(c) => c ?? fetchAndCacheSite(compId, siteId),
),
),
);
}
if (includes.has("notes") && subTtl)
prewarmPromises.push(
_ignoreErrors(
getCachedNotes(cwOppId).then(
(c) => c ?? fetchAndCacheNotes(cwOppId, subTtl),
),
),
);
if (includes.has("contacts") && subTtl)
prewarmPromises.push(
_ignoreErrors(
getCachedContacts(cwOppId).then(
(c) => c ?? fetchAndCacheContacts(cwOppId, subTtl),
),
),
);
if (includes.has("products") && prodTtl)
prewarmPromises.push(
_ignoreErrors(
getCachedProducts(cwOppId).then(
(c) => c ?? fetchAndCacheProducts(cwOppId, prodTtl),
),
),
);
// fetchItem runs its own CW calls (opp, activities, company) —
// these execute concurrently with the sub-resource pre-warming above.
const [item] = await Promise.all([
opportunities.fetchItem(identifier),
...prewarmPromises,
]);
// Sub-resources now hit warm Redis cache (near-instant)
const subResourcePromises: Record<string, Promise<any>> = { const subResourcePromises: Record<string, Promise<any>> = {
_site: item.fetchSite(), _site: item.fetchSite(),
}; };
@@ -147,7 +47,7 @@ export default createRoute(
subResourcePromises.quotes = generatedQuotes subResourcePromises.quotes = generatedQuotes
.fetchByOpportunity(item.id) .fetchByOpportunity(item.id)
.then((quotes) => .then((quotes) =>
quotes.map((q) => q.toJson({ includeRegenData, includeRegenParams })), quotes.map((q) => q.toJson({ includeRegenData, includeRegenParams }))
); );
} }
@@ -158,7 +58,7 @@ export default createRoute(
const gatedData = await processObjectValuePerms( const gatedData = await processObjectValuePerms(
item.toJson(), item.toJson(),
"obj.opportunity", "obj.opportunity",
c.get("user"), c.get("user")
); );
const originalOpportunityNoteText = (gatedData as any).notes; const originalOpportunityNoteText = (gatedData as any).notes;
@@ -179,9 +79,9 @@ export default createRoute(
const response = apiResponse.successful( const response = apiResponse.successful(
"Opportunity fetched successfully!", "Opportunity fetched successfully!",
gatedData, gatedData
); );
return c.json(response, response.status as ContentfulStatusCode); return c.json(response, response.status as ContentfulStatusCode);
}, },
authMiddleware({ permissions: ["sales.opportunity.fetch"] }), authMiddleware({ permissions: ["sales.opportunity.fetch"] })
); );
@@ -3,9 +3,23 @@ import { opportunities } from "../../../../../managers/opportunities";
import { apiResponse } from "../../../../../modules/api-utils/apiResponse"; import { apiResponse } from "../../../../../modules/api-utils/apiResponse";
import { ContentfulStatusCode } from "hono/utils/http-status"; import { ContentfulStatusCode } from "hono/utils/http-status";
import { authMiddleware } from "../../../../middleware/authorization"; import { authMiddleware } from "../../../../middleware/authorization";
import { resolveMember } from "../../../../../modules/cw-utils/members/memberCache"; import { prisma } from "../../../../../constants";
import { z } from "zod"; import { z } from "zod";
// Helper to resolve member from DB
async function resolveMember(identifier: string | null | undefined) {
if (!identifier) return null;
const member = await prisma.cwMember.findFirst({ where: { identifier } });
return member
? {
id: member.id,
identifier: member.identifier,
name: `${member.firstName} ${member.lastName}`.trim(),
cwMemberId: member.cwMemberId,
}
: { id: null, identifier, name: identifier, cwMemberId: null };
}
/* POST /v1/sales/opportunities/opportunity/:identifier/notes */ /* POST /v1/sales/opportunities/opportunity/:identifier/notes */
export default createRoute( export default createRoute(
"post", "post",
@@ -38,10 +52,10 @@ export default createRoute(
: null, : null,
flagged: created.flagged, flagged: created.flagged,
enteredBy: await resolveMember(created.enteredBy), enteredBy: await resolveMember(created.enteredBy),
}, }
); );
return c.json(response, response.status as ContentfulStatusCode); return c.json(response, response.status as ContentfulStatusCode);
}, },
authMiddleware({ permissions: ["sales.opportunity.note.create"] }), authMiddleware({ permissions: ["sales.opportunity.note.create"] })
); );
@@ -4,9 +4,23 @@ import { apiResponse } from "../../../../../modules/api-utils/apiResponse";
import { ContentfulStatusCode } from "hono/utils/http-status"; import { ContentfulStatusCode } from "hono/utils/http-status";
import { authMiddleware } from "../../../../middleware/authorization"; import { authMiddleware } from "../../../../middleware/authorization";
import GenericError from "../../../../../Errors/GenericError"; import GenericError from "../../../../../Errors/GenericError";
import { resolveMember } from "../../../../../modules/cw-utils/members/memberCache"; import { prisma } from "../../../../../constants";
import { z } from "zod"; import { z } from "zod";
// Helper to resolve member from DB
async function resolveMember(identifier: string | null | undefined) {
if (!identifier) return null;
const member = await prisma.cwMember.findFirst({ where: { identifier } });
return member
? {
id: member.id,
identifier: member.identifier,
name: `${member.firstName} ${member.lastName}`.trim(),
cwMemberId: member.cwMemberId,
}
: { id: null, identifier, name: identifier, cwMemberId: null };
}
/* PATCH /v1/sales/opportunities/opportunity/:identifier/notes/:noteId */ /* PATCH /v1/sales/opportunities/opportunity/:identifier/notes/:noteId */
export default createRoute( export default createRoute(
"patch", "patch",
@@ -48,10 +62,10 @@ export default createRoute(
: null, : null,
flagged: updated.flagged, flagged: updated.flagged,
enteredBy: await resolveMember(updated.enteredBy), enteredBy: await resolveMember(updated.enteredBy),
}, }
); );
return c.json(response, response.status as ContentfulStatusCode); return c.json(response, response.status as ContentfulStatusCode);
}, },
authMiddleware({ permissions: ["sales.opportunity.note.update"] }), authMiddleware({ permissions: ["sales.opportunity.note.update"] })
); );
@@ -4,6 +4,8 @@ import { apiResponse } from "../../../../../modules/api-utils/apiResponse";
import { ContentfulStatusCode } from "hono/utils/http-status"; import { ContentfulStatusCode } from "hono/utils/http-status";
import { authMiddleware } from "../../../../middleware/authorization"; import { authMiddleware } from "../../../../middleware/authorization";
import { processObjectValuePerms } from "../../../../../modules/permission-utils/processObjectPermissions"; import { processObjectValuePerms } from "../../../../../modules/permission-utils/processObjectPermissions";
import { ForecastProductController } from "../../../../../controllers/ForecastProductController";
import { opportunityCw } from "../../../../../modules/cw-utils/opportunities/opportunities";
import { z } from "zod"; import { z } from "zod";
const productItemSchema = z const productItemSchema = z
@@ -26,6 +28,8 @@ const productItemSchema = z
recurringCost: z.number().optional(), recurringCost: z.number().optional(),
cycles: z.number().int().min(0).optional(), cycles: z.number().int().min(0).optional(),
sequenceNumber: z.number().int().min(0).optional(), sequenceNumber: z.number().int().min(0).optional(),
procurementNotes: z.string().optional(),
productNarrative: z.string().optional(),
}) })
.strict(); .strict();
@@ -34,12 +38,25 @@ const addProductSchema = z.union([
z.array(productItemSchema).min(1, "At least one product is required"), z.array(productItemSchema).min(1, "At least one product is required"),
]); ]);
const procurementCreateSchema = z.object({
catalogItem: z.object({ id: z.number().int().positive() }),
description: z.string().min(1),
customerDescription: z.string().optional(),
quantity: z.number().positive().optional(),
price: z.number().optional(),
cost: z.number().optional(),
taxableFlag: z.boolean().optional(),
billableOption: z.string().optional(),
procurementNotes: z.string().optional(),
productNarrative: z.string().optional(),
});
/* POST /v1/sales/opportunities/opportunity/:identifier/products */ /* POST /v1/sales/opportunities/opportunity/:identifier/products */
export default createRoute( export default createRoute(
"post", "post",
["/opportunities/opportunity/:identifier/products"], ["/opportunities/opportunity/:identifier/products"],
async (c) => { async (c) => {
const identifier = c.req.param("identifier"); const identifier = c.req.param("identifier") as string;
const body = await c.req.json(); const body = await c.req.json();
const validated = addProductSchema.parse(body); const validated = addProductSchema.parse(body);
@@ -56,25 +73,152 @@ export default createRoute(
const item = await opportunities.fetchRecord(identifier); const item = await opportunities.fetchRecord(identifier);
// Strip customerDescription from forecast payloads — CW only accepts // Procurement-first: when catalogItem is available after field-level
// it on procurement products, not forecast items. // permission gating, create through procurement.
const customerDescriptions = gatedItems.map( // Fallback: if catalogItem is gated/missing, use forecast creation so
(g: any) => g.customerDescription, // callers without catalog permissions can still add products.
); const procurementInputs: Array<{
const forecastPayloads = gatedItems.map( index: number;
({ customerDescription, ...rest }: any) => rest, payload: z.infer<typeof procurementCreateSchema>;
}> = [];
const forecastInputs: Array<{
index: number;
payload: Record<string, unknown>;
customerDescription?: string;
}> = [];
for (const [index, g] of gatedItems.entries()) {
const hasCatalogItem =
typeof g?.catalogItem?.id === "number" && g.catalogItem.id > 0;
if (!hasCatalogItem) {
const { customerDescription, ...forecastPayload } = g as any;
const normalizedForecastPayload: Record<string, unknown> = {
...forecastPayload,
};
const hasAnyDescription =
typeof normalizedForecastPayload.forecastDescription === "string" ||
typeof normalizedForecastPayload.productDescription === "string";
// CW rejects bare forecast objects with only defaults (status/type).
// Ensure a minimal description exists for permission-reduced payloads.
if (!hasAnyDescription) {
normalizedForecastPayload.forecastDescription = "Line Item";
}
forecastInputs.push({
index,
payload: normalizedForecastPayload,
customerDescription:
typeof customerDescription === "string"
? customerDescription
: undefined,
});
continue;
}
const quantity =
typeof g.quantity === "number" && g.quantity > 0 ? g.quantity : undefined;
const totalRevenue =
typeof g.revenue === "number" && Number.isFinite(g.revenue)
? g.revenue
: undefined;
const totalCost =
typeof g.cost === "number" && Number.isFinite(g.cost)
? g.cost
: undefined;
const price =
totalRevenue === undefined
? undefined
: quantity && quantity > 0
? totalRevenue / quantity
: totalRevenue;
const cost =
totalCost === undefined
? undefined
: quantity && quantity > 0
? totalCost / quantity
: totalCost;
procurementInputs.push({
index,
payload: procurementCreateSchema.parse({
catalogItem: g.catalogItem,
description:
g.productDescription ??
g.forecastDescription ??
g.customerDescription ??
"Line Item",
customerDescription:
typeof g.customerDescription === "string"
? g.customerDescription
: undefined,
quantity,
price,
cost,
taxableFlag:
typeof g.taxableFlag === "boolean" ? g.taxableFlag : undefined,
billableOption: "Billable",
procurementNotes:
typeof (g as any).procurementNotes === "string"
? (g as any).procurementNotes
: undefined,
productNarrative:
typeof (g as any).productNarrative === "string"
? (g as any).productNarrative
: undefined,
}),
});
}
const createdByIndex = new Map<number, ForecastProductController>();
if (procurementInputs.length > 0) {
const createdProcurement = await item.addProcurementProducts(
procurementInputs.map((entry) => entry.payload),
); );
const created = await item.addProducts(forecastPayloads); const indexByForecastId = new Map<number, number>();
for (const [createdIdx, proc] of createdProcurement.entries()) {
if (typeof proc.forecastDetailId === "number") {
indexByForecastId.set(
proc.forecastDetailId,
procurementInputs[createdIdx]!.index,
);
}
}
// If any items included customerDescription, patch the linked if (indexByForecastId.size > 0) {
// procurement products after creation. This is best-effort since const createdForecastItems =
// newly created forecast items may not have a linked procurement (await opportunityCw.fetchProducts(item.cwOpportunityId)).forecastItems?.filter(
// product yet. (fi) => indexByForecastId.has(fi.id),
const procurementUpdates = created ) ?? [];
for (const fi of createdForecastItems) {
const originalIndex = indexByForecastId.get(fi.id);
if (typeof originalIndex === "number") {
createdByIndex.set(originalIndex, new ForecastProductController(fi));
}
}
}
}
if (forecastInputs.length > 0) {
const createdForecast = await item.addProducts(
forecastInputs.map((entry) => entry.payload),
);
for (const [createdIdx, createdItem] of createdForecast.entries()) {
const input = forecastInputs[createdIdx]!;
createdByIndex.set(input.index, createdItem);
}
const procurementUpdates = createdForecast
.map((product, idx) => ({ .map((product, idx) => ({
product, product,
customerDescription: customerDescriptions[idx], customerDescription: forecastInputs[idx]?.customerDescription,
})) }))
.filter((entry) => entry.customerDescription != null); .filter((entry) => entry.customerDescription != null);
@@ -89,13 +233,22 @@ export default createRoute(
), ),
); );
} }
}
const created = inputItems
.map((_, index) => createdByIndex.get(index))
.filter(
(entry): entry is ForecastProductController => entry !== undefined,
);
const isBatch = Array.isArray(body); const isBatch = Array.isArray(body);
const response = apiResponse.created( const response = apiResponse.created(
isBatch isBatch
? `${created.length} product(s) added to opportunity successfully!` ? `${created.length} product(s) added to opportunity successfully!`
: "Product added to opportunity successfully!", : "Product added to opportunity successfully!",
isBatch ? created.map((p) => p.toJson()) : created[0]!.toJson(), isBatch
? created.map((p) => p.toJson())
: created[0]?.toJson() ?? null,
); );
return c.json(response, response.status as ContentfulStatusCode); return c.json(response, response.status as ContentfulStatusCode);
}, },
@@ -8,7 +8,7 @@ import { z } from "zod";
const cancelProductSchema = z const cancelProductSchema = z
.object({ .object({
quantityCancelled: z.number().int().min(0), quantityCancelled: z.number().min(0),
cancellationReason: z.string().nullable().optional(), cancellationReason: z.string().nullable().optional(),
}) })
.strict(); .strict();
@@ -4,6 +4,9 @@ import { apiResponse } from "../../../../../modules/api-utils/apiResponse";
import { ContentfulStatusCode } from "hono/utils/http-status"; import { ContentfulStatusCode } from "hono/utils/http-status";
import { authMiddleware } from "../../../../middleware/authorization"; import { authMiddleware } from "../../../../middleware/authorization";
import GenericError from "../../../../../Errors/GenericError"; import GenericError from "../../../../../Errors/GenericError";
import { ForecastProductController } from "../../../../../controllers/ForecastProductController";
import { opportunityCw } from "../../../../../modules/cw-utils/opportunities/opportunities";
import { prisma } from "../../../../../constants";
import { z } from "zod"; import { z } from "zod";
const PRODUCT_NARRATIVE_FIELD_ID = 46; const PRODUCT_NARRATIVE_FIELD_ID = 46;
@@ -24,14 +27,14 @@ const updateProductSchema = z
.refine( .refine(
(value) => (value) =>
Object.values(value).some((item) => item !== undefined && item !== null), Object.values(value).some((item) => item !== undefined && item !== null),
"At least one editable field is required", "At least one editable field is required"
); );
const upsertCustomTextField = ( const upsertCustomTextField = (
fields: Array<Record<string, unknown>>, fields: Array<Record<string, unknown>>,
fieldId: number, fieldId: number,
caption: string, caption: string,
value: string, value: string
) => { ) => {
const next = [...fields]; const next = [...fields];
const idx = next.findIndex((f) => Number(f.id) === fieldId); const idx = next.findIndex((f) => Number(f.id) === fieldId);
@@ -76,12 +79,47 @@ export default createRoute(
const input = updateProductSchema.parse(body); const input = updateProductSchema.parse(body);
const opportunity = await opportunities.fetchRecord(identifier); const opportunity = await opportunities.fetchRecord(identifier);
const forecastItems = await opportunity.fetchProducts(); const cwForecast = await opportunityCw.fetchProducts(
const forecastItem = forecastItems.find( opportunity.cwOpportunityId
(item) => item.cwForecastId === productId, );
const cwForecastItems = cwForecast.forecastItems ?? [];
const cwForecastIds = new Set(cwForecastItems.map((item) => item.id));
let forecastItemId = productId;
if (!cwForecastIds.has(forecastItemId)) {
const procurementItems = await opportunityCw.fetchProcurementProducts(
opportunity.cwOpportunityId
); );
if (!forecastItem) { const matchedProcurement = procurementItems.find(
(item) => Number((item as any).id) === productId
);
const mappedForecastDetailId = Number(
(matchedProcurement as any)?.forecastDetailId
);
if (
Number.isInteger(mappedForecastDetailId) &&
mappedForecastDetailId > 0 &&
cwForecastIds.has(mappedForecastDetailId)
) {
forecastItemId = mappedForecastDetailId;
console.warn(
"[ProductUpdate] Resolved procurement product ID to forecast item ID",
{
opportunity: identifier,
requestedProductId: productId,
resolvedForecastItemId: forecastItemId,
}
);
}
}
const rawForecastItem = cwForecastItems.find(
(item) => item.id === forecastItemId
);
if (!rawForecastItem) {
throw new GenericError({ throw new GenericError({
status: 404, status: 404,
name: "ForecastItemNotFound", name: "ForecastItemNotFound",
@@ -89,6 +127,7 @@ export default createRoute(
}); });
} }
const forecastItem = new ForecastProductController(rawForecastItem);
const forecastJson = forecastItem.toJson(); const forecastJson = forecastItem.toJson();
const effectiveQuantity = input.quantity ?? forecastJson.quantity ?? 1; const effectiveQuantity = input.quantity ?? forecastJson.quantity ?? 1;
@@ -101,12 +140,12 @@ export default createRoute(
} }
if (input.unitPrice !== undefined) { if (input.unitPrice !== undefined) {
forecastPatch.revenue = Number( forecastPatch.revenue = Number(
(input.unitPrice * effectiveQuantity).toFixed(2), (input.unitPrice * effectiveQuantity).toFixed(2)
); );
} }
if (input.unitCost !== undefined) { if (input.unitCost !== undefined) {
forecastPatch.cost = Number( forecastPatch.cost = Number(
(input.unitCost * effectiveQuantity).toFixed(2), (input.unitCost * effectiveQuantity).toFixed(2)
); );
} }
if (input.taxableFlag !== undefined) { if (input.taxableFlag !== undefined) {
@@ -114,19 +153,22 @@ export default createRoute(
} }
const existingProcurement = const existingProcurement =
await opportunity.fetchProcurementProductByForecastItem(productId); await opportunity.fetchProcurementProductByForecastItem(forecastItemId);
if ( const hasNarrativeOrNotesValue =
(input.productNarrative !== undefined || (input.productNarrative !== undefined &&
input.procurementNotes !== undefined) && input.productNarrative !== null) ||
!existingProcurement (input.procurementNotes !== undefined && input.procurementNotes !== null);
) {
throw new GenericError({ if (hasNarrativeOrNotesValue && !existingProcurement) {
status: 400, console.warn(
name: "ProcurementLinkRequired", "[ProductUpdate] Ignoring procurement-only narrative fields for non-linked product",
message: {
"Product Narrative and Procurement Notes can only be updated on products linked to a procurement record", opportunity: identifier,
}); productId: forecastItemId,
requestedProductId: productId,
}
);
} }
let updatedProcurement = existingProcurement; let updatedProcurement = existingProcurement;
@@ -164,7 +206,7 @@ export default createRoute(
updatedFields, updatedFields,
PROCUREMENT_NOTES_FIELD_ID, PROCUREMENT_NOTES_FIELD_ID,
"Procurement Notes", "Procurement Notes",
input.procurementNotes, input.procurementNotes
); );
} }
if ( if (
@@ -175,7 +217,7 @@ export default createRoute(
updatedFields, updatedFields,
PRODUCT_NARRATIVE_FIELD_ID, PRODUCT_NARRATIVE_FIELD_ID,
"Product Narrative", "Product Narrative",
input.productNarrative, input.productNarrative
); );
} }
if ( if (
@@ -190,28 +232,47 @@ export default createRoute(
if (Object.keys(procurementPatch).length > 0) { if (Object.keys(procurementPatch).length > 0) {
updatedProcurement = updatedProcurement =
await opportunity.updateProcurementProductByForecastItem( await opportunity.updateProcurementProductByForecastItem(
productId, forecastItemId,
procurementPatch, procurementPatch
); );
} }
} }
let updatedForecast = forecastJson; let updatedForecast = forecastJson;
if (Object.keys(forecastPatch).length > 0) { if (Object.keys(forecastPatch).length > 0) {
const patched = await opportunity.updateProduct(productId, forecastPatch); const patched = await opportunity.updateProduct(
forecastItemId,
forecastPatch
);
updatedForecast = patched.toJson(); updatedForecast = patched.toJson();
} }
// Write changed fields back to the local ProductData row so fetchProducts()
// reflects the edit immediately without waiting for the next dalpuri sync.
const localPatch: Record<string, unknown> = {};
if (input.quantity !== undefined) localPatch.qty = input.quantity;
if (input.unitPrice !== undefined) localPatch.unitPrice = input.unitPrice;
if (input.unitCost !== undefined) localPatch.unitCost = input.unitCost;
if (input.productDescription !== undefined) localPatch.shortDescription = input.productDescription;
if (input.taxableFlag !== undefined) localPatch.taxableFlag = input.taxableFlag;
if (input.productNarrative !== undefined) localPatch.productNarrative = input.productNarrative;
if (Object.keys(localPatch).length > 0) {
await prisma.productData.update({
where: { id: productId },
data: localPatch,
});
}
const updatedFields = Array.isArray(updatedProcurement?.customFields) const updatedFields = Array.isArray(updatedProcurement?.customFields)
? updatedProcurement.customFields ? updatedProcurement.customFields
: []; : [];
const procurementNotes = const procurementNotes =
updatedFields.find( updatedFields.find(
(field: any) => field?.id === PROCUREMENT_NOTES_FIELD_ID, (field: any) => field?.id === PROCUREMENT_NOTES_FIELD_ID
)?.value ?? null; )?.value ?? null;
const productNarrative = const productNarrative =
updatedFields.find( updatedFields.find(
(field: any) => field?.id === PRODUCT_NARRATIVE_FIELD_ID, (field: any) => field?.id === PRODUCT_NARRATIVE_FIELD_ID
)?.value ?? null; )?.value ?? null;
const quantity = const quantity =
@@ -230,11 +291,13 @@ export default createRoute(
quantity, quantity,
unitPrice, unitPrice,
unitCost, unitCost,
requestedProductId: productId,
forecastItemId,
procurementNotes, procurementNotes,
productNarrative, productNarrative,
}); });
return c.json(response, response.status as ContentfulStatusCode); return c.json(response, response.status as ContentfulStatusCode);
}, },
authMiddleware({ permissions: ["sales.opportunity.product.update"] }), authMiddleware({ permissions: ["sales.opportunity.product.update"] })
); );
@@ -0,0 +1,25 @@
import { createRoute } from "../../../../../modules/api-utils/createRoute";
import { opportunities } from "../../../../../managers/opportunities";
import { apiResponse } from "../../../../../modules/api-utils/apiResponse";
import { ContentfulStatusCode } from "hono/utils/http-status";
import { authMiddleware } from "../../../../middleware/authorization";
/* GET /v1/sales/opportunities/opportunity/:identifier/quotes/narrative */
export default createRoute(
"get",
["/opportunities/opportunity/:identifier/quotes/narrative"],
async (c) => {
const identifier = c.req.param("identifier");
const item = await opportunities.fetchRecord(identifier);
const quoteNarrative = await item.fetchQuoteNarrative();
const response = apiResponse.successful(
"Quote narrative fetched successfully!",
{
quoteNarrative,
}
);
return c.json(response, response.status as ContentfulStatusCode);
},
authMiddleware({ permissions: ["sales.opportunity.fetch"] })
);
@@ -53,9 +53,9 @@ export default createRoute(
{ {
mimeType: "application/pdf", mimeType: "application/pdf",
contentBase64: previewBase64, contentBase64: previewBase64,
}, }
); );
return c.json(response, response.status as ContentfulStatusCode); return c.json(response, response.status as ContentfulStatusCode);
}, },
authMiddleware({ permissions: ["sales.opportunity.quote.preview"] }), authMiddleware({ permissions: ["sales.opportunity.quote.preview"] })
); );
@@ -0,0 +1,38 @@
import { createRoute } from "../../../../../modules/api-utils/createRoute";
import { opportunities } from "../../../../../managers/opportunities";
import { apiResponse } from "../../../../../modules/api-utils/apiResponse";
import { ContentfulStatusCode } from "hono/utils/http-status";
import { authMiddleware } from "../../../../middleware/authorization";
import { z } from "zod";
const quoteNarrativeSchema = z
.object({
quoteNarrative: z.string().optional().nullable(),
})
.strict();
/* PATCH /v1/sales/opportunities/opportunity/:identifier/quotes/narrative */
export default createRoute(
"patch",
["/opportunities/opportunity/:identifier/quotes/narrative"],
async (c) => {
const identifier = c.req.param("identifier");
const body = quoteNarrativeSchema.parse(
await c.req.json().catch(() => ({}))
);
const item = await opportunities.fetchRecord(identifier);
const quoteNarrative = await item.updateQuoteNarrative(
body.quoteNarrative ?? null
);
const response = apiResponse.successful(
"Quote narrative saved successfully!",
{
quoteNarrative,
}
);
return c.json(response, response.status as ContentfulStatusCode);
},
authMiddleware({ permissions: ["sales.opportunity.update"] })
);
@@ -10,6 +10,7 @@ const updateSchema = z
.object({ .object({
name: z.string().min(1).optional(), name: z.string().min(1).optional(),
notes: z.string().optional(), notes: z.string().optional(),
interest: z.enum(["HOT", "WARM", "COLD"]).nullable().optional(),
rating: z.object({ id: z.number() }).optional(), rating: z.object({ id: z.number() }).optional(),
type: z.object({ id: z.number() }).optional(), type: z.object({ id: z.number() }).optional(),
stage: z.object({ id: z.number() }).optional(), stage: z.object({ id: z.number() }).optional(),
@@ -50,7 +51,7 @@ export default createRoute(
const response = apiResponse.successful( const response = apiResponse.successful(
"Opportunity updated successfully!", "Opportunity updated successfully!",
updated.toJson(), updated.toJson()
); );
return c.json(response, response.status as ContentfulStatusCode); return c.json(response, response.status as ContentfulStatusCode);
@@ -77,7 +78,7 @@ export default createRoute(
errors: cwErrors, errors: cwErrors,
meta: { timestamp: Date.now() }, meta: { timestamp: Date.now() },
}, },
cwStatus as ContentfulStatusCode, cwStatus as ContentfulStatusCode
); );
} }
@@ -89,5 +90,5 @@ export default createRoute(
}); });
} }
}, },
authMiddleware({ permissions: ["sales.opportunity.update"] }), authMiddleware({ permissions: ["sales.opportunity.update"] })
); );
+16 -5
View File
@@ -6,6 +6,7 @@ import { authMiddleware } from "../../middleware/authorization";
import GenericError from "../../../Errors/GenericError"; import GenericError from "../../../Errors/GenericError";
import { z } from "zod"; import { z } from "zod";
import { cwMembers } from "../../../managers/cwMembers"; import { cwMembers } from "../../../managers/cwMembers";
import { prisma } from "../../../constants";
import { import {
createWorkflowActivity, createWorkflowActivity,
OptimaType, OptimaType,
@@ -18,6 +19,7 @@ const createSchema = z.object({
.min(1) .min(1)
.transform((v) => new Date(v).toISOString().replace(/\.\d{3}Z$/, "Z")), .transform((v) => new Date(v).toISOString().replace(/\.\d{3}Z$/, "Z")),
notes: z.string().optional(), notes: z.string().optional(),
interest: z.enum(["HOT", "WARM", "COLD"]).nullable().optional(),
rating: z.object({ id: z.number() }).optional(), rating: z.object({ id: z.number() }).optional(),
type: z.object({ id: z.number() }).optional(), type: z.object({ id: z.number() }).optional(),
stage: z.object({ id: z.number() }).optional(), stage: z.object({ id: z.number() }).optional(),
@@ -42,9 +44,18 @@ export default createRoute(
async (c) => { async (c) => {
const body = await c.req.json(); const body = await c.req.json();
const data = createSchema.parse(body); const data = createSchema.parse(body);
const { interest, ...cwCreateData } = data;
try { try {
const item = await opportunities.createItem(data); const item = await opportunities.createItem(cwCreateData);
if (interest !== undefined) {
await prisma.opportunity.update({
where: { uid: item.id },
data: { interest },
});
item.interest = interest;
}
// Create a workflow activity for the new opportunity // Create a workflow activity for the new opportunity
try { try {
@@ -69,14 +80,14 @@ export default createRoute(
} catch (activityErr) { } catch (activityErr) {
console.error( console.error(
"[Opportunity Create] Failed to create workflow activity:", "[Opportunity Create] Failed to create workflow activity:",
activityErr, activityErr
); );
// Don't fail the opportunity creation if the activity fails // Don't fail the opportunity creation if the activity fails
} }
const response = apiResponse.created( const response = apiResponse.created(
"Opportunity created successfully!", "Opportunity created successfully!",
item.toJson(), item.toJson()
); );
return c.json(response, response.status as ContentfulStatusCode); return c.json(response, response.status as ContentfulStatusCode);
@@ -103,7 +114,7 @@ export default createRoute(
errors: cwErrors, errors: cwErrors,
meta: { timestamp: Date.now() }, meta: { timestamp: Date.now() },
}, },
cwStatus as ContentfulStatusCode, cwStatus as ContentfulStatusCode
); );
} }
@@ -115,5 +126,5 @@ export default createRoute(
}); });
} }
}, },
authMiddleware({ permissions: ["sales.opportunity.create"] }), authMiddleware({ permissions: ["sales.opportunity.create"] })
); );
+27 -60
View File
@@ -2,12 +2,8 @@ import { createRoute } from "../../../modules/api-utils/createRoute";
import { apiResponse } from "../../../modules/api-utils/apiResponse"; import { apiResponse } from "../../../modules/api-utils/apiResponse";
import { ContentfulStatusCode } from "hono/utils/http-status"; import { ContentfulStatusCode } from "hono/utils/http-status";
import { authMiddleware } from "../../middleware/authorization"; import { authMiddleware } from "../../middleware/authorization";
import GenericError from "../../../Errors/GenericError"; import { getSalesOpportunityMetricsForMember, getSalesOpportunityMetricsAll } from "../../../modules/cache/salesOpportunityMetricsCache";
import { import { prisma } from "../../../constants";
getSalesOpportunityMetricsAll,
getSalesOpportunityMetricsForMember,
refreshSalesOpportunityMetricsCache,
} from "../../../modules/cache/salesOpportunityMetricsCache";
/* GET /v1/sales/opportunities/metrics */ /* GET /v1/sales/opportunities/metrics */
export default createRoute( export default createRoute(
@@ -15,60 +11,39 @@ export default createRoute(
["/opportunities/metrics"], ["/opportunities/metrics"],
async (c) => { async (c) => {
const user = c.get("user"); const user = c.get("user");
const scope = (c.req.query("scope") ?? "me").toLowerCase(); const scope = c.req.query("scope");
const requestedIdentifier = c.req.query("identifier")?.trim().toLowerCase(); const identifierOverride = c.req.query("identifier");
const currentUserIdentifier = user?.cwIdentifier?.trim().toLowerCase();
if (
scope === "all" &&
!(await user.hasPermission("sales.opportunity.metrics.all"))
) {
throw new GenericError({
name: "InsufficientPermission",
message:
"You do not have permission to view metrics for all active members.",
status: 403,
});
}
const usingIdentifierOverride =
scope !== "all" &&
!!requestedIdentifier &&
requestedIdentifier !== currentUserIdentifier;
if (
usingIdentifierOverride &&
!(await user.hasPermission(
"sales.opportunity.metrics.identifier.override",
))
) {
throw new GenericError({
name: "InsufficientPermission",
message:
"You do not have permission to query metrics by overriding the member identifier.",
status: 403,
});
}
const requireWarmCache = async () => {
const all = await getSalesOpportunityMetricsAll();
if (all) return all;
await refreshSalesOpportunityMetricsCache();
return getSalesOpportunityMetricsAll();
};
// scope=all — return metrics for all active members (requires permission)
if (scope === "all") { if (scope === "all") {
const all = await requireWarmCache(); if (!(await user.hasPermission("sales.opportunity.metrics.all"))) {
return c.json({ status: 403, message: "Forbidden", successful: false }, 403);
}
const allMetrics = await getSalesOpportunityMetricsAll();
const response = apiResponse.successful( const response = apiResponse.successful(
"Sales opportunity metrics fetched successfully!", "Sales opportunity metrics fetched successfully!",
all, allMetrics,
); );
return c.json(response, response.status as ContentfulStatusCode); return c.json(response, response.status as ContentfulStatusCode);
} }
const targetIdentifier = requestedIdentifier ?? currentUserIdentifier; // Determine which CW identifier to look up
let cwIdentifier: string | null = null;
if (!targetIdentifier) { if (identifierOverride) {
if (!(await user.hasPermission("sales.opportunity.metrics.identifier.override"))) {
return c.json({ status: 403, message: "Forbidden", successful: false }, 403);
}
cwIdentifier = identifierOverride;
} else {
const dbUser = await prisma.user.findFirst({
where: { id: user.id },
select: { cwIdentifier: true },
});
cwIdentifier = dbUser?.cwIdentifier ?? null;
}
if (!cwIdentifier) {
const response = apiResponse.successful( const response = apiResponse.successful(
"Sales opportunity metrics fetched successfully!", "Sales opportunity metrics fetched successfully!",
null, null,
@@ -76,18 +51,10 @@ export default createRoute(
return c.json(response, response.status as ContentfulStatusCode); return c.json(response, response.status as ContentfulStatusCode);
} }
let metrics = await getSalesOpportunityMetricsForMember(targetIdentifier); const metrics = await getSalesOpportunityMetricsForMember(cwIdentifier);
if (!metrics) {
await refreshSalesOpportunityMetricsCache();
metrics = await getSalesOpportunityMetricsForMember(targetIdentifier);
}
const response = apiResponse.successful( const response = apiResponse.successful(
"Sales opportunity metrics fetched successfully!", "Sales opportunity metrics fetched successfully!",
{
identifier: targetIdentifier,
metrics, metrics,
},
); );
return c.json(response, response.status as ContentfulStatusCode); return c.json(response, response.status as ContentfulStatusCode);
}, },
+22
View File
@@ -0,0 +1,22 @@
import { createRoute } from "../../../modules/api-utils/createRoute";
import { schedules } from "../../../managers/schedules";
import { apiResponse } from "../../../modules/api-utils/apiResponse";
import { ContentfulStatusCode } from "hono/utils/http-status";
import { authMiddleware } from "../../middleware/authorization";
/* /v1/schedule/schedules/:identifier */
export default createRoute(
"get",
["/schedules/:identifier"],
async (c) => {
const schedule = await schedules.fetch(c.req.param("identifier"));
const response = apiResponse.successful(
"Schedule Fetched Successfully!",
schedule.toJson(),
);
return c.json(response, response.status as ContentfulStatusCode);
},
authMiddleware({ permissions: ["schedule.fetch"] }),
);
+22
View File
@@ -0,0 +1,22 @@
import { createRoute } from "../../modules/api-utils/createRoute";
import { schedules } from "../../managers/schedules";
import { apiResponse } from "../../modules/api-utils/apiResponse";
import { ContentfulStatusCode } from "hono/utils/http-status";
import { authMiddleware } from "../middleware/authorization";
/* /v1/schedule/count */
export default createRoute(
"get",
["/count"],
async (c) => {
const count = await schedules.count();
const response = apiResponse.successful(
"Schedule count fetched successfully!",
{ count },
);
return c.json(response, response.status as ContentfulStatusCode);
},
authMiddleware({ permissions: ["schedule.fetch.many"] }),
);
+42
View File
@@ -0,0 +1,42 @@
import { createRoute } from "../../modules/api-utils/createRoute";
import { schedules } from "../../managers/schedules";
import { apiResponse } from "../../modules/api-utils/apiResponse";
import { ContentfulStatusCode } from "hono/utils/http-status";
import { authMiddleware } from "../middleware/authorization";
/* /v1/schedule/schedules */
export default createRoute(
"get",
["/schedules"],
async (c) => {
const page = new Number(c.req.query("page") ?? 1) as number;
const rpp = new Number(c.req.query("rpp") ?? 30) as number;
const search = c.req.query("search") as string;
const data = search
? await schedules.search(search, page, rpp)
: await schedules.fetchPages(page, rpp);
const totalRecords = search
? (await schedules.search(search, 1, 999999)).length
: await schedules.count();
const response = apiResponse.successful(
"Schedules Fetched Successfully!",
data.map((s) => s.toJson()),
{
pagination: {
previousPage: page == 1 ? null : page - 1,
currentPage: page,
nextPage: page >= totalRecords / rpp ? null : page + 1,
totalPages: Math.ceil(totalRecords / rpp),
totalRecords,
listedRecords: rpp,
},
},
);
return c.json(response, response.status as ContentfulStatusCode);
},
authMiddleware({ permissions: ["schedule.fetch.many"] }),
);
+7
View File
@@ -0,0 +1,7 @@
import { default as fetchAll } from "./fetchAll";
import { default as fetch } from "./[id]/fetch";
import { default as count } from "./count";
import { default as fetchByDateRange } from "./member/fetchByDateRange";
import { default as fetchMySchedule } from "./me/fetchMySchedule";
export { count, fetch, fetchAll, fetchByDateRange, fetchMySchedule };
@@ -0,0 +1,63 @@
import { createRoute } from "../../../modules/api-utils/createRoute";
import { schedules } from "../../../managers/schedules";
import { apiResponse } from "../../../modules/api-utils/apiResponse";
import { ContentfulStatusCode } from "hono/utils/http-status";
import { authMiddleware } from "../../middleware/authorization";
import GenericError from "../../../Errors/GenericError";
/* /v1/schedule/@me?start=<ISO>&end=<ISO> */
export default createRoute(
"get",
["/@me"],
async (c) => {
const user = c.get("user");
if (!user?.cwIdentifier) {
throw new GenericError({
name: "BadRequest",
message: "Your account is not linked to a ConnectWise member. Cannot fetch schedule.",
status: 400,
});
}
const startParam = c.req.query("start");
const endParam = c.req.query("end");
if (!startParam || !endParam) {
throw new GenericError({
name: "BadRequest",
message: "Query params 'start' and 'end' are required (ISO 8601 date strings).",
status: 400,
});
}
const startDate = new Date(startParam);
const endDate = new Date(endParam);
if (isNaN(startDate.getTime()) || isNaN(endDate.getTime())) {
throw new GenericError({
name: "BadRequest",
message: "Invalid date format. Use ISO 8601 (e.g. 2026-04-01T00:00:00Z).",
status: 400,
});
}
if (startDate >= endDate) {
throw new GenericError({
name: "BadRequest",
message: "'start' must be before 'end'.",
status: 400,
});
}
const data = await schedules.fetchByMemberDateRange(user.cwIdentifier, startDate, endDate);
const response = apiResponse.successful(
"Your schedule entries fetched successfully!",
data.map((s) => s.toJson()),
);
return c.json(response, response.status as ContentfulStatusCode);
},
authMiddleware({ permissions: ["schedule.fetch"] }),
);
@@ -0,0 +1,62 @@
import { createRoute } from "../../../modules/api-utils/createRoute";
import { schedules } from "../../../managers/schedules";
import { apiResponse } from "../../../modules/api-utils/apiResponse";
import { ContentfulStatusCode } from "hono/utils/http-status";
import { authMiddleware } from "../../middleware/authorization";
import GenericError from "../../../Errors/GenericError";
/* /v1/schedule/member/:memberId?start=<ISO>&end=<ISO> */
export default createRoute(
"get",
["/member/:memberId"],
async (c) => {
const memberId = c.req.param("memberId");
const startParam = c.req.query("start");
const endParam = c.req.query("end");
if (!memberId) {
throw new GenericError({
name: "BadRequest",
message: "memberId path parameter is required.",
status: 400,
});
}
if (!startParam || !endParam) {
throw new GenericError({
name: "BadRequest",
message: "Query params 'start' and 'end' are required (ISO 8601 date strings).",
status: 400,
});
}
const startDate = new Date(startParam);
const endDate = new Date(endParam);
if (isNaN(startDate.getTime()) || isNaN(endDate.getTime())) {
throw new GenericError({
name: "BadRequest",
message: "Invalid date format. Use ISO 8601 (e.g. 2026-04-01T00:00:00Z).",
status: 400,
});
}
if (startDate >= endDate) {
throw new GenericError({
name: "BadRequest",
message: "'start' must be before 'end'.",
status: 400,
});
}
const data = await schedules.fetchByMemberDateRange(memberId, startDate, endDate);
const response = apiResponse.successful(
"Schedule entries fetched successfully!",
data.map((s) => s.toJson()),
);
return c.json(response, response.status as ContentfulStatusCode);
},
authMiddleware({ permissions: ["schedule.fetch.many"] }),
);
+26 -13
View File
@@ -4,6 +4,18 @@ import { ZodError } from "zod";
import { cors } from "hono/cors"; import { cors } from "hono/cors";
import GenericError from "../Errors/GenericError"; import GenericError from "../Errors/GenericError";
import teapot from "./teapot"; import teapot from "./teapot";
import authRouter from "./routers/authRouter";
import userRouter from "./routers/user";
import companyRouter from "./routers/companyRouter";
import credentialRouter from "./routers/credentialRouter";
import credentialTypeRouter from "./routers/credentialTypeRouter";
import roleRouter from "./routers/roleRouter";
import permissionRouter from "./routers/permissionRouter";
import unifiRouter from "./routers/unifiRouter";
import procurementRouter from "./routers/procurementRouter";
import salesRouter from "./routers/salesRouter";
import cwRouter from "./routers/cwRouter";
import scheduleRouter from "./routers/scheduleRouter";
const app = new Hono(); const app = new Hono();
const v1 = new Hono(); const v1 = new Hono();
@@ -24,7 +36,7 @@ app.onError((err, ctx) => {
return ctx.json( return ctx.json(
apiResponse.zodError(err), apiResponse.zodError(err),
//@ts-ignore //@ts-ignore
apiResponse.zodError(err).status, apiResponse.zodError(err).status
); );
} }
@@ -41,23 +53,24 @@ app.notFound((c) => {
message: `Cannot ${c.req.method.toUpperCase()} ${c.req.path}`, message: `Cannot ${c.req.method.toUpperCase()} ${c.req.path}`,
status: 404, status: 404,
cause: "Unknown", cause: "Unknown",
}), })
); );
return c.json(response, response.status); return c.json(response, response.status);
}); });
v1.route("/teapot", teapot); v1.route("/teapot", teapot);
v1.route("/auth", require("./routers/authRouter").default); v1.route("/auth", authRouter);
v1.route("/user", require("./routers/user").default); v1.route("/user", userRouter);
v1.route("/company", require("./routers/companyRouter").default); v1.route("/company", companyRouter);
v1.route("/credential", require("./routers/credentialRouter").default); v1.route("/credential", credentialRouter);
v1.route("/credential-type", require("./routers/credentialTypeRouter").default); v1.route("/credential-type", credentialTypeRouter);
v1.route("/role", require("./routers/roleRouter").default); v1.route("/role", roleRouter);
v1.route("/permissions", require("./routers/permissionRouter").default); v1.route("/permissions", permissionRouter);
v1.route("/unifi", require("./routers/unifiRouter").default); v1.route("/unifi", unifiRouter);
v1.route("/procurement", require("./routers/procurementRouter").default); v1.route("/procurement", procurementRouter);
v1.route("/sales", require("./routers/salesRouter").default); v1.route("/sales", salesRouter);
v1.route("/cw", require("./routers/cwRouter").default); v1.route("/cw", cwRouter);
v1.route("/schedule", scheduleRouter);
app.route("/v1", v1); app.route("/v1", v1);
export default app; export default app;
@@ -2,7 +2,7 @@ import { Socket } from "socket.io";
import { attachSocketEventPermissions } from "../middleware/authorization"; import { attachSocketEventPermissions } from "../middleware/authorization";
import { opportunities } from "../../../managers/opportunities"; import { opportunities } from "../../../managers/opportunities";
const LIVE_QUOTE_PREVIEW_PERMISSION = "sales.opportunity.fetch"; const LIVE_QUOTE_PREVIEW_PERMISSION = "sales.opportunity.quote.preview";
export const registerLiveQuotePreviewHandlers = (socket: Socket) => { export const registerLiveQuotePreviewHandlers = (socket: Socket) => {
attachSocketEventPermissions(socket, { attachSocketEventPermissions(socket, {
@@ -15,7 +15,7 @@ export const registerLiveQuotePreviewHandlers = (socket: Socket) => {
"opp:live_quote_preview", "opp:live_quote_preview",
async ( async (
payload: { id?: string | number }, payload: { id?: string | number },
ack?: (response: { ok: boolean; event?: string; error?: string }) => void, ack?: (response: { ok: boolean; event?: string; error?: string }) => void
) => { ) => {
const oppId = payload?.id; const oppId = payload?.id;
const normalizedId = const normalizedId =
@@ -97,6 +97,6 @@ export const registerLiveQuotePreviewHandlers = (socket: Socket) => {
id: normalizedId, id: normalizedId,
event: dataEvent, event: dataEvent,
}); });
}, }
); );
}; };
+4 -6
View File
@@ -15,17 +15,15 @@ export default createRoute(
const processWlans = await Promise.all( const processWlans = await Promise.all(
wlans.map((wlan) => wlans.map((wlan) =>
processObjectValuePerms(wlan, "unifi.site.wifi.read", c.get("user")), processObjectValuePerms(wlan, "unifi.site.wifi.read", c.get("user"))
), )
); );
console.log(processWlans);
const response = apiResponse.successful( const response = apiResponse.successful(
"UniFi WiFi Networks Fetched Successfully!", "UniFi WiFi Networks Fetched Successfully!",
processWlans, processWlans
); );
return c.json(response, response.status as ContentfulStatusCode); return c.json(response, response.status as ContentfulStatusCode);
}, },
authMiddleware({ permissions: ["unifi.access", "unifi.site.wifi"] }), authMiddleware({ permissions: ["unifi.access", "unifi.site.wifi"] })
); );
+4 -2
View File
@@ -7,6 +7,8 @@ import { authMiddleware } from "../../middleware/authorization";
const updateSchema = z const updateSchema = z
.object({ .object({
name: z.string().optional(), name: z.string().optional(),
firstName: z.string().nullable().optional(),
lastName: z.string().nullable().optional(),
image: z.string().optional(), image: z.string().optional(),
}) })
.strict(); .strict();
@@ -19,10 +21,10 @@ export default createRoute(
const updatedUser = await c.get("user")?.update(body); const updatedUser = await c.get("user")?.update(body);
const response = apiResponse.successful( const response = apiResponse.successful(
"Successfully updated user.", "Successfully updated user.",
updatedUser?.toJson(), updatedUser?.toJson()
); );
return c.json(response, response.status as ContentfulStatusCode); return c.json(response, response.status as ContentfulStatusCode);
}, },
authMiddleware({ scopes: ["user.write"] }), authMiddleware({ scopes: ["user.write"] })
); );
+4 -2
View File
@@ -9,6 +9,8 @@ import GenericError from "../../Errors/GenericError";
const updateSchema = z const updateSchema = z
.object({ .object({
name: z.string().optional(), name: z.string().optional(),
firstName: z.string().nullable().optional(),
lastName: z.string().nullable().optional(),
image: z.string().optional(), image: z.string().optional(),
roles: z.array(z.string()).optional(), roles: z.array(z.string()).optional(),
permissions: z.array(z.string()).optional(), permissions: z.array(z.string()).optional(),
@@ -60,9 +62,9 @@ export default createRoute(
const response = apiResponse.successful( const response = apiResponse.successful(
"User Updated Successfully!", "User Updated Successfully!",
user.toJson(), user.toJson()
); );
return c.json(response, response.status as ContentfulStatusCode); return c.json(response, response.status as ContentfulStatusCode);
}, },
authMiddleware({ permissions: ["user.write.other"] }), authMiddleware({ permissions: ["user.write.other"] })
); );
-25
View File
@@ -4,7 +4,6 @@ import { Prisma, PrismaClient } from "../generated/prisma/client";
import * as msal from "@azure/msal-node"; import * as msal from "@azure/msal-node";
import { Server } from "socket.io"; import { Server } from "socket.io";
import { Server as Engine } from "@socket.io/bun-engine"; import { Server as Engine } from "@socket.io/bun-engine";
import { io as createSocketClient } from "socket.io-client";
import axios from "axios"; import axios from "axios";
import { UnifiClient } from "./modules/unifi-api/UnifiClient"; import { UnifiClient } from "./modules/unifi-api/UnifiClient";
import { attachCwApiLogger } from "./modules/cw-utils/cwApiLogger"; import { attachCwApiLogger } from "./modules/cw-utils/cwApiLogger";
@@ -13,18 +12,11 @@ import Redis from "ioredis";
const connectionString = `${process.env.DATABASE_URL}`; const connectionString = `${process.env.DATABASE_URL}`;
const adapter = new PrismaPg({ connectionString }); const adapter = new PrismaPg({ connectionString });
interface EnvKey {
PORT: number;
}
// ENV CONSTANTS // ENV CONSTANTS
export const PORT = process.env.PORT; export const PORT = process.env.PORT;
export const API_BASE_URL = export const API_BASE_URL =
process.env.API_BASE_URL || `http://localhost:${PORT || 3000}`; process.env.API_BASE_URL || `http://localhost:${PORT || 3000}`;
export const COLLECTOR_WS_URL =
process.env.COLLECTOR_WS_URL || "http://localhost:7204";
export const COLLECTOR_PSK = process.env.COLLECTOR_PSK || "";
export const prisma = new PrismaClient({ adapter }); export const prisma = new PrismaClient({ adapter });
@@ -77,23 +69,6 @@ const engine = new Engine();
io.bind(engine); io.bind(engine);
export { io, engine }; export { io, engine };
export const collectorSocket = createSocketClient(COLLECTOR_WS_URL, {
autoConnect: true,
reconnection: true,
reconnectionDelay: 1000,
reconnectionDelayMax: 5000,
transports: ["websocket"],
auth: COLLECTOR_PSK ? { psk: COLLECTOR_PSK } : undefined,
});
export const connectCollectorSocket = () => {
if (collectorSocket.connected) {
return;
}
collectorSocket.connect();
};
// Connectwise API Client // Connectwise API Client
const connectWiseApi = axios.create({ const connectWiseApi = axios.create({
-21
View File
@@ -5,7 +5,6 @@ import {
CWCreateActivity, CWCreateActivity,
} from "../modules/cw-utils/activities/activity.types"; } from "../modules/cw-utils/activities/activity.types";
import { activityCw } from "../modules/cw-utils/activities/activities"; import { activityCw } from "../modules/cw-utils/activities/activities";
import { fetchActivity } from "../modules/cw-utils/activities/fetchActivity";
/** /**
* Activity Controller * Activity Controller
@@ -127,26 +126,6 @@ export class ActivityController {
this.cwUpdatedBy = data._info?.updatedBy ?? null; this.cwUpdatedBy = data._info?.updatedBy ?? null;
} }
/**
* Refresh from ConnectWise
*
* Fetches the latest activity data from CW and returns
* a new ActivityController instance with updated state.
*/
public async refreshFromCW(): Promise<ActivityController> {
const cwData = await fetchActivity(this.cwActivityId);
return new ActivityController(cwData);
}
/**
* Fetch raw CW data
*
* Returns the raw ConnectWise activity object.
*/
public async fetchCwData(): Promise<CWActivity> {
return fetchActivity(this.cwActivityId);
}
/** /**
* Update in ConnectWise * Update in ConnectWise
* *
+46 -21
View File
@@ -1,9 +1,27 @@
import { CatalogItem } from "../../generated/prisma/client"; import {
CatalogItem,
CatalogCategory,
CatalogSubcategory,
CatalogManufacturer,
} from "../../generated/prisma/client";
import { prisma } from "../constants"; import { prisma } from "../constants";
import { catalogCw } from "../modules/cw-utils/procurement/catalog"; import { catalogCw } from "../modules/cw-utils/procurement/catalog";
import { CatalogItem as CWCatalogItem } from "../modules/cw-utils/procurement/catalog.types"; import { CatalogItem as CWCatalogItem } from "../modules/cw-utils/procurement/catalog.types";
import GenericError from "../Errors/GenericError"; import GenericError from "../Errors/GenericError";
/**
* Shape of the Prisma result when catalog items are queried with
* `include: { linkedItems, manufacturer, subcategory: { include: { category } } }`.
*/
type CatalogItemWithRelations = CatalogItem & {
linkedItems?: (CatalogItem & {
manufacturer?: CatalogManufacturer | null;
subcategory?: CatalogSubcategory & { category?: CatalogCategory | null };
})[];
manufacturer?: CatalogManufacturer | null;
subcategory?: (CatalogSubcategory & { category?: CatalogCategory | null }) | null;
};
/** /**
* Catalog Item Controller * Catalog Item Controller
* *
@@ -12,13 +30,15 @@ import GenericError from "../Errors/GenericError";
* the internal database representation with ConnectWise catalog data. * the internal database representation with ConnectWise catalog data.
*/ */
export class CatalogItemController { export class CatalogItemController {
/** The ConnectWise catalog record ID (`CatalogItem.id` in Prisma — Int @unique). */
public readonly cwCatalogId: number;
/** The Prisma primary key (UUID). */
public readonly id: string; public readonly id: string;
public name: string; public name: string;
public description: string | null; public description: string | null;
public customerDescription: string | null; public customerDescription: string | null;
public internalNotes: string | null; public internalNotes: string | null;
public readonly cwCatalogId: number;
public readonly identifier: string | null; public readonly identifier: string | null;
public category: string | null; public category: string | null;
@@ -48,24 +68,29 @@ export class CatalogItemController {
public readonly createdAt: Date; public readonly createdAt: Date;
public updatedAt: Date; public updatedAt: Date;
constructor( constructor(itemData: CatalogItemWithRelations) {
itemData: CatalogItem & { // `id` (Int @unique) is the ConnectWise catalog record ID.
linkedItems?: CatalogItem[]; // `uid` (String @id) is the Prisma primary key.
}, this.cwCatalogId = itemData.id;
) { this.id = itemData.uid;
this.id = itemData.id;
this.name = itemData.name; this.name = itemData.name;
this.description = itemData.description; this.description = itemData.description;
this.customerDescription = itemData.customerDescription; this.customerDescription = itemData.customerDescription;
this.internalNotes = itemData.internalNotes; this.internalNotes = itemData.internalNotes;
this.cwCatalogId = itemData.cwCatalogId;
this.identifier = itemData.identifier; this.identifier = itemData.identifier;
this.category = itemData.category;
this.categoryCwId = itemData.categoryCwId; // Extract relation data into flat fields
this.subcategory = itemData.subcategory; const sub = itemData.subcategory;
this.subcategoryCwId = itemData.subcategoryCwId; const cat = sub?.category;
this.manufacturer = itemData.manufacturer; const mfr = itemData.manufacturer;
this.manufactureCwId = itemData.manufactureCwId;
this.category = cat?.name ?? null;
this.categoryCwId = cat?.id ?? null;
this.subcategory = sub?.name ?? null;
this.subcategoryCwId = sub?.id ?? null;
this.manufacturer = mfr?.name ?? null;
this.manufactureCwId = mfr?.id ?? null;
this.partNumber = itemData.partNumber; this.partNumber = itemData.partNumber;
this.vendorName = itemData.vendorName; this.vendorName = itemData.vendorName;
this.vendorSku = itemData.vendorSku; this.vendorSku = itemData.vendorSku;
@@ -97,7 +122,7 @@ export class CatalogItemController {
if (onHand !== this.onHand) { if (onHand !== this.onHand) {
await prisma.catalogItem.update({ await prisma.catalogItem.update({
where: { id: this.id }, where: { uid: this.id },
data: { onHand }, data: { onHand },
}); });
this.onHand = onHand; this.onHand = onHand;
@@ -137,7 +162,7 @@ export class CatalogItemController {
} }
const target = await prisma.catalogItem.findFirst({ const target = await prisma.catalogItem.findFirst({
where: { id: targetId }, where: { uid: targetId },
}); });
if (!target) { if (!target) {
@@ -150,9 +175,9 @@ export class CatalogItemController {
} }
const updated = await prisma.catalogItem.update({ const updated = await prisma.catalogItem.update({
where: { id: this.id }, where: { uid: this.id },
data: { data: {
linkedItems: { connect: { id: targetId } }, linkedItems: { connect: { uid: targetId } },
}, },
include: { linkedItems: true }, include: { linkedItems: true },
}); });
@@ -174,9 +199,9 @@ export class CatalogItemController {
*/ */
public async unlinkItem(targetId: string): Promise<CatalogItemController> { public async unlinkItem(targetId: string): Promise<CatalogItemController> {
const updated = await prisma.catalogItem.update({ const updated = await prisma.catalogItem.update({
where: { id: this.id }, where: { uid: this.id },
data: { data: {
linkedItems: { disconnect: { id: targetId } }, linkedItems: { disconnect: { uid: targetId } },
}, },
include: { linkedItems: true }, include: { linkedItems: true },
}); });
+209 -143
View File
@@ -1,204 +1,270 @@
import { Company } from "../../generated/prisma/client";
import { connectWiseApi } from "../constants";
import { fetchCwCompanyById } from "../modules/cw-utils/fetchCompany";
import { fetchCompanyConfigurations } from "../modules/cw-utils/configurations/fetchCompanyConfigurations";
import { updateCwInternalCompany } from "../modules/cw-utils/updateCompany";
import { import {
fetchCompanySites, Company,
fetchCompanySite, CompanyAddress,
serializeCwSite, Contact,
} from "../modules/cw-utils/sites/companySites"; } from "../../generated/prisma/client";
import { Company as CWCompany, Contact } from "../types/ConnectWiseTypes"; import { connectWiseApi, prisma } from "../constants";
import GenericError from "../Errors/GenericError";
import { processConfigurationResponse } from "../modules/cw-utils/configurations/processConfigurationResponse";
import { withCwRetry } from "../modules/cw-utils/withCwRetry";
import type { ConfigurationResponse } from "../types/ConnectWiseTypes";
// Type for company data with relations
type CompanyWithRelations = Company & {
contacts?: Contact[];
companyAddresses?: CompanyAddress[];
};
/** /**
* Company Controller * Company Controller
* *
* This class is for creating a controller that can manage company data, * This class manages company data from the local database.
* synchronize with external systems, and provide methods for accessing * Data is synced from ConnectWise via the dalpuri service.
* and updating company information within our internal system.
*/ */
export class CompanyController { export class CompanyController {
public readonly id: string; public readonly id: number;
public readonly uid: string;
public name: string; public name: string;
public readonly cw_Identifier: string; public phone: string | null;
public readonly cw_CompanyId: number; public website: string | null;
public cw_Data?: {
company: CWCompany;
defaultContact: Contact | null;
allContacts: Contact[];
};
constructor(companyData: Company, cwData?: typeof this.cw_Data) { private _contacts: Contact[] = [];
private _addresses: CompanyAddress[] = [];
private _defaultContact: Contact | null = null;
private _defaultAddress: CompanyAddress | null = null;
constructor(companyData: CompanyWithRelations) {
this.id = companyData.id; this.id = companyData.id;
this.uid = companyData.uid;
this.name = companyData.name; this.name = companyData.name;
this.cw_Identifier = companyData.cw_Identifier; this.phone = companyData.phone;
this.cw_CompanyId = companyData.cw_CompanyId; this.website = companyData.website;
this.cw_Data = cwData;
if (companyData.contacts) {
this._contacts = companyData.contacts;
this._defaultContact =
companyData.contacts.find((c) => c.default) ?? null;
}
if (companyData.companyAddresses) {
this._addresses = companyData.companyAddresses;
this._defaultAddress =
companyData.companyAddresses.find((a) => a.defaultFlag) ?? null;
}
} }
/** /**
* Hydrate CW Data * Hydrate Data
* *
* Fetches and populates the full ConnectWise company data * Loads contacts and addresses from the local database if not already loaded.
* (company, default contact, all contacts) if not already loaded.
*
* @returns {ThisType}
*/ */
public async hydrateCwData() { public async hydrateData() {
if (this.cw_Data) return this; if (this._contacts.length === 0) {
this._contacts = await prisma.contact.findMany({
where: { companyId: this.id },
});
this._defaultContact = this._contacts.find((c) => c.default) ?? null;
}
const cwCompany = await fetchCwCompanyById(this.cw_CompanyId); if (this._addresses.length === 0) {
if (!cwCompany) return this; this._addresses = await prisma.companyAddress.findMany({
where: { companyId: this.id },
const allContactsData = await connectWiseApi.get( });
`${cwCompany._info.contacts_href}&pageSize=1000`, this._defaultAddress = this._addresses.find((a) => a.defaultFlag) ?? null;
); }
// Derive default contact from allContacts instead of a separate CW call
const defaultContactId = cwCompany.defaultContact?.id;
const defaultContactData = defaultContactId
? ((allContactsData.data as any[]).find(
(c: any) => c.id === defaultContactId,
) ?? null)
: null;
this.cw_Data = {
company: cwCompany,
defaultContact: defaultContactData,
allContacts: allContactsData.data,
};
return this; return this;
} }
/** /**
* Refresh Internal Company Data from ConnectWise * Refresh from DB
* *
* This method fetches the latest company data from ConnectWise and updates * Reloads the company data from the local database.
* the internal company information accordingly.
*
* @returns {ThisType} - Updated Controller
*/ */
public async refreshFromCW() { public async refreshFromDb() {
const data = await updateCwInternalCompany(this.cw_CompanyId); const data = await prisma.company.findUnique({
where: { id: this.id },
});
this.name = data?.name || this.name; if (data) {
return this; this.name = data.name;
this.phone = data.phone;
this.website = data.website;
} }
/** return this;
* Fetch ConnectWise Company Data
*
* This method retrieves the latest company data directly from ConnectWise
* using the stored ConnectWise Company ID.
*
* @returns {Company}
*/
public async fetchCwData(): Promise<CWCompany | null> {
const data = await fetchCwCompanyById(this.cw_CompanyId);
return data;
} }
/** /**
* Fetch Company Configurations * Fetch Company Configurations
* *
* This method retrieves the configurations associated with * Fetches configurations directly from ConnectWise.
* the company from ConnectWise.
*
* @returns {ProcessedConfiguration}
*/ */
public async fetchConfigurations() { public async fetchConfigurations() {
const data = await fetchCompanyConfigurations(this.cw_CompanyId); const pageSize = 1000;
return data; const conditions = encodeURIComponent(`company/id=${this.id}`);
const configurations: ConfigurationResponse = [];
try {
for (let page = 1; ; page++) {
const response = await withCwRetry(
() =>
connectWiseApi.get<ConfigurationResponse>(
`/company/configurations?page=${page}&pageSize=${pageSize}&conditions=${conditions}`,
),
{ label: `company-configurations:${this.id}:page-${page}` },
);
const items = Array.isArray(response.data) ? response.data : [];
configurations.push(...items);
if (items.length < pageSize) break;
}
return processConfigurationResponse(configurations);
} catch (error: any) {
const cwStatus = Number(error?.response?.status);
const status = cwStatus >= 400 && cwStatus <= 599 ? cwStatus : 502;
throw new GenericError({
message: `Failed to fetch company configurations from ConnectWise`,
name: "ConnectWiseFetchFailed",
cause:
error?.response?.data?.message ??
error?.response?.statusText ??
error?.message ??
"Unknown ConnectWise error",
status,
});
}
} }
/** /**
* Fetch Company Sites * Fetch Company Sites
* *
* Retrieves all sites for this company from ConnectWise * Retrieves all sites (addresses) for this company from local DB.
* and returns them as serialized site objects.
*/ */
public async fetchSites() { public async fetchSites() {
const sites = await fetchCompanySites(this.cw_CompanyId); const sites = await prisma.companyAddress.findMany({
return sites.map(serializeCwSite); where: { companyId: this.id },
});
return sites.map((site) => ({
id: site.id,
name: site.name,
addressLine1: site.addressLine1,
addressLine2: site.addressLine2,
city: site.city,
state: site.state,
zip: site.zipCode,
country: site.country,
phone: site.phone,
fax: site.fax,
defaultFlag: site.defaultFlag,
inactiveFlag: site.inactiveFlag,
}));
} }
/** /**
* Fetch Company Site by ID * Fetch Company Site by ID
* *
* Retrieves a single site by its ConnectWise site ID * Retrieves a single site by its ID from local DB.
* and returns a serialized site object.
*
* @param cwSiteId - The ConnectWise site ID
*/ */
public async fetchSite(cwSiteId: number) { public async fetchSite(siteId: number) {
const site = await fetchCompanySite(this.cw_CompanyId, cwSiteId); const site = await prisma.companyAddress.findFirst({
return serializeCwSite(site); where: { id: siteId, companyId: this.id },
});
if (!site) return null;
return {
id: site.id,
name: site.name,
addressLine1: site.addressLine1,
addressLine2: site.addressLine2,
city: site.city,
state: site.state,
zip: site.zipCode,
country: site.country,
phone: site.phone,
fax: site.fax,
defaultFlag: site.defaultFlag,
inactiveFlag: site.inactiveFlag,
};
}
/**
* Get all contacts
*/
public getContacts() {
return this._contacts;
}
/**
* Get default contact
*/
public getDefaultContact() {
return this._defaultContact;
}
/**
* Get default address
*/
public getDefaultAddress() {
return this._defaultAddress;
} }
public toJson(opts?: { public toJson(opts?: {
includeAddress: boolean; includeAddress?: boolean;
includePrimaryContact: boolean; includePrimaryContact?: boolean;
includeAllContacts?: boolean; includeAllContacts?: boolean;
}) { }) {
return { const cw_Data: Record<string, unknown> = {};
id: this.id,
name: this.name, if (opts?.includeAddress) {
cw_Identifier: this.cw_Identifier, cw_Data.address = this._defaultAddress
cw_CompanyId: this.cw_CompanyId,
cw_Data: {
address: !opts?.includeAddress
? undefined
: {
line1: this.cw_Data?.company.addressLine1,
line2: this.cw_Data?.company.addressLine2 ?? null,
city: this.cw_Data?.company.city,
state: this.cw_Data?.company.state,
zip: this.cw_Data?.company.zip,
country: this.cw_Data?.company.country
? this.cw_Data.company.country.name
: "United States",
},
primaryContact: !opts?.includePrimaryContact
? undefined
: this.cw_Data?.defaultContact
? { ? {
firstName: this.cw_Data.defaultContact.firstName, line1: this._defaultAddress.addressLine1,
lastName: this.cw_Data.defaultContact.lastName, line2: this._defaultAddress.addressLine2,
cwId: this.cw_Data.defaultContact.id, city: this._defaultAddress.city,
inactive: this.cw_Data.defaultContact.inactiveFlag, state: this._defaultAddress.state,
title: this.cw_Data.defaultContact.title, zip: this._defaultAddress.zipCode,
phone: this.cw_Data.defaultContact.defaultPhoneNbr, country: this._defaultAddress.country ?? "US",
email: (() => {
if (!this.cw_Data?.defaultContact?.communicationItems)
return null;
return (
this.cw_Data.defaultContact.communicationItems.find(
(v) => v.type.name === "Email",
)?.value ?? null
);
})(),
} }
: null, : null;
allContacts: !opts?.includeAllContacts }
? undefined
: this.cw_Data?.allContacts.map((contact) => ({ if (opts?.includePrimaryContact) {
cw_Data.primaryContact = this._defaultContact
? {
firstName: this._defaultContact.firstName,
lastName: this._defaultContact.lastName,
cwId: this._defaultContact.id,
inactive: !this._defaultContact.active,
title: this._defaultContact.title,
phone: this._defaultContact.phone,
email: this._defaultContact.email,
}
: null;
}
if (opts?.includeAllContacts) {
cw_Data.allContacts = this._contacts.map((contact) => ({
firstName: contact.firstName, firstName: contact.firstName,
lastName: contact.lastName, lastName: contact.lastName,
cwId: contact.id, cwId: contact.id,
inactive: contact.inactiveFlag, inactive: !contact.active,
title: contact.title, title: contact.title,
phone: contact.defaultPhoneNbr, phone: contact.phone,
email: (() => { email: contact.email,
if (!contact.communicationItems) return null; }));
return ( }
contact.communicationItems.find(
(v) => v.type.name === "Email", return {
)?.value ?? null id: this.uid,
); name: this.name,
})(), cw_CompanyId: this.id,
})), cw_Data,
},
}; };
} }
} }
-19
View File
@@ -1,5 +1,4 @@
import type { CwMember } from "../../generated/prisma/client"; import type { CwMember } from "../../generated/prisma/client";
import type { CWMember } from "../modules/cw-utils/members/fetchAllMembers";
/** /**
* CW Member Controller * CW Member Controller
@@ -44,24 +43,6 @@ export class CwMemberController {
return name || this.identifier; 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 * To JSON
* *
@@ -8,6 +8,9 @@ import { CWForecastItem } from "../modules/cw-utils/opportunities/opportunity.ty
* locally all data is sourced directly from the ConnectWise API. * locally all data is sourced directly from the ConnectWise API.
*/ */
export class ForecastProductController { export class ForecastProductController {
private static readonly PROCUREMENT_NOTES_FIELD_ID = 29;
private static readonly PRODUCT_NARRATIVE_FIELD_ID = 46;
public readonly cwForecastId: number; public readonly cwForecastId: number;
public forecastDescription: string; public forecastDescription: string;
@@ -24,6 +27,8 @@ export class ForecastProductController {
public productDescription: string; public productDescription: string;
public customerDescription: string | null; public customerDescription: string | null;
public description: string | null;
public procurementNotes: string | null;
public productNarrative: string | null; public productNarrative: string | null;
public productClass: string; public productClass: string;
public forecastType: string; public forecastType: string;
@@ -49,6 +54,11 @@ export class ForecastProductController {
public cwLastUpdated: Date | null; public cwLastUpdated: Date | null;
public cwUpdatedBy: string | null; public cwUpdatedBy: string | null;
// Pricing data (from local ProductData)
public unitPrice: number;
public listPrice: number;
public discount: number;
// Cancellation data (from procurement products endpoint) // Cancellation data (from procurement products endpoint)
public cancelledFlag: boolean; public cancelledFlag: boolean;
public quantityCancelled: number; public quantityCancelled: number;
@@ -77,8 +87,23 @@ export class ForecastProductController {
this.productDescription = data.productDescription; this.productDescription = data.productDescription;
this.customerDescription = data.customerDescription ?? null; this.customerDescription = data.customerDescription ?? null;
this.description = null;
this.procurementNotes =
data.procurementNotes ??
data.customFields
?.find(
(f) => f.id === ForecastProductController.PROCUREMENT_NOTES_FIELD_ID
)
?.value?.toString() ??
null;
this.productNarrative = this.productNarrative =
data.customFields?.find((f) => f.id === 46)?.value?.toString() ?? null; data.productNarrative ??
data.customFields
?.find(
(f) => f.id === ForecastProductController.PRODUCT_NARRATIVE_FIELD_ID
)
?.value?.toString() ??
null;
this.productClass = data.productClass; this.productClass = data.productClass;
this.forecastType = data.forecastType; this.forecastType = data.forecastType;
@@ -105,6 +130,11 @@ export class ForecastProductController {
: null; : null;
this.cwUpdatedBy = data._info?.updatedBy ?? null; this.cwUpdatedBy = data._info?.updatedBy ?? null;
// Pricing defaults — enriched later via applyPricingData()
this.unitPrice = 0;
this.listPrice = 0;
this.discount = 0;
// Cancellation defaults — enriched later via applyCancellationData() // Cancellation defaults — enriched later via applyCancellationData()
this.cancelledFlag = false; this.cancelledFlag = false;
this.quantityCancelled = 0; this.quantityCancelled = 0;
@@ -133,8 +163,19 @@ export class ForecastProductController {
public applyProcurementCustomFields(data: { public applyProcurementCustomFields(data: {
customFields?: Array<{ id: number; value?: unknown }>; customFields?: Array<{ id: number; value?: unknown }>;
}): void { }): void {
const notes = data.customFields
?.find(
(f) => f.id === ForecastProductController.PROCUREMENT_NOTES_FIELD_ID
)
?.value?.toString();
if (notes) {
this.procurementNotes = notes;
}
const narrative = data.customFields const narrative = data.customFields
?.find((f) => f.id === 46) ?.find(
(f) => f.id === ForecastProductController.PRODUCT_NARRATIVE_FIELD_ID
)
?.value?.toString(); ?.value?.toString();
if (narrative) { if (narrative) {
this.productNarrative = narrative; this.productNarrative = narrative;
@@ -257,6 +298,7 @@ export class ForecastProductController {
: null, : null,
productDescription: this.productDescription, productDescription: this.productDescription,
customerDescription: this.customerDescription, customerDescription: this.customerDescription,
procurementNotes: this.procurementNotes,
productNarrative: this.productNarrative, productNarrative: this.productNarrative,
productClass: this.productClass, productClass: this.productClass,
forecastType: this.forecastType, forecastType: this.forecastType,
@@ -281,6 +323,25 @@ export class ForecastProductController {
cwUpdatedBy: this.cwUpdatedBy, cwUpdatedBy: this.cwUpdatedBy,
onHand: this.onHand, onHand: this.onHand,
inStock: this.inStock, inStock: this.inStock,
unitPrice: this.unitPrice,
listPrice: this.listPrice,
discount: this.discount,
}; };
} }
/**
* Apply Pricing Data
*
* Enriches this forecast product with pricing data from the local
* ProductData table.
*/
public applyPricingData(data: {
unitPrice?: number;
listPrice?: number;
discount?: number;
}): void {
this.unitPrice = data.unitPrice ?? 0;
this.listPrice = data.listPrice ?? 0;
this.discount = data.discount ?? 0;
}
} }
@@ -33,7 +33,7 @@ export class GeneratedQuoteController {
data: GeneratedQuotes & { data: GeneratedQuotes & {
opportunity?: Opportunity | null; opportunity?: Opportunity | null;
createdBy?: (User & { roles: Role[] }) | null; createdBy?: (User & { roles: Role[] }) | null;
}, }
) { ) {
this.id = data.id; this.id = data.id;
@@ -67,7 +67,7 @@ export class GeneratedQuoteController {
if (this._opportunity) return this._opportunity; if (this._opportunity) return this._opportunity;
const opportunity = await prisma.opportunity.findFirst({ const opportunity = await prisma.opportunity.findFirst({
where: { id: this.opportunityId }, where: { uid: this.opportunityId },
}); });
if (!opportunity) return null; if (!opportunity) return null;
File diff suppressed because it is too large Load Diff
+8 -5
View File
@@ -89,7 +89,7 @@ export class RoleController {
}); });
throw new PermissionsVerificationError( throw new PermissionsVerificationError(
`Unable to verify permissions for role '${this.title}, it is recommended that you override and rewrite these permissions immediately.`, `Unable to verify permissions for role '${this.title}, it is recommended that you override and rewrite these permissions immediately.`,
(err as Error).message, (err as Error).message
); );
} }
@@ -261,14 +261,14 @@ export class RoleController {
title: string; title: string;
moniker: string; moniker: string;
permissions: string[]; permissions: string[];
}>, }>
) { ) {
const schema = z const schema = z
.object({ .object({
title: z.string().min(1, "Title cannot be empty."), title: z.string().min(1, "Title cannot be empty."),
moniker: z.string().min(1, "Moniker cannot be empty."), moniker: z.string().min(1, "Moniker cannot be empty."),
permissions: z.array( permissions: z.array(
z.string().min(1, "Permission node cannot be empty"), z.string().min(1, "Permission node cannot be empty")
), ),
}) })
.partial() .partial()
@@ -284,7 +284,7 @@ export class RoleController {
if (checkMoniker && checkMoniker.moniker !== this.moniker) if (checkMoniker && checkMoniker.moniker !== this.moniker)
throw new RoleError( throw new RoleError(
"Moniker is already taken.", "Moniker is already taken.",
"Another role with this moniker already exists in the databse.", "Another role with this moniker already exists in the databse."
); );
} }
@@ -337,7 +337,10 @@ export class RoleController {
users: opts?.viewUsers users: opts?.viewUsers
? this._users.map((v) => ({ ? this._users.map((v) => ({
id: v.id, id: v.id,
name: v.name, name:
`${v.firstName ?? ""} ${v.lastName ?? ""}`.trim() ||
v.login ||
v.email,
login: v.login, login: v.login,
roles: v.roles.map((r: any) => r.id), roles: v.roles.map((r: any) => r.id),
})) }))
+84
View File
@@ -0,0 +1,84 @@
import {
Schedule,
ScheduleStatus,
ScheduleType,
ScheduleSpan,
} from "../../generated/prisma/client";
type ScheduleWithRelations = Schedule & {
status?: ScheduleStatus | null;
type?: ScheduleType | null;
scheduleSpan?: ScheduleSpan | null;
};
export class ScheduleController {
public readonly id: number;
public readonly uid: string;
public name: string;
public description: string | null;
private _data: ScheduleWithRelations;
constructor(data: ScheduleWithRelations) {
this.id = data.id;
this.uid = data.uid;
this.name = data.name;
this.description = data.description;
this._data = data;
}
public toJson() {
const d = this._data;
return {
id: d.uid,
cwId: d.id,
memberId: d.memberId,
name: d.name,
description: d.description,
closedFlag: d.closedFlag,
reminderFlag: d.reminderFlag,
allDayFlag: d.allDayFlag,
acknowledgementFlag: d.acknowledgementFlag,
meetingFlag: d.meetingFlag,
recurringFlag: d.recurringFlag,
billableFlag: d.billableFlag,
acknowledgedById: d.acknowledgedById,
acknowledgedAt: d.acknowledgedAt,
startDate: d.startDate,
endDate: d.endDate,
hoursScheduled: d.hoursScheduled,
duration: d.duration,
hoursPerDay: d.hoursPerDay,
reminderMinutes: d.reminderMinutes,
closedById: d.closedById,
closedAt: d.closedAt,
createdById: d.createdById,
updatedById: d.updatedById,
createdAt: d.createdAt,
updatedAt: d.updatedAt,
status: d.status
? {
id: d.status.id,
uid: d.status.uid,
name: d.status.name,
color: d.status.color,
}
: null,
type: d.type
? {
id: d.type.id,
uid: d.type.uid,
name: d.type.name,
displayColor: d.type.displayColor,
}
: null,
scheduleSpan: d.scheduleSpan
? {
id: d.scheduleSpan.id,
scheduleSpanId: d.scheduleSpan.scheduleSpanId,
spanDesc: d.scheduleSpan.spanDesc,
}
: null,
};
}
}
+59 -12
View File
@@ -1,6 +1,5 @@
import { Collection } from "@discordjs/collection"; import { Collection } from "@discordjs/collection";
import { Role } from "../../generated/prisma/client"; import { Role, User } from "../../generated/prisma/client";
import { User } from "../../generated/prisma/browser";
import { SessionTokensObject } from "./SessionController"; import { SessionTokensObject } from "./SessionController";
import { sessions } from "../managers/sessions"; import { sessions } from "../managers/sessions";
import BodyError from "../Errors/BodyError"; import BodyError from "../Errors/BodyError";
@@ -15,11 +14,13 @@ import { permissionsPrivateKey } from "../constants";
export default class UserController { export default class UserController {
public id: string; public id: string;
public name: string | null; public firstName: string | null;
public lastName: string | null;
public login: string; public login: string;
public email: string; public email: string;
public image: string | null; public image: string | null;
public cwIdentifier: string | null; public cwIdentifier: string | null;
public cwMemberId: number | null;
private _roles: Collection<string, Role>; private _roles: Collection<string, Role>;
private _permissions: string | null; private _permissions: string | null;
@@ -33,13 +34,38 @@ export default class UserController {
public createdAt: Date; public createdAt: Date;
public updatedAt: Date; public updatedAt: Date;
public get name(): string | null {
const full = [this.firstName, this.lastName]
.filter(Boolean)
.join(" ")
.trim();
return full.length > 0 ? full : null;
}
private _splitName(name: string): {
firstName: string | null;
lastName: string | null;
} {
const trimmed = name.trim();
if (!trimmed) return { firstName: null, lastName: null };
const parts = trimmed.split(/\s+/);
const firstName = parts.shift() ?? null;
const lastName = parts.length > 0 ? parts.join(" ") : null;
return { firstName, lastName };
}
constructor(userdata: User & { roles: Role[] }) { constructor(userdata: User & { roles: Role[] }) {
this.id = userdata.id; this.id = userdata.id;
this.name = userdata.name; this.firstName = userdata.firstName ?? null;
this.lastName = userdata.lastName ?? null;
this.login = userdata.login; this.login = userdata.login;
this.email = userdata.email; this.email = userdata.email;
this.image = userdata.image; this.image = userdata.image;
this.cwIdentifier = userdata.cwIdentifier ?? null; this.cwIdentifier = userdata.cwIdentifier ?? null;
this.cwMemberId = userdata.cwMemberId ?? null;
this.updatedAt = userdata.updatedAt; this.updatedAt = userdata.updatedAt;
this.createdAt = userdata.createdAt; this.createdAt = userdata.createdAt;
this._permissions = userdata.permissions ?? null; this._permissions = userdata.permissions ?? null;
@@ -62,11 +88,13 @@ export default class UserController {
*/ */
private _updateInternalValues(userdata: User) { private _updateInternalValues(userdata: User) {
this.id = userdata.id; this.id = userdata.id;
this.name = userdata.name; this.firstName = userdata.firstName ?? null;
this.lastName = userdata.lastName ?? null;
this.login = userdata.login; this.login = userdata.login;
this.email = userdata.email; this.email = userdata.email;
this.image = userdata.image; this.image = userdata.image;
this.cwIdentifier = userdata.cwIdentifier ?? null; this.cwIdentifier = userdata.cwIdentifier ?? null;
this.cwMemberId = userdata.cwMemberId ?? null;
this.updatedAt = userdata.updatedAt; this.updatedAt = userdata.updatedAt;
this.createdAt = userdata.createdAt; this.createdAt = userdata.createdAt;
} }
@@ -92,17 +120,33 @@ export default class UserController {
* @param data - A partial of the user data * @param data - A partial of the user data
* @returns {Promise<UserController>} - The updated user controller * @returns {Promise<UserController>} - The updated user controller
*/ */
public async update(data: Partial<Pick<User, "name" | "image">>) { public async update(
if (Object.keys(data).length == 0) data: Partial<Pick<User, "firstName" | "lastName" | "image">> & {
name?: string;
}
) {
const updateData: Partial<Pick<User, "firstName" | "lastName" | "image">> =
{};
if (data.image !== undefined) updateData.image = data.image;
if (data.name !== undefined) {
const parsed = this._splitName(data.name);
updateData.firstName = parsed.firstName;
updateData.lastName = parsed.lastName;
}
if (data.firstName !== undefined) updateData.firstName = data.firstName;
if (data.lastName !== undefined) updateData.lastName = data.lastName;
if (Object.keys(updateData).length == 0)
throw new BodyError("Body cannot be empty."); throw new BodyError("Body cannot be empty.");
const updatedUser = await prisma.user.update({ const updatedUser = await prisma.user.update({
where: { id: this.id }, where: { id: this.id },
data, data: updateData,
}); });
this._updateInternalValues(updatedUser); this._updateInternalValues(updatedUser);
events.emit("user:updated", { user: this, updatedValues: data }); events.emit("user:updated", { user: this, updatedValues: updateData });
return this; return this;
} }
@@ -118,7 +162,7 @@ export default class UserController {
*/ */
public async setRoles(roleIdentifiers: string[]): Promise<UserController> { public async setRoles(roleIdentifiers: string[]): Promise<UserController> {
const resolvedRoles = await Promise.all( const resolvedRoles = await Promise.all(
roleIdentifiers.map((identifier) => roles.fetch(identifier)), roleIdentifiers.map((identifier) => roles.fetch(identifier))
); );
const updatedUser = await prisma.user.update({ const updatedUser = await prisma.user.update({
@@ -242,8 +286,8 @@ export default class UserController {
await Promise.all( await Promise.all(
this._roles.map(async (v) => this._roles.map(async (v) =>
collection.set(v.id, await roles.fetch(v.id)), collection.set(v.id, await roles.fetch(v.id))
), )
); );
return collection; return collection;
@@ -307,6 +351,8 @@ export default class UserController {
return { return {
id: this.id, id: this.id,
name: this.name, name: this.name,
firstName: this.firstName,
lastName: this.lastName,
roles: opts?.safeReturn roles: opts?.safeReturn
? undefined ? undefined
: this._roles.size > 0 : this._roles.size > 0
@@ -325,6 +371,7 @@ export default class UserController {
login: opts?.safeReturn ? undefined : this.login, login: opts?.safeReturn ? undefined : this.login,
email: opts?.safeReturn ? undefined : this.email, email: opts?.safeReturn ? undefined : this.email,
cwIdentifier: opts?.safeReturn ? undefined : this.cwIdentifier, cwIdentifier: opts?.safeReturn ? undefined : this.cwIdentifier,
cwMemberId: this.cwMemberId,
image: this.image, image: this.image,
createdAt: this.createdAt, createdAt: this.createdAt,
updatedAt: this.updatedAt, updatedAt: this.updatedAt,
+52 -168
View File
@@ -1,61 +1,32 @@
import { refresh } from "./api/auth";
import app from "./api/server"; import app from "./api/server";
import { setupSockets } from "./api/sockets"; import { setupSockets } from "./api/sockets";
import { import { engine, PORT, prisma } from "./constants";
COLLECTOR_WS_URL,
connectCollectorSocket,
collectorSocket,
engine,
PORT,
prisma,
unifi,
unifiPassword,
unifiUsername,
} from "./constants";
import { unifiSites } from "./managers/unifiSites"; import { unifiSites } from "./managers/unifiSites";
import { refreshCompanies } from "./modules/cw-utils/refreshCompanies";
import { refreshCatalog } from "./modules/cw-utils/procurement/refreshCatalog";
import { refreshInventory } from "./modules/cw-utils/procurement/refreshInventory";
import { listenInventoryAdjustments } from "./modules/cw-utils/procurement/listenInventoryAdjustments";
import { refreshSalesOpportunityMetricsCache } from "./modules/cache/salesOpportunityMetricsCache";
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 } from "./modules/globalEvents"; import { events } from "./modules/globalEvents";
import { setupEventDebugger } from "./modules/logging/eventDebugger"; import { setupEventDebugger } from "./modules/logging/eventDebugger";
import { signPermissions } from "./modules/permission-utils/signPermissions"; import { signPermissions } from "./modules/permission-utils/signPermissions";
import { RoleController } from "./controllers/RoleController"; import { RoleController } from "./controllers/RoleController";
import cuid from "cuid"; import { initializeWorkerSystem, getBoss } from "./workert";
import { WorkerQueue } from "./modules/workers/queues";
import { enqueueIncrementalSync } from "./modules/workers/incremental-sync";
import { startCommsServer } from "./modules/workers/coms"; import { startCommsServer } from "./modules/workers/coms";
import { import cuid from "cuid";
enqueueActiveOpportunityRefreshJob,
startOpportunityCacheWorkers, const startupArgs = new Set(Bun.argv.slice(2));
} from "./workert"; const simpleTerminalMode =
startupArgs.has("-st") || startupArgs.has("--simple-terminal");
// Setup global event debugger in non-production environments // Setup global event debugger in non-production environments
if (Bun.env.NODE_ENV == "development") { if (Bun.env.NODE_ENV == "development" && !simpleTerminalMode) {
setupEventDebugger({ processLabel: "API" }); setupEventDebugger({ processLabel: "API" });
} }
/** Concise error message for interval logs — avoids dumping full Axios error objects. */ // Helper to run a startup task safely — failures are logged but never crash the process.
const briefErr = (err: any): string => {
if (err?.isAxiosError) {
const method = (err.config?.method ?? "?").toUpperCase();
const url = err.config?.url ?? "?";
return `${method} ${url}${err.code ?? `HTTP ${err.response?.status}`}`;
}
return err?.message ?? String(err);
};
// Helper to run a startup sync safely — failures are logged but never crash the process.
const safeStartup = async (label: string, fn: () => Promise<void>) => { const safeStartup = async (label: string, fn: () => Promise<void>) => {
try { try {
await fn(); await fn();
} catch (err) { } catch (err) {
console.error( console.error(`[startup] ${label} failed`, err);
`[startup] ${label} failed — will retry on next interval`,
err,
);
} }
}; };
@@ -83,33 +54,30 @@ console.log(`[startup] Server listening on port ${PORT}`);
setupSockets(); setupSockets();
console.log("[startup] Socket namespaces initialized"); console.log("[startup] Socket namespaces initialized");
collectorSocket.on("connect", () => { // Initialize worker system (PgBoss connection)
console.log( await safeStartup("initializeWorkerSystem", () => initializeWorkerSystem());
`[startup] Collector socket connected: ${COLLECTOR_WS_URL} (${collectorSocket.id})`,
);
});
collectorSocket.on("connect_error", (err) => {
console.error(`[startup] Collector socket connect_error: ${err.message}`);
});
connectCollectorSocket();
console.log("[startup] Collector socket initialization started");
// Start the inter-process comms server so the worker can connect on :8671
startCommsServer(); startCommsServer();
console.log("[startup] Worker comms server initialized"); console.log("[startup] Comms server listening on :8671");
const embeddedWorkersEnabled = Bun.env.START_EMBEDDED_WORKERS === "true"; // Enqueue a full dalpuri sync on startup
if (embeddedWorkersEnabled) { await safeStartup("enqueueDalpuriFullSync", async () => {
await safeStartup( const jobId = await getBoss().send(WorkerQueue.DALPURI_FULL_SYNC, {}, { singletonKey: `startup-${Date.now()}` });
"startOpportunityCacheWorkers", if (jobId) {
startOpportunityCacheWorkers, console.log(`[startup] Dalpuri full sync enqueued: ${jobId}`);
);
} else { } else {
console.log( console.warn("[startup] Dalpuri full sync send returned null — job may already be pending or PgBoss not ready");
"[startup] Embedded opportunity workers disabled on API node (set START_EMBEDDED_WORKERS=true to override)",
);
} }
});
// Broadcast incremental sync jobs from the API process every 5s so the
// interval survives worker restarts.
setInterval(() => {
enqueueIncrementalSync().catch((err) =>
console.error(`[interval] enqueueIncrementalSync failed: ${err?.message ?? err}`)
);
}, 5_000);
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Background initialisation — none of this blocks the server. // Background initialisation — none of this blocks the server.
@@ -141,114 +109,30 @@ await safeStartup("ensureAdminRole", async () => {
} }
}); });
// Refresh the internal list of companies every minute // Enqueue an initial cold-load metrics refresh on startup
await safeStartup("refreshCompanies", refreshCompanies); await safeStartup("enqueueSalesMetricsRefresh", async () => {
const jobId = await getBoss().send(
WorkerQueue.REFRESH_SALES_METRICS,
{ forceColdLoad: true },
{ singletonKey: `startup-metrics-${Date.now()}` }
);
if (jobId) {
console.log(`[startup] Sales metrics refresh enqueued: ${jobId}`);
} else {
console.warn("[startup] Sales metrics refresh send returned null — job may already be pending");
}
});
// Enqueue a metrics refresh every 5 minutes
setInterval(() => { setInterval(() => {
return refreshCompanies().catch((err) => getBoss()
console.error(`[interval] refreshCompanies failed: ${briefErr(err)}`), .send(WorkerQueue.REFRESH_SALES_METRICS, {}, { singletonKey: "metrics-interval" })
);
}, 60 * 1000);
// Refresh the internal catalog every 30 minutes
await safeStartup("refreshCatalog", refreshCatalog);
setInterval(
() => {
return refreshCatalog().catch((err) =>
console.error(`[interval] refreshCatalog failed: ${briefErr(err)}`),
);
},
30 * 60 * 1000,
);
// Fallback full inventory sweep every 6 hours (listener handles real-time deltas)
setInterval(
() => {
return refreshInventory().catch((err) =>
console.error(`[interval] refreshInventory failed: ${briefErr(err)}`),
);
},
6 * 60 * 60 * 1000,
);
// Listen for procurement adjustment changes and sync changed products to DB + cache
await safeStartup("listenInventoryAdjustments", listenInventoryAdjustments);
setInterval(() => {
return listenInventoryAdjustments().catch((err) =>
console.error(
`[interval] listenInventoryAdjustments failed: ${briefErr(err)}`,
),
);
}, 60 * 1000);
// Refresh opportunity CW cache every 20 minutes (activities + company hydration)
// NOTE: Do NOT await — register the interval immediately so the cache refresh
// is never blocked by a slow/stuck startup task above.
safeStartup("enqueueActiveOpportunityRefreshJob", async () => {
await enqueueActiveOpportunityRefreshJob();
});
setInterval(
() => {
return enqueueActiveOpportunityRefreshJob().catch((err) => {
console.error(
`[interval] enqueueActiveOpportunityRefreshJob failed: ${briefErr(err)}`,
);
});
},
20 * 60 * 1000,
);
// Refresh User Defined Fields every 5 minutes
await safeStartup("refreshUDFs", async () => {
await userDefinedFieldsCw.refresh();
});
setInterval(
() => {
return userDefinedFieldsCw
.refresh()
.catch((err) => .catch((err) =>
console.error(`[interval] refreshUDFs failed: ${briefErr(err)}`), console.error(`[interval] REFRESH_SALES_METRICS enqueue failed: ${err?.message ?? err}`)
);
},
5 * 60 * 1000,
);
// Refresh sales opportunity metrics cache for active CW members every 5 minutes
await safeStartup("refreshSalesOpportunityMetricsCache", () =>
refreshSalesOpportunityMetricsCache({ forceColdLoad: true }),
);
setInterval(
() => {
return refreshSalesOpportunityMetricsCache().catch((err) =>
console.error(
`[interval] refreshSalesOpportunityMetricsCache failed: ${briefErr(err)}`,
),
);
},
5 * 60 * 1000,
);
// Refresh CW identifiers for all users every 30 minutes
await safeStartup("refreshCwIdentifiers", refreshCwIdentifiers);
setInterval(
() => {
return refreshCwIdentifiers().catch((err) =>
console.error(`[interval] refreshCwIdentifiers failed: ${briefErr(err)}`),
);
},
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,
); );
}, 5 * 60 * 1000);
// Sync UniFi sites
await safeStartup("syncSites", async () => { await safeStartup("syncSites", async () => {
await unifiSites.syncSites(); await unifiSites.syncSites();
}); });
@@ -256,6 +140,6 @@ setInterval(() => {
return unifiSites return unifiSites
.syncSites() .syncSites()
.catch((err) => .catch((err) =>
console.error(`[interval] syncSites failed: ${briefErr(err)}`), console.error(`[interval] syncSites failed: ${err?.message ?? err}`)
); );
}, 60 * 1000); }, 60 * 1000);
+13 -72
View File
@@ -1,5 +1,4 @@
import { ActivityController } from "../controllers/ActivityController"; import { ActivityController } from "../controllers/ActivityController";
import { connectWiseApi } from "../constants";
import GenericError from "../Errors/GenericError"; import GenericError from "../Errors/GenericError";
import { activityCw } from "../modules/cw-utils/activities/activities"; import { activityCw } from "../modules/cw-utils/activities/activities";
import { import {
@@ -18,28 +17,20 @@ export const activities = {
* @returns {Promise<ActivityController>} * @returns {Promise<ActivityController>}
*/ */
async fetchItem(cwActivityId: number): Promise<ActivityController> { async fetchItem(cwActivityId: number): Promise<ActivityController> {
try { // TODO: Query local Activity table when synced by dalpuri
const cwData = await activityCw.fetch(cwActivityId);
return new ActivityController(cwData);
} catch (error) {
const errBody = (error as any).response?.data || error;
throw new GenericError({ throw new GenericError({
name: "FetchActivityError", name: "NotAvailable",
message: `Failed to fetch activity ${cwActivityId}`, message: "Activity fetch from local DB not yet implemented",
cause: typeof errBody === "string" ? errBody : JSON.stringify(errBody), status: 501,
status: (error as any).status ?? 502,
}); });
}
}, },
/** /**
* Fetch All Activities (Paginated) * Fetch All Activities (Paginated)
* *
* Fetches activities from ConnectWise with optional conditions and pagination.
*
* @param page - Page number (1-based) * @param page - Page number (1-based)
* @param rpp - Records per page * @param rpp - Records per page
* @param conditions - Optional CW conditions string for filtering * @param conditions - Optional conditions string for filtering
* @returns {Promise<ActivityController[]>} * @returns {Promise<ActivityController[]>}
*/ */
async fetchPages( async fetchPages(
@@ -47,73 +38,32 @@ export const activities = {
rpp: number, rpp: number,
conditions?: string, conditions?: string,
): Promise<ActivityController[]> { ): Promise<ActivityController[]> {
try { // TODO: Query local Activity table when synced by dalpuri
const pageNum = Math.max(page, 1); return [];
const conditionsParam = conditions
? `&conditions=${encodeURIComponent(conditions)}`
: "";
const response = await connectWiseApi.get(
`/sales/activities?page=${pageNum}&pageSize=${rpp}${conditionsParam}`,
);
const items = response.data;
return items.map((item: any) => new ActivityController(item));
} catch (error) {
const errBody = (error as any).response?.data || error;
throw new GenericError({
name: "FetchActivitiesError",
message: "Failed to fetch activities from ConnectWise",
cause: typeof errBody === "string" ? errBody : JSON.stringify(errBody),
status: 502,
});
}
}, },
/** /**
* Fetch Activities by Company * Fetch Activities by Company
* *
* Fetches all activities for a company by its ConnectWise company ID.
*
* @param cwCompanyId - The ConnectWise company ID * @param cwCompanyId - The ConnectWise company ID
* @returns {Promise<ActivityController[]>} * @returns {Promise<ActivityController[]>}
*/ */
async fetchByCompany(cwCompanyId: number): Promise<ActivityController[]> { async fetchByCompany(cwCompanyId: number): Promise<ActivityController[]> {
try { // TODO: Query local Activity table when synced by dalpuri
const collection = await activityCw.fetchByCompany(cwCompanyId); return [];
return collection.map((item) => new ActivityController(item));
} catch (error) {
const errBody = (error as any).response?.data || error;
throw new GenericError({
name: "FetchCompanyActivitiesError",
message: `Failed to fetch activities for company ${cwCompanyId}`,
cause: typeof errBody === "string" ? errBody : JSON.stringify(errBody),
status: 502,
});
}
}, },
/** /**
* Fetch Activities by Opportunity * Fetch Activities by Opportunity
* *
* Fetches all activities for an opportunity by its ConnectWise opportunity ID.
*
* @param cwOpportunityId - The ConnectWise opportunity ID * @param cwOpportunityId - The ConnectWise opportunity ID
* @returns {Promise<ActivityController[]>} * @returns {Promise<ActivityController[]>}
*/ */
async fetchByOpportunity( async fetchByOpportunity(
cwOpportunityId: number, cwOpportunityId: number,
): Promise<ActivityController[]> { ): Promise<ActivityController[]> {
try { // TODO: Query local Activity table when synced by dalpuri
const collection = await activityCw.fetchByOpportunity(cwOpportunityId); return [];
return collection.map((item) => new ActivityController(item));
} catch (error) {
const errBody = (error as any).response?.data || error;
throw new GenericError({
name: "FetchOpportunityActivitiesError",
message: `Failed to fetch activities for opportunity ${cwOpportunityId}`,
cause: typeof errBody === "string" ? errBody : JSON.stringify(errBody),
status: 502,
});
}
}, },
/** /**
@@ -196,16 +146,7 @@ export const activities = {
* @returns {Promise<number>} * @returns {Promise<number>}
*/ */
async count(conditions?: string): Promise<number> { async count(conditions?: string): Promise<number> {
try { // TODO: Count from local Activity table when synced by dalpuri
return await activityCw.countItems(conditions); return 0;
} catch (error) {
const errBody = (error as any).response?.data || error;
throw new GenericError({
name: "CountActivitiesError",
message: "Failed to count activities in ConnectWise",
cause: typeof errBody === "string" ? errBody : JSON.stringify(errBody),
status: 502,
});
}
}, },
}; };
+16 -26
View File
@@ -1,35 +1,23 @@
import { connectWiseApi, prisma } from "../constants"; import { prisma } from "../constants";
import { CompanyController } from "../controllers/CompanyController"; import { CompanyController } from "../controllers/CompanyController";
import { Company } from "../types/ConnectWiseTypes";
export const companies = { export const companies = {
async fetch(identifier: string | number): Promise<CompanyController> { async fetch(identifier: string | number): Promise<CompanyController> {
const search = await prisma.company.findFirst({ const isNumeric =
where: { typeof identifier === "number" || /^\d+$/.test(String(identifier));
OR: [{ id: identifier as string }],
const company = await prisma.company.findFirst({
where: isNumeric
? { id: Number(identifier) }
: {
OR: [{ uid: String(identifier) }, { name: String(identifier) }],
}, },
include: { contacts: true, companyAddresses: true },
}); });
if (!search) throw new Error("Unknown company."); if (!company) throw new Error("Unknown company.");
const freshCwData: { data: Company } = await connectWiseApi.get( return new CompanyController(company);
`/company/companies/${search.cw_CompanyId}`,
);
const contactHref = freshCwData.data.defaultContact?._info?.contact_href;
const defaultContactData = contactHref
? await connectWiseApi.get(contactHref)
: undefined;
const allContactsData = await connectWiseApi.get(
`${freshCwData.data._info.contacts_href}&pageSize=1000`,
);
return new CompanyController(search, {
company: freshCwData.data,
defaultContact: defaultContactData?.data ?? null,
allContacts: allContactsData.data,
});
}, },
async count() { async count() {
@@ -75,12 +63,14 @@ export const companies = {
const skip = (page > 1 ? page : 0) * rpp; const skip = (page > 1 ? page : 0) * rpp;
const take = rpp ?? 30; const take = rpp ?? 30;
const numericQuery = parseInt(query, 10);
const data = prisma.company.findMany({ const data = prisma.company.findMany({
where: { where: {
OR: [ OR: [
{ cw_Identifier: { contains: query, mode: "insensitive" } },
{ name: { contains: query, mode: "insensitive" } }, { name: { contains: query, mode: "insensitive" } },
{ id: { contains: query, mode: "insensitive" } }, { uid: { contains: query, mode: "insensitive" } },
...(!isNaN(numericQuery) ? [{ id: numericQuery }] : []),
], ],
}, },
skip, skip,
+2 -3
View File
@@ -2,6 +2,7 @@ import { prisma } from "../constants";
import { CredentialTypeController } from "../controllers/CredentialTypeController"; import { CredentialTypeController } from "../controllers/CredentialTypeController";
import { CredentialTypeField } from "../modules/credentials/credentialTypeDefs"; import { CredentialTypeField } from "../modules/credentials/credentialTypeDefs";
import GenericError from "../Errors/GenericError"; import GenericError from "../Errors/GenericError";
import { Prisma } from "@prisma/client";
export const credentialTypes = { export const credentialTypes = {
/** /**
@@ -83,7 +84,7 @@ export const credentialTypes = {
data: { data: {
name: data.name, name: data.name,
permissionScope: data.permissionScope, permissionScope: data.permissionScope,
fields: data.fields as any, fields: data.fields as Prisma.JsonArray,
icon: data.icon, icon: data.icon,
}, },
include: { include: {
@@ -91,8 +92,6 @@ export const credentialTypes = {
}, },
}); });
console.log(credentialType.fields);
return new CredentialTypeController(credentialType); return new CredentialTypeController(credentialType);
}, },
+15 -13
View File
@@ -8,6 +8,7 @@ import {
} from "../modules/credentials/credentialTypeDefs"; } from "../modules/credentials/credentialTypeDefs";
import { generateSecureValue } from "../modules/credentials/generateSecureValue"; import { generateSecureValue } from "../modules/credentials/generateSecureValue";
import GenericError from "../Errors/GenericError"; import GenericError from "../Errors/GenericError";
import { Prisma } from "@prisma/client";
/** /**
* Standard include clause used by every credential query. * Standard include clause used by every credential query.
@@ -115,7 +116,8 @@ export const credentials = {
}); });
} }
const typeFields = credentialType.fields! as any as CredentialTypeField[]; const typeFields =
credentialType.fields! as Prisma.JsonArray as CredentialTypeField[];
// Validate the fields against acceptable fields (exclude multi-credential fields // Validate the fields against acceptable fields (exclude multi-credential fields
// from value validation since they don't carry a direct value). // from value validation since they don't carry a direct value).
@@ -129,16 +131,16 @@ export const credentials = {
})) as CredentialTypeField[]; })) as CredentialTypeField[];
const validatedFields = await fieldValidator( const validatedFields = await fieldValidator(
data.fields as any as CredentialField[], data.fields as Prisma.JsonArray as CredentialField[],
acceptableFields, acceptableFields
); );
// Separate secure, non-secure, and multi-credential fields // Separate secure, non-secure, and multi-credential fields
const secureFields = validatedFields.filter( const secureFields = validatedFields.filter(
(f) => f.secure && !f.isMultiCredential, (f) => f.secure && !f.isMultiCredential
); );
const nonSecureFields = validatedFields.filter( const nonSecureFields = validatedFields.filter(
(f) => !f.secure && !f.isMultiCredential, (f) => !f.secure && !f.isMultiCredential
); );
// Build fields object for non-secure fields // Build fields object for non-secure fields
@@ -181,7 +183,7 @@ export const credentials = {
// Create inline sub-credentials when provided // Create inline sub-credentials when provided
if (data.subCredentials) { if (data.subCredentials) {
for (const [fieldId, subCredDataList] of Object.entries( for (const [fieldId, subCredDataList] of Object.entries(
data.subCredentials, data.subCredentials
)) { )) {
const fieldDef = typeFields.find((f) => f.id === fieldId); const fieldDef = typeFields.find((f) => f.id === fieldId);
@@ -200,8 +202,8 @@ export const credentials = {
for (const subCredData of subCredDataList) { for (const subCredData of subCredDataList) {
const validatedSubFields = await fieldValidator( const validatedSubFields = await fieldValidator(
subCredData.fields as any as CredentialField[], subCredData.fields as Prisma.JsonArray as CredentialField[],
subFieldDefs, subFieldDefs
); );
const subSecure = validatedSubFields.filter((f) => f.secure); const subSecure = validatedSubFields.filter((f) => f.secure);
@@ -267,7 +269,7 @@ export const credentials = {
data: { data: {
name: string; name: string;
fields: { fieldId: string; value: string }[]; fields: { fieldId: string; value: string }[];
}, }
): Promise<CredentialController> { ): Promise<CredentialController> {
const parent = await prisma.credential.findFirst({ const parent = await prisma.credential.findFirst({
where: { id: parentId }, where: { id: parentId },
@@ -297,8 +299,8 @@ export const credentials = {
const subFieldDefs = (fieldDef.subFields ?? []) as CredentialTypeField[]; const subFieldDefs = (fieldDef.subFields ?? []) as CredentialTypeField[];
const validatedFields = await fieldValidator( const validatedFields = await fieldValidator(
data.fields as any as CredentialField[], data.fields as Prisma.JsonArray as CredentialField[],
subFieldDefs, subFieldDefs
); );
const secureFields = validatedFields.filter((f) => f.secure); const secureFields = validatedFields.filter((f) => f.secure);
@@ -352,7 +354,7 @@ export const credentials = {
*/ */
async removeSubCredential( async removeSubCredential(
parentId: string, parentId: string,
subCredentialId: string, subCredentialId: string
): Promise<void> { ): Promise<void> {
const subCredential = await prisma.credential.findFirst({ const subCredential = await prisma.credential.findFirst({
where: { id: subCredentialId, subCredentialOfId: parentId }, where: { id: subCredentialId, subCredentialOfId: parentId },
@@ -382,7 +384,7 @@ export const credentials = {
for (const key of Object.keys(parentFields)) { for (const key of Object.keys(parentFields)) {
if (Array.isArray(parentFields[key])) { if (Array.isArray(parentFields[key])) {
parentFields[key] = parentFields[key].filter( parentFields[key] = parentFields[key].filter(
(id: string) => id !== subCredentialId, (id: string) => id !== subCredentialId
); );
} }
} }
+7 -7
View File
@@ -31,7 +31,7 @@ export const generatedQuotes = {
}, },
async fetchByOpportunity( async fetchByOpportunity(
opportunityId: string, opportunityId: string
): Promise<GeneratedQuoteController[]> { ): Promise<GeneratedQuoteController[]> {
const rows = await prisma.generatedQuotes.findMany({ const rows = await prisma.generatedQuotes.findMany({
where: { opportunityId }, where: { opportunityId },
@@ -43,7 +43,7 @@ export const generatedQuotes = {
}, },
async fetchByCreator( async fetchByCreator(
createdById: string, createdById: string
): Promise<GeneratedQuoteController[]> { ): Promise<GeneratedQuoteController[]> {
const rows = await prisma.generatedQuotes.findMany({ const rows = await prisma.generatedQuotes.findMany({
where: { createdById }, where: { createdById },
@@ -55,7 +55,7 @@ export const generatedQuotes = {
}, },
async fetchByHash( async fetchByHash(
quoteRegenHash: string, quoteRegenHash: string
): Promise<GeneratedQuoteController | null> { ): Promise<GeneratedQuoteController | null> {
const quote = await prisma.generatedQuotes.findUnique({ const quote = await prisma.generatedQuotes.findUnique({
where: { quoteRegenHash }, where: { quoteRegenHash },
@@ -76,15 +76,15 @@ export const generatedQuotes = {
createdById: string; createdById: string;
}): Promise<GeneratedQuoteController> { }): Promise<GeneratedQuoteController> {
const opportunity = await prisma.opportunity.findFirst({ const opportunity = await prisma.opportunity.findFirst({
where: { id: data.opportunityId }, where: { uid: data.opportunityId },
select: { id: true }, select: { uid: true },
}); });
if (!opportunity) { if (!opportunity) {
throw new GenericError({ throw new GenericError({
message: "Opportunity not found", message: "Opportunity not found",
name: "OpportunityNotFound", name: "OpportunityNotFound",
cause: `No opportunity exists with ID '${data.opportunityId}'`, cause: `No opportunity exists with uid '${data.opportunityId}'`,
status: 404, status: 404,
}); });
} }
@@ -147,7 +147,7 @@ export const generatedQuotes = {
name?: string | null; name?: string | null;
email: string; email: string;
fetchAction: string; fetchAction: string;
}, }
): Promise<GeneratedQuoteController> { ): Promise<GeneratedQuoteController> {
const existing = await prisma.generatedQuotes.findFirst({ const existing = await prisma.generatedQuotes.findFirst({
where: { id }, where: { id },
File diff suppressed because it is too large Load Diff
+114 -46
View File
@@ -11,10 +11,14 @@ import {
/** /**
* Standard include clause used by catalog item queries. * Standard include clause used by catalog item queries.
* Includes one level of linked items. * Includes one level of linked items plus the manufacturer and
* subcategory (with its parent category) relations so the
* CatalogItemController can populate all fields.
*/ */
const catalogItemInclude = { const catalogItemInclude = {
linkedItems: true, linkedItems: true,
manufacturer: true,
subcategory: { include: { category: true } },
} as const; } as const;
const LABOR_STYLE_CANDIDATES = { const LABOR_STYLE_CANDIDATES = {
@@ -22,8 +26,22 @@ const LABOR_STYLE_CANDIDATES = {
tech: ["LABOR & INSTALLATION - TECH", "LABOR - TECH", "LABOR TECH"], tech: ["LABOR & INSTALLATION - TECH", "LABOR - TECH", "LABOR TECH"],
} as const; } as const;
const DEFAULT_CATALOG_RPP = 30;
function normalizePositiveInt(value: number, fallback: number): number {
const parsed = Number(value);
if (!Number.isFinite(parsed) || parsed < 1) return fallback;
return Math.floor(parsed);
}
function normalizeFiniteNumber(value?: number): number | undefined {
if (value === undefined) return undefined;
const parsed = Number(value);
return Number.isFinite(parsed) ? parsed : undefined;
}
async function findCatalogByExactCandidates( async function findCatalogByExactCandidates(
candidates: readonly string[], candidates: readonly string[]
): Promise<CatalogItemController | null> { ): Promise<CatalogItemController | null> {
for (const candidate of candidates) { for (const candidate of candidates) {
const item = await prisma.catalogItem.findFirst({ const item = await prisma.catalogItem.findFirst({
@@ -44,7 +62,7 @@ async function findCatalogByExactCandidates(
} }
async function findCatalogByLaborStyle( async function findCatalogByLaborStyle(
style: "field" | "tech", style: "field" | "tech"
): Promise<CatalogItemController | null> { ): Promise<CatalogItemController | null> {
const fallback = await prisma.catalogItem.findFirst({ const fallback = await prisma.catalogItem.findFirst({
where: { where: {
@@ -92,6 +110,8 @@ export interface CatalogFilterOpts {
*/ */
function buildFilterWhere(opts: CatalogFilterOpts = {}) { function buildFilterWhere(opts: CatalogFilterOpts = {}) {
const conditions: Record<string, unknown>[] = []; const conditions: Record<string, unknown>[] = [];
const minPrice = normalizeFiniteNumber(opts.minPrice);
const maxPrice = normalizeFiniteNumber(opts.maxPrice);
const parseNumericId = (value?: string): number | null => { const parseNumericId = (value?: string): number | null => {
if (!value) return null; if (!value) return null;
@@ -131,14 +151,14 @@ function buildFilterWhere(opts: CatalogFilterOpts = {}) {
if (opts.category) { if (opts.category) {
if (categoryId) { if (categoryId) {
const categoryOr: Record<string, unknown>[] = [ const categoryOr: Record<string, unknown>[] = [
{ categoryCwId: categoryId }, { subcategory: { is: { category: { is: { id: categoryId } } } } },
]; ];
if (resolvedCategoryName) { if (resolvedCategoryName) {
categoryOr.push({ category: resolvedCategoryName }); categoryOr.push({ subcategory: { is: { category: { is: { name: resolvedCategoryName } } } } });
} }
conditions.push({ OR: categoryOr }); conditions.push({ OR: categoryOr });
} else { } else {
conditions.push({ category: opts.category }); conditions.push({ subcategory: { is: { category: { is: { name: opts.category } } } } });
} }
} }
@@ -146,40 +166,43 @@ function buildFilterWhere(opts: CatalogFilterOpts = {}) {
if (subcategoryId) { if (subcategoryId) {
const resolvedSubcategoryName = resolveSubcategoryNameById(subcategoryId); const resolvedSubcategoryName = resolveSubcategoryNameById(subcategoryId);
const subcategoryOr: Record<string, unknown>[] = [ const subcategoryOr: Record<string, unknown>[] = [
{ subcategoryCwId: subcategoryId }, { subcategory: { is: { id: subcategoryId } } },
]; ];
if (resolvedSubcategoryName) { if (resolvedSubcategoryName) {
subcategoryOr.push({ subcategory: resolvedSubcategoryName }); subcategoryOr.push({ subcategory: { is: { name: resolvedSubcategoryName } } });
} }
conditions.push({ OR: subcategoryOr }); conditions.push({ OR: subcategoryOr });
} else { } else {
conditions.push({ subcategory: opts.subcategory }); conditions.push({ subcategory: { is: { name: opts.subcategory } } });
} }
} }
if (opts.group && opts.category) { if (opts.group && opts.category) {
if (!resolvedCategoryName) { if (!resolvedCategoryName) {
conditions.push({ category: "__unknown_category__" }); conditions.push({ subcategory: { is: { category: { is: { name: "__unknown_category__" } } } } });
} }
if (resolvedCategoryName) { if (resolvedCategoryName) {
const subcats = getSubcategoriesForGroup( const subcats = getSubcategoriesForGroup(
resolvedCategoryName, resolvedCategoryName,
opts.group, opts.group
); );
if (subcats.length > 0) { if (subcats.length > 0) {
conditions.push({ subcategory: { in: subcats } }); conditions.push({ subcategory: { is: { name: { in: subcats } } } });
} }
} }
} else if (opts.group && !opts.category) { } else if (opts.group && !opts.category) {
// Try to find the group in any category // Try to find the group in any category
const {
CATEGORY_TREE,
isCategoryGroup,
} = require("../modules/catalog-categories/catalogCategories");
for (const cat of CATEGORY_TREE) { for (const cat of CATEGORY_TREE) {
const subcats = getSubcategoriesForGroup(cat.name, opts.group); const subcats = getSubcategoriesForGroup(cat.name, opts.group);
if (subcats.length > 0) { if (subcats.length > 0) {
conditions.push({ category: cat.name, subcategory: { in: subcats } }); conditions.push({
subcategory: {
is: {
name: { in: subcats },
category: { is: { name: cat.name } },
},
},
});
break; break;
} }
} }
@@ -187,20 +210,32 @@ function buildFilterWhere(opts: CatalogFilterOpts = {}) {
if (opts.manufacturer) { if (opts.manufacturer) {
conditions.push({ conditions.push({
manufacturer: { contains: opts.manufacturer, mode: "insensitive" }, manufacturer: {
is: {
name: { contains: opts.manufacturer, mode: "insensitive" },
},
},
}); });
} }
if (opts.ecosystem) { if (opts.ecosystem) {
const eco = ECOSYSTEM_TREE.find( const eco = ECOSYSTEM_TREE.find(
(e) => e.name.toLowerCase() === opts.ecosystem!.toLowerCase(), (e) => e.name.toLowerCase() === opts.ecosystem!.toLowerCase()
); );
if (eco && eco.manufacturers.length > 0) { if (eco && eco.manufacturers.length > 0) {
conditions.push({ conditions.push({
OR: eco.manufacturers.map((m) => ({ OR: eco.manufacturers.map((m) => ({
manufacturer: { contains: m.name, mode: "insensitive" as const }, manufacturer: {
subcategory: { startsWith: m.subcategoryPrefix }, is: {
category: m.category, name: { contains: m.name, mode: "insensitive" as const },
},
},
subcategory: {
is: {
name: { startsWith: m.subcategoryPrefix },
category: { is: { name: m.category } },
},
},
})), })),
}); });
} }
@@ -210,12 +245,12 @@ function buildFilterWhere(opts: CatalogFilterOpts = {}) {
conditions.push({ onHand: { gt: 0 } }); conditions.push({ onHand: { gt: 0 } });
} }
if (opts.minPrice !== undefined) { if (minPrice !== undefined) {
conditions.push({ price: { gte: opts.minPrice } }); conditions.push({ price: { gte: minPrice } });
} }
if (opts.maxPrice !== undefined) { if (maxPrice !== undefined) {
conditions.push({ price: { lte: opts.maxPrice } }); conditions.push({ price: { lte: maxPrice } });
} }
return conditions.length > 0 ? { AND: conditions } : undefined; return conditions.length > 0 ? { AND: conditions } : undefined;
@@ -237,10 +272,10 @@ export const procurement = {
const item = await prisma.catalogItem.findFirst({ const item = await prisma.catalogItem.findFirst({
where: isNumeric where: isNumeric
? { cwCatalogId: Number(identifier) } ? { id: Number(identifier) }
: { : {
OR: [ OR: [
{ id: identifier as string }, { uid: identifier as string },
{ identifier: identifier as string }, { identifier: identifier as string },
], ],
}, },
@@ -302,10 +337,12 @@ export const procurement = {
async fetchPages( async fetchPages(
page: number, page: number,
rpp: number, rpp: number,
opts?: CatalogFilterOpts, opts?: CatalogFilterOpts
): Promise<CatalogItemController[]> { ): Promise<CatalogItemController[]> {
const skip = (Math.max(page, 1) - 1) * rpp; const safePage = normalizePositiveInt(page, 1);
const take = rpp; const safeRpp = normalizePositiveInt(rpp, DEFAULT_CATALOG_RPP);
const skip = (safePage - 1) * safeRpp;
const take = safeRpp;
const items = await prisma.catalogItem.findMany({ const items = await prisma.catalogItem.findMany({
where: buildFilterWhere(opts), where: buildFilterWhere(opts),
@@ -334,10 +371,12 @@ export const procurement = {
query: string, query: string,
page: number, page: number,
rpp: number, rpp: number,
opts?: CatalogFilterOpts, opts?: CatalogFilterOpts
): Promise<CatalogItemController[]> { ): Promise<CatalogItemController[]> {
const skip = (Math.max(page, 1) - 1) * rpp; const safePage = normalizePositiveInt(page, 1);
const take = rpp; const safeRpp = normalizePositiveInt(rpp, DEFAULT_CATALOG_RPP);
const skip = (safePage - 1) * safeRpp;
const take = safeRpp;
const filterWhere = buildFilterWhere(opts) ?? {}; const filterWhere = buildFilterWhere(opts) ?? {};
@@ -350,7 +389,11 @@ export const procurement = {
{ description: { contains: query, mode: "insensitive" } }, { description: { contains: query, mode: "insensitive" } },
{ partNumber: { contains: query, mode: "insensitive" } }, { partNumber: { contains: query, mode: "insensitive" } },
{ vendorSku: { contains: query, mode: "insensitive" } }, { vendorSku: { contains: query, mode: "insensitive" } },
{ manufacturer: { contains: query, mode: "insensitive" } }, {
manufacturer: {
is: { name: { contains: query, mode: "insensitive" } },
},
},
], ],
}, },
skip, skip,
@@ -371,7 +414,7 @@ export const procurement = {
* @returns {Promise<number>} - Total count * @returns {Promise<number>} - Total count
*/ */
async count( async count(
opts?: CatalogFilterOpts & { activeOnly?: boolean }, opts?: CatalogFilterOpts & { activeOnly?: boolean }
): Promise<number> { ): Promise<number> {
// Support legacy `activeOnly` flag by mapping it to `includeInactive` // Support legacy `activeOnly` flag by mapping it to `includeInactive`
const filterOpts: CatalogFilterOpts = { const filterOpts: CatalogFilterOpts = {
@@ -407,7 +450,11 @@ export const procurement = {
{ description: { contains: query, mode: "insensitive" } }, { description: { contains: query, mode: "insensitive" } },
{ partNumber: { contains: query, mode: "insensitive" } }, { partNumber: { contains: query, mode: "insensitive" } },
{ vendorSku: { contains: query, mode: "insensitive" } }, { vendorSku: { contains: query, mode: "insensitive" } },
{ manufacturer: { contains: query, mode: "insensitive" } }, {
manufacturer: {
is: { name: { contains: query, mode: "insensitive" } },
},
},
], ],
}, },
}); });
@@ -425,18 +472,39 @@ export const procurement = {
*/ */
async fetchDistinctValues( async fetchDistinctValues(
field: "category" | "subcategory" | "manufacturer", field: "category" | "subcategory" | "manufacturer",
opts?: CatalogFilterOpts, opts?: CatalogFilterOpts
): Promise<string[]> { ): Promise<string[]> {
if (field === "manufacturer") {
const items = await prisma.catalogItem.findMany({ const items = await prisma.catalogItem.findMany({
where: buildFilterWhere(opts), where: buildFilterWhere(opts),
select: { [field]: true }, select: { manufacturer: { select: { name: true } } },
distinct: [field],
orderBy: { [field]: "asc" },
}); });
const names = items
return items .map((item) => item.manufacturer?.name ?? null)
.map((item: Record<string, unknown>) => item[field] as string | null)
.filter((v): v is string => v !== null); .filter((v): v is string => v !== null);
return [...new Set(names)].sort();
}
if (field === "subcategory") {
const items = await prisma.catalogItem.findMany({
where: buildFilterWhere(opts),
select: { subcategory: { select: { name: true } } },
});
const names = items
.map((item) => item.subcategory?.name ?? null)
.filter((v): v is string => v !== null);
return [...new Set(names)].sort();
}
// field === "category"
const items = await prisma.catalogItem.findMany({
where: buildFilterWhere(opts),
select: { subcategory: { select: { category: { select: { name: true } } } } },
});
const names = items
.map((item) => item.subcategory?.category?.name ?? null)
.filter((v): v is string => v !== null);
return [...new Set(names)].sort();
}, },
/** /**
@@ -450,7 +518,7 @@ export const procurement = {
*/ */
async linkItems( async linkItems(
sourceIdentifier: string | number, sourceIdentifier: string | number,
targetIdentifier: string | number, targetIdentifier: string | number
): Promise<CatalogItemController> { ): Promise<CatalogItemController> {
const source = await procurement.fetchItem(sourceIdentifier); const source = await procurement.fetchItem(sourceIdentifier);
const target = await procurement.fetchItem(targetIdentifier); const target = await procurement.fetchItem(targetIdentifier);
@@ -469,7 +537,7 @@ export const procurement = {
*/ */
async unlinkItems( async unlinkItems(
sourceIdentifier: string | number, sourceIdentifier: string | number,
targetIdentifier: string | number, targetIdentifier: string | number
): Promise<CatalogItemController> { ): Promise<CatalogItemController> {
const source = await procurement.fetchItem(sourceIdentifier); const source = await procurement.fetchItem(sourceIdentifier);
const target = await procurement.fetchItem(targetIdentifier); const target = await procurement.fetchItem(targetIdentifier);
+92
View File
@@ -0,0 +1,92 @@
import { prisma } from "../constants";
import { ScheduleController } from "../controllers/ScheduleController";
const scheduleIncludes = {
status: true,
type: true,
scheduleSpan: true,
} as const;
export const schedules = {
async fetch(identifier: string | number): Promise<ScheduleController> {
const isNumeric =
typeof identifier === "number" || /^\d+$/.test(String(identifier));
const schedule = await prisma.schedule.findFirst({
where: isNumeric
? { id: Number(identifier) }
: { uid: String(identifier) },
include: scheduleIncludes,
});
if (!schedule) throw new Error("Unknown schedule.");
return new ScheduleController(schedule);
},
async count() {
return await prisma.schedule.count();
},
async fetchPages(page: number, rpp: number) {
page = page.valueOf();
rpp = rpp.valueOf();
const skip = (page > 1 ? page : 0) * rpp;
const take = rpp ?? 30;
const data = await prisma.schedule.findMany({
skip,
take,
include: scheduleIncludes,
orderBy: { startDate: "desc" },
});
return data.map((s) => new ScheduleController(s));
},
async search(query: string, page: number, rpp: number) {
page = page.valueOf();
rpp = rpp.valueOf();
const skip = (page > 1 ? page : 0) * rpp;
const take = rpp ?? 30;
const numericQuery = parseInt(query, 10);
const data = await prisma.schedule.findMany({
where: {
OR: [
{ name: { contains: query, mode: "insensitive" } },
{ description: { contains: query, mode: "insensitive" } },
{ uid: { contains: query, mode: "insensitive" } },
...(!isNaN(numericQuery) ? [{ id: numericQuery }] : []),
],
},
skip,
take,
include: scheduleIncludes,
orderBy: { startDate: "desc" },
});
return data.map((s) => new ScheduleController(s));
},
async fetchByMemberDateRange(
memberId: string,
startDate: Date,
endDate: Date
) {
const data = await prisma.schedule.findMany({
where: {
memberId,
startDate: { gte: startDate },
endDate: { lte: endDate },
},
include: scheduleIncludes,
orderBy: { startDate: "asc" },
});
return data.map((s) => new ScheduleController(s));
},
};
+39 -12
View File
@@ -1,10 +1,8 @@
import { ms } from "zod/locales";
import { User } from "../../generated/prisma/client"; import { User } from "../../generated/prisma/client";
import { prisma } from "../constants"; import { prisma } from "../constants";
import { SessionTokensObject } from "../controllers/SessionController"; import { SessionTokensObject } from "../controllers/SessionController";
import UserController from "../controllers/UserController"; import UserController from "../controllers/UserController";
import { fetchMicrosoftUser } from "../modules/fetchMicrosoftUser"; import { fetchMicrosoftUser } from "../modules/fetchMicrosoftUser";
import { findCwIdentifierByEmail } from "../modules/cw-utils/members/fetchAllMembers";
import { events } from "../modules/globalEvents"; import { events } from "../modules/globalEvents";
import { sessions } from "./sessions"; import { sessions } from "./sessions";
import * as msal from "@azure/msal-node"; import * as msal from "@azure/msal-node";
@@ -22,7 +20,7 @@ export const users = {
* @param authRequest - The code supplied in the callback url of the Microsoft oAuth transaction * @param authRequest - The code supplied in the callback url of the Microsoft oAuth transaction
*/ */
async authenticate( async authenticate(
authRequest: msal.AuthenticationResult, authRequest: msal.AuthenticationResult
): Promise<SessionTokensObject> { ): Promise<SessionTokensObject> {
let id = authRequest.uniqueId as string; let id = authRequest.uniqueId as string;
@@ -63,7 +61,7 @@ export const users = {
email: string; email: string;
login: string; login: string;
userId: string; userId: string;
}>, }>
) { ) {
if (Object.keys(identifier).length == 0) return null; if (Object.keys(identifier).length == 0) return null;
const userData = await prisma.user.findFirst({ const userData = await prisma.user.findFirst({
@@ -90,18 +88,45 @@ export const users = {
*/ */
async createUser(token: string): Promise<UserController> { async createUser(token: string): Promise<UserController> {
const msData = await fetchMicrosoftUser(token); const msData = await fetchMicrosoftUser(token);
const resolvedEmail = (msData.mail ?? msData.userPrincipalName ?? "")
.trim()
.toLowerCase();
if (!resolvedEmail) {
throw new Error("Microsoft account did not include an email address.");
}
const resolvedLogin = (msData.userPrincipalName ?? resolvedEmail)
.trim()
.toLowerCase();
// Attempt to resolve the user's ConnectWise identifier by email // Attempt to resolve the user's ConnectWise identifier by email
const cwIdentifier = await findCwIdentifierByEmail(msData.mail).catch( const cwIdentifier = await prisma.cwMember
() => null, .findFirst({ where: { officeEmail: resolvedEmail } })
); .then((m) => m?.identifier ?? null)
.catch(() => null);
const newUser = await prisma.user.create({ const existingUser = await prisma.user.findUnique({
data: { where: { email: resolvedEmail },
select: { id: true },
});
const newUser = await prisma.user.upsert({
where: { email: resolvedEmail },
create: {
userId: msData.id, userId: msData.id,
email: msData.mail ?? msData.userPrincipalName, email: resolvedEmail,
name: `${msData.givenName} ${msData.surname}`, firstName: msData.givenName ?? null,
login: msData.userPrincipalName, lastName: msData.surname ?? null,
login: resolvedLogin,
cwIdentifier,
token,
},
update: {
userId: msData.id,
firstName: msData.givenName ?? null,
lastName: msData.surname ?? null,
login: resolvedLogin,
cwIdentifier, cwIdentifier,
token, token,
}, },
@@ -109,7 +134,9 @@ export const users = {
}); });
let controller = new UserController(newUser); let controller = new UserController(newUser);
if (!existingUser) {
events.emit("user:created", controller); events.emit("user:created", controller);
}
return controller; return controller;
}, },
@@ -1,168 +0,0 @@
/**
* @module computeCacheTTL
*
* Adaptive Cache TTL Algorithm
* ============================
*
* Determines how long a cached record should live before it must be
* re-fetched from the upstream source (e.g. ConnectWise API).
*
* The algorithm prioritises freshness for records that are actively
* being worked on, while avoiding unnecessary API calls for stale or
* inactive data.
*
* ## Spec
*
* | # | Condition | TTL (ms) | TTL (human) | Rationale |
* |---|------------------------------------------------------------------|----------|-------------|--------------------------------------------------------------------|
* | 1 | `closedFlag` is `true` | `null` | Do not cache| Closed records are rarely accessed; caching wastes memory. |
* | 2 | `expectedCloseDate` OR `lastUpdated` is within the last **5 days**| 60 000 | 60 seconds | High-activity window data changes frequently and must stay fresh.|
* | 3 | `expectedCloseDate` OR `lastUpdated` is within the last **14 days**| 90 000 | 90 seconds | Moderate activity still relevant, but changes less often. |
* | 4 | Everything else (older than 14 days) | 900 000 | 15 minutes | Low activity safe to serve from cache for longer. |
*
* ## Evaluation order
*
* Rules are evaluated **top-to-bottom**; the first matching rule wins.
* Rule 2 (5-day window) is a subset of Rule 3 (14-day window), so it
* must be checked first.
*
* ## Inputs
*
* | Field | Type | Description |
* |--------------------|------------------|--------------------------------------------------------------------|
* | `closedFlag` | `boolean` | Whether the record is closed / inactive. |
* | `expectedCloseDate`| `Date \| null` | The projected close date (future-looking relevance signal). |
* | `lastUpdated` | `Date \| null` | The last time the upstream record was modified (backward-looking). |
* | `now` | `Date` (optional)| Override for the current timestamp; defaults to `new Date()`. |
*
* ## Output
*
* Returns `number | null`:
* - A positive integer representing the TTL in **milliseconds**, or
* - `null` when the record should **not** be cached at all.
*
* ## Usage
*
* ```ts
* import { computeCacheTTL } from "../modules/algorithms/computeCacheTTL";
*
* const ttl = computeCacheTTL({
* closedFlag: opportunity.closedFlag,
* expectedCloseDate: opportunity.expectedCloseDate,
* lastUpdated: opportunity.cwLastUpdated,
* });
*
* if (ttl !== null) {
* await redis.set(key, serialised, "PX", ttl);
* }
* ```
*/
// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------
/** 60 seconds TTL for high-activity records (within 5 days).
* Must exceed the 30-second background refresh interval so the cache
* stays warm between cycles. */
export const TTL_HIGH_ACTIVITY = 60_000;
/** 90 seconds TTL for moderate-activity records (within 14 days). */
export const TTL_MODERATE_ACTIVITY = 90_000;
/** 15 minutes TTL for low-activity / stale records. */
export const TTL_LOW_ACTIVITY = 900_000;
/** 30 days in milliseconds. */
const THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1000;
/** 5 days in milliseconds. */
const FIVE_DAYS_MS = 5 * 24 * 60 * 60 * 1000;
/** 14 days in milliseconds. */
const FOURTEEN_DAYS_MS = 14 * 24 * 60 * 60 * 1000;
// ---------------------------------------------------------------------------
// Input type
// ---------------------------------------------------------------------------
export interface CacheTTLInput {
/** Whether the record is closed / inactive. */
closedFlag: boolean;
/** When the record was closed — used for recently-closed caching (within 30 days). */
closedDate: Date | null;
/** The projected close date — serves as a forward-looking relevance signal. */
expectedCloseDate: Date | null;
/** The date the upstream record was last modified — backward-looking signal. */
lastUpdated: Date | null;
/**
* Override for the current timestamp.
* Useful for deterministic testing. Defaults to `new Date()`.
*/
now?: Date;
}
// ---------------------------------------------------------------------------
// Algorithm
// ---------------------------------------------------------------------------
/**
* Compute the cache TTL for a record based on its activity signals.
*
* @param input - The record's activity signals. See {@link CacheTTLInput}.
* @returns The TTL in milliseconds, or `null` if the record should not be cached.
*
* @see Module-level JSDoc for the full spec table and evaluation rules.
*/
export function computeCacheTTL(input: CacheTTLInput): number | null {
const {
closedFlag,
closedDate,
expectedCloseDate,
lastUpdated,
now = new Date(),
} = input;
const nowMs = now.getTime();
/**
* Check whether a date falls within a window around `now`.
*
* "Within" means the date is between `now - windowMs` and `now + windowMs`,
* allowing both past updates and future-scheduled dates to qualify.
*/
const isWithinWindow = (date: Date | null, windowMs: number): boolean => {
if (!date) return false;
const diff = Math.abs(nowMs - date.getTime());
return diff <= windowMs;
};
// Rule 1 — Closed records
if (closedFlag) {
// Rule 1b — Recently closed (within 30 days) → cache at low-activity TTL
if (isWithinWindow(closedDate, THIRTY_DAYS_MS)) {
return TTL_LOW_ACTIVITY;
}
// Rule 1a — Closed longer than 30 days → do not cache
return null;
}
// Rule 2 — High activity (5 days)
if (
isWithinWindow(expectedCloseDate, FIVE_DAYS_MS) ||
isWithinWindow(lastUpdated, FIVE_DAYS_MS)
) {
return TTL_HIGH_ACTIVITY;
}
// Rule 3 — Moderate activity (14 days)
if (
isWithinWindow(expectedCloseDate, FOURTEEN_DAYS_MS) ||
isWithinWindow(lastUpdated, FOURTEEN_DAYS_MS)
) {
return TTL_MODERATE_ACTIVITY;
}
// Rule 4 — Low activity / stale
return TTL_LOW_ACTIVITY;
}
@@ -1,116 +0,0 @@
/**
* @module computeProductsCacheTTL
*
* Adaptive Cache TTL for Opportunity Products
* ============================================
*
* Determines how long products (forecast items) should be cached in
* Redis before being re-fetched from ConnectWise.
*
* Products have unique caching rules compared to notes or contacts
* because they are typically finalised before a deal closes and do not
* change once the opportunity reaches a terminal status.
*
* ## Spec
*
* | # | Condition | TTL (ms) | TTL (human) | Rationale |
* |---|------------------------------------------------------------------------------|------------|-------------|---------------------------------------------------------------------------------------|
* | 1 | Status is **Won**, **Lost**, **Pending Won**, or **Pending Lost** | `null` | No cache | Products on terminal / near-terminal opps are static; no need to keep them warm. |
* | 2 | Opportunity is **not cacheable** (main cache TTL is `null`) | `null` | No cache | If the opp itself is evicted, sub-resources follow suit. |
* | 3 | `lastUpdated` is within the last **3 days** | 15 000 | 15 seconds | Actively-worked deals products are being edited and need near-real-time freshness. |
* | 4 | Everything else | 1 200 000 | 20 minutes | Lazy on-demand cache: fetched when requested, expires after 20 min without refresh. |
*
* ## Evaluation order
*
* Rules are evaluated top-to-bottom; the first matching rule wins.
*
* ## Inputs
*
* Extends {@link CacheTTLInput} from `computeCacheTTL` with an
* additional `statusCwId` field used to identify terminal statuses.
*
* ## Output
*
* Returns `number | null`:
* - Positive integer = TTL in **milliseconds**.
* - `null` = do **not** cache.
*/
import type { CacheTTLInput } from "./computeCacheTTL";
import { computeCacheTTL } from "./computeCacheTTL";
import { QUOTE_STATUSES } from "../../types/QuoteStatuses";
// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------
/** 45 seconds TTL for hot products (opportunity updated within 3 days).
* Must exceed the 30-second background refresh interval so the cache
* stays warm between cycles. */
export const PRODUCTS_TTL_HOT = 45_000;
/** 20 minutes — TTL for on-demand product cache (lazy fallback). */
export const PRODUCTS_TTL_LAZY = 1_200_000;
/** 3 days in milliseconds. */
const THREE_DAYS_MS = 3 * 24 * 60 * 60 * 1000;
/**
* Set of all CW status IDs that map to a Won or Lost canonical status.
*
* Built at module load from {@link QUOTE_STATUSES} so it stays in sync
* with any future status additions.
*/
export const WON_LOST_STATUS_IDS: ReadonlySet<number> = new Set(
QUOTE_STATUSES.filter((s) => s.wonFlag || s.lostFlag).flatMap((s) => [
s.id,
...s.optimaEquivalency,
]),
);
// ---------------------------------------------------------------------------
// Input type
// ---------------------------------------------------------------------------
export interface ProductsCacheTTLInput extends CacheTTLInput {
/** The CW status ID of the opportunity. */
statusCwId: number | null;
}
// ---------------------------------------------------------------------------
// Algorithm
// ---------------------------------------------------------------------------
/**
* Compute the cache TTL for an opportunity's products.
*
* @param input - The opportunity's activity signals plus status ID.
* @returns TTL in milliseconds, or `null` if products should not be cached.
*/
export function computeProductsCacheTTL(
input: ProductsCacheTTLInput,
): number | null {
const { statusCwId, lastUpdated, now = new Date() } = input;
// Rule 1 — Terminal statuses: Won / Lost / Pending Won / Pending Lost
if (statusCwId !== null && WON_LOST_STATUS_IDS.has(statusCwId)) {
return null;
}
// Rule 2 — If the opportunity itself is not cacheable, skip products too
const mainTTL = computeCacheTTL(input);
if (mainTTL === null) {
return null;
}
// Rule 3 — Hot: updated within the last 3 days
if (lastUpdated) {
const diff = Math.abs(now.getTime() - lastUpdated.getTime());
if (diff <= THREE_DAYS_MS) {
return PRODUCTS_TTL_HOT;
}
}
// Rule 4 — Lazy fallback
return PRODUCTS_TTL_LAZY;
}
@@ -1,118 +0,0 @@
/**
* @module computeSubResourceCacheTTL
*
* Adaptive Cache TTL for Opportunity Sub-Resources
* =================================================
*
* Determines how long cached sub-resource data (notes, contacts) should
* live before being re-fetched from ConnectWise.
*
* Sub-resources change less frequently than the opportunity record itself
* or its activity feed, so TTLs are longer than the primary cache. The
* same activity-signal heuristics are used (expected close date, last
* updated, closed status) but with relaxed durations.
*
* ## Spec
*
* | # | Condition | TTL (ms) | TTL (human) | Rationale |
* |---|-------------------------------------------------------------------|----------|-------------|--------------------------------------------------------------------|
* | 1 | `closedFlag` is `true` AND closed > 30 days ago | `null` | Do not cache| Old closed records are rarely accessed. |
* | 1b| `closedFlag` is `true` AND closed within 30 days | 300 000 | 5 minutes | Recently-closed records may still be viewed occasionally. |
* | 2 | `expectedCloseDate` OR `lastUpdated` within **5 days** | 60 000 | 60 seconds | Active deals contacts/notes may still change. |
* | 3 | `expectedCloseDate` OR `lastUpdated` within **14 days** | 120 000 | 2 minutes | Moderate activity less likely to change. |
* | 4 | Everything else (older than 14 days) | 300 000 | 5 minutes | Low activity safe to cache longer. |
*
* ## Evaluation order
*
* Rules are evaluated top-to-bottom; the first matching rule wins.
*
* ## Inputs
*
* Uses the same {@link CacheTTLInput} interface as `computeCacheTTL`.
*
* ## Output
*
* Returns `number | null`:
* - Positive integer = TTL in **milliseconds**.
* - `null` = do **not** cache.
*/
import type { CacheTTLInput } from "./computeCacheTTL";
// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------
/** 60 seconds — TTL for high-activity sub-resources (within 5 days). */
export const SUB_TTL_HIGH_ACTIVITY = 60_000;
/** 2 minutes — TTL for moderate-activity sub-resources (within 14 days). */
export const SUB_TTL_MODERATE_ACTIVITY = 120_000;
/** 5 minutes — TTL for low-activity / stale sub-resources. */
export const SUB_TTL_LOW_ACTIVITY = 300_000;
/** 30 days in milliseconds. */
const THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1000;
/** 5 days in milliseconds. */
const FIVE_DAYS_MS = 5 * 24 * 60 * 60 * 1000;
/** 14 days in milliseconds. */
const FOURTEEN_DAYS_MS = 14 * 24 * 60 * 60 * 1000;
// ---------------------------------------------------------------------------
// Algorithm
// ---------------------------------------------------------------------------
/**
* Compute the cache TTL for an opportunity sub-resource (notes, contacts).
*
* @param input - The opportunity's activity signals. See {@link CacheTTLInput}.
* @returns The TTL in milliseconds, or `null` if the data should not be cached.
*/
export function computeSubResourceCacheTTL(
input: CacheTTLInput,
): number | null {
const {
closedFlag,
closedDate,
expectedCloseDate,
lastUpdated,
now = new Date(),
} = input;
const nowMs = now.getTime();
const isWithinWindow = (date: Date | null, windowMs: number): boolean => {
if (!date) return false;
return Math.abs(nowMs - date.getTime()) <= windowMs;
};
// Rule 1 — Closed records
if (closedFlag) {
if (isWithinWindow(closedDate, THIRTY_DAYS_MS)) {
return SUB_TTL_LOW_ACTIVITY;
}
return null;
}
// Rule 2 — High activity (5 days)
if (
isWithinWindow(expectedCloseDate, FIVE_DAYS_MS) ||
isWithinWindow(lastUpdated, FIVE_DAYS_MS)
) {
return SUB_TTL_HIGH_ACTIVITY;
}
// Rule 3 — Moderate activity (14 days)
if (
isWithinWindow(expectedCloseDate, FOURTEEN_DAYS_MS) ||
isWithinWindow(lastUpdated, FOURTEEN_DAYS_MS)
) {
return SUB_TTL_MODERATE_ACTIVITY;
}
// Rule 4 — Low activity / stale
return SUB_TTL_LOW_ACTIVITY;
}
-659
View File
@@ -1,659 +0,0 @@
/**
* @module opportunityCache
*
* Redis-backed cache for expensive ConnectWise API data associated
* with opportunities.
*
* ## What is cached
*
* Each non-closed opportunity may have cached payloads keyed by its `cwOpportunityId`:
*
* - **Activities** (`opp:activities:{cwOpportunityId}`) the raw `CWActivity[]` array
* - **Company CW data** (`opp:company-cw:{cw_CompanyId}`) hydrated company / contacts blob
* - **Notes** (`opp:notes:{cwOpportunityId}`) raw CW notes array
* - **Contacts** (`opp:contacts:{cwOpportunityId}`) raw CW contacts array
* - **Products** (`opp:products:{cwOpportunityId}`) raw CW forecast + procurement products blob
*
* TTLs are computed dynamically via {@link computeCacheTTL}.
*
* ## Background refresh (Worker-based)
*
* ** This module is now READ-ONLY.** Cache refresh logic has been moved to workers:
*
* - {@link refreshActiveOpportunitiesWorker} Scheduled to run every 20 minutes
* to run a unified cache pass across all opportunities. Active/recent records
* use adaptive TTLs and archived records use {@link TTL_ARCHIVED_MS}.
*
* See `src/modules/workers/cache/` for worker implementations.
*
* ## This module now provides
*
* - `getCached*()` functions for reading cached data
* - `fetchAndCache*()` functions used internally by workers
* - `invalidate*()` functions for cache invalidation after mutations
* - Cache key helpers for Redis operations
*/
import { prisma, redis } from "../../constants";
import { activityCw } from "../cw-utils/activities/activities";
import { computeCacheTTL } from "../algorithms/computeCacheTTL";
import { computeSubResourceCacheTTL } from "../algorithms/computeSubResourceCacheTTL";
import {
computeProductsCacheTTL,
PRODUCTS_TTL_HOT,
} from "../algorithms/computeProductsCacheTTL";
import { connectWiseApi } from "../../constants";
import { fetchCwCompanyById } from "../cw-utils/fetchCompany";
import { fetchCompanySite } from "../cw-utils/sites/companySites";
import { opportunityCw } from "../cw-utils/opportunities/opportunities";
import { withCwRetry } from "../cw-utils/withCwRetry";
import { events } from "../globalEvents";
// ---------------------------------------------------------------------------
// Key helpers
// ---------------------------------------------------------------------------
const ACTIVITY_PREFIX = "opp:activities:";
const COMPANY_CW_PREFIX = "opp:company-cw:";
const NOTES_PREFIX = "opp:notes:";
const CONTACTS_PREFIX = "opp:contacts:";
const PRODUCTS_PREFIX = "opp:products:";
const SITE_PREFIX = "opp:site:";
const OPP_CW_PREFIX = "opp:cw-data:";
/** Redis key for cached activities by CW opportunity ID. */
export const activityCacheKey = (cwOppId: number) =>
`${ACTIVITY_PREFIX}${cwOppId}`;
/** Redis key for cached company CW hydration data by CW company ID. */
export const companyCwCacheKey = (cwCompanyId: number) =>
`${COMPANY_CW_PREFIX}${cwCompanyId}`;
/** Redis key for cached opportunity notes by CW opportunity ID. */
export const notesCacheKey = (cwOppId: number) => `${NOTES_PREFIX}${cwOppId}`;
/** Redis key for cached opportunity contacts by CW opportunity ID. */
export const contactsCacheKey = (cwOppId: number) =>
`${CONTACTS_PREFIX}${cwOppId}`;
/** Redis key for cached opportunity products by CW opportunity ID. */
export const productsCacheKey = (cwOppId: number) =>
`${PRODUCTS_PREFIX}${cwOppId}`;
/** Redis key for cached company site by CW company ID + site ID. */
export const siteCacheKey = (cwCompanyId: number, cwSiteId: number) =>
`${SITE_PREFIX}${cwCompanyId}:${cwSiteId}`;
/** Redis key for cached CW opportunity response by CW opportunity ID. */
export const oppCwDataCacheKey = (cwOppId: number) =>
`${OPP_CW_PREFIX}${cwOppId}`;
// ---------------------------------------------------------------------------
// Read helpers
// ---------------------------------------------------------------------------
/**
* Retrieve cached CW activities for an opportunity.
*
* @returns The parsed `CWActivity[]` or `null` on cache miss.
*/
export async function getCachedActivities(
cwOpportunityId: number,
): Promise<any[] | null> {
const raw = await redis.get(activityCacheKey(cwOpportunityId));
if (!raw) return null;
try {
return JSON.parse(raw);
} catch {
return null;
}
}
/**
* Retrieve cached company CW hydration data.
*
* @returns `{ company, defaultContact, allContacts }` or `null` on cache miss.
*/
export async function getCachedCompanyCwData(
cwCompanyId: number,
): Promise<{ company: any; defaultContact: any; allContacts: any[] } | null> {
const raw = await redis.get(companyCwCacheKey(cwCompanyId));
if (!raw) return null;
try {
return JSON.parse(raw);
} catch {
return null;
}
}
/**
* Retrieve cached opportunity notes (raw CW data).
*
* @returns The parsed raw CW notes array or `null` on cache miss.
*/
export async function getCachedNotes(
cwOpportunityId: number,
): Promise<any[] | null> {
const raw = await redis.get(notesCacheKey(cwOpportunityId));
if (!raw) return null;
try {
return JSON.parse(raw);
} catch {
return null;
}
}
/**
* Retrieve cached opportunity contacts (raw CW data).
*
* @returns The parsed raw CW contacts array or `null` on cache miss.
*/
export async function getCachedContacts(
cwOpportunityId: number,
): Promise<any[] | null> {
const raw = await redis.get(contactsCacheKey(cwOpportunityId));
if (!raw) return null;
try {
return JSON.parse(raw);
} catch {
return null;
}
}
/**
* Retrieve cached opportunity products (raw CW forecast + procurement blob).
*
* @returns `{ forecast, procProducts }` or `null` on cache miss.
*/
export async function getCachedProducts(
cwOpportunityId: number,
): Promise<{ forecast: any; procProducts: any[] } | null> {
const raw = await redis.get(productsCacheKey(cwOpportunityId));
if (!raw) return null;
try {
return JSON.parse(raw);
} catch {
return null;
}
}
/**
* Retrieve cached CW site data for a company/site pair.
*
* @returns Parsed site data or `null` on cache miss.
*/
export async function getCachedSite(
cwCompanyId: number,
cwSiteId: number,
): Promise<any | null> {
const raw = await redis.get(siteCacheKey(cwCompanyId, cwSiteId));
if (!raw) return null;
try {
return JSON.parse(raw);
} catch {
return null;
}
}
/**
* Retrieve cached CW opportunity response data.
*
* @returns Parsed CW opportunity object or `null` on cache miss.
*/
export async function getCachedOppCwData(
cwOpportunityId: number,
): Promise<any | null> {
const raw = await redis.get(oppCwDataCacheKey(cwOpportunityId));
if (!raw) return null;
try {
return JSON.parse(raw);
} catch {
return null;
}
}
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
/** Check whether an error is an Axios 404 (resource not found in CW). */
function isNotFoundError(err: unknown): boolean {
if (typeof err !== "object" || err === null) return false;
const e = err as Record<string, any>;
return e.isAxiosError === true && e.response?.status === 404;
}
/**
* Check whether an error is a transient network / timeout error.
*
* These are safe to swallow in background refresh tasks CW will be
* retried on the next refresh cycle. Logs a concise one-line warning
* instead of dumping the full Axios error object.
*/
function isTransientError(err: unknown): boolean {
if (typeof err !== "object" || err === null) return false;
const e = err as Record<string, any>;
if (!e.isAxiosError) return false;
const code = e.code as string | undefined;
return (
code === "ECONNABORTED" ||
code === "ECONNREFUSED" ||
code === "ECONNRESET" ||
code === "ETIMEDOUT" ||
code === "ERR_NETWORK" ||
code === "ENETUNREACH" ||
code === "ERR_BAD_RESPONSE"
);
}
/** Build a concise error description for logging (avoids dumping entire Axios objects). */
function describeError(err: unknown): string {
if (typeof err !== "object" || err === null) return String(err);
const e = err as Record<string, any>;
if (e.isAxiosError) {
const method = (e.config?.method ?? "?").toUpperCase();
const url = e.config?.url ?? "unknown";
const code = e.code ?? "";
const status = e.response?.status ?? "";
return `${method} ${url}${code || `HTTP ${status}`} (${e.message})`;
}
return e.message ?? String(err);
}
/**
* When true, transient-error warnings inside fetchAndCache* are suppressed.
* Used during background refresh to avoid flooding the terminal the
* refresh function prints a single summary line instead.
*/
let _suppressTransientWarnings = false;
// ---------------------------------------------------------------------------
// Write helpers
// ---------------------------------------------------------------------------
/**
* Fetch activities from CW and cache them with the appropriate TTL.
*
* Returns an empty array if CW responds with 404 (opportunity doesn't
* exist or was deleted upstream).
*
* @returns The raw `CWActivity[]` collection (as plain array).
*/
export async function fetchAndCacheActivities(
cwOpportunityId: number,
ttlMs: number,
): Promise<any[]> {
try {
// Use the direct (single-call) variant to avoid the extra count request
const arr = await activityCw.fetchByOpportunityDirect(cwOpportunityId);
await redis.set(
activityCacheKey(cwOpportunityId),
JSON.stringify(arr),
"PX",
ttlMs,
);
return arr;
} catch (err) {
if (isNotFoundError(err)) return [];
if (isTransientError(err)) {
console.warn(
`[cache] activities opp#${cwOpportunityId}: ${describeError(err)}`,
);
return [];
}
throw err;
}
}
/**
* Fetch company CW data (company, contacts) and cache with the given TTL.
*
* @returns The hydration blob or `null` if the company doesn't exist in CW.
*/
export async function fetchAndCacheCompanyCwData(
cwCompanyId: number,
ttlMs: number,
): Promise<{ company: any; defaultContact: any; allContacts: any[] } | null> {
try {
// Fetch company and all-contacts in parallel — the allContacts URL
// can be constructed directly without the company response.
const [cwCompany, allContactsData] = await Promise.all([
fetchCwCompanyById(cwCompanyId),
withCwRetry(
() =>
connectWiseApi.get(
`/company/companies/${cwCompanyId}/contacts?pageSize=1000`,
),
{ label: `company#${cwCompanyId}/allContacts` },
),
]);
if (!cwCompany) return null;
// Default contact: derive from allContacts instead of making an
// extra serial CW call. The company object carries the default
// contact's ID, so we can pull it from the list we already fetched.
const defaultContactId = cwCompany.defaultContact?.id;
const defaultContactData = defaultContactId
? ((allContactsData.data as any[]).find(
(c: any) => c.id === defaultContactId,
) ?? null)
: null;
const blob = {
company: cwCompany,
defaultContact: defaultContactData,
allContacts: allContactsData.data,
};
await redis.set(
companyCwCacheKey(cwCompanyId),
JSON.stringify(blob),
"PX",
ttlMs,
);
return blob;
} catch (err) {
if (isNotFoundError(err)) return null;
if (isTransientError(err)) {
console.warn(`[cache] company#${cwCompanyId}: ${describeError(err)}`);
return null;
}
throw err;
}
}
/**
* Fetch opportunity notes from CW and cache the raw response.
*
* Returns an empty array if CW responds with 404.
*
* @returns The raw CW notes array.
*/
export async function fetchAndCacheNotes(
cwOpportunityId: number,
ttlMs: number,
): Promise<any[]> {
try {
const notes = await opportunityCw.fetchNotes(cwOpportunityId);
await redis.set(
notesCacheKey(cwOpportunityId),
JSON.stringify(notes),
"PX",
ttlMs,
);
return notes;
} catch (err) {
if (isNotFoundError(err)) return [];
if (isTransientError(err)) {
console.warn(
`[cache] notes opp#${cwOpportunityId}: ${describeError(err)}`,
);
return [];
}
throw err;
}
}
/**
* Fetch opportunity contacts from CW and cache the raw response.
*
* Returns an empty array if CW responds with 404.
*
* @returns The raw CW contacts array.
*/
export async function fetchAndCacheContacts(
cwOpportunityId: number,
ttlMs: number,
): Promise<any[]> {
try {
const contacts = await opportunityCw.fetchContacts(cwOpportunityId);
await redis.set(
contactsCacheKey(cwOpportunityId),
JSON.stringify(contacts),
"PX",
ttlMs,
);
return contacts;
} catch (err) {
if (isNotFoundError(err)) return [];
if (isTransientError(err)) {
console.warn(
`[cache] contacts opp#${cwOpportunityId}: ${describeError(err)}`,
);
return [];
}
throw err;
}
}
/**
* Invalidate cached notes for an opportunity.
*
* Call this after any note mutation (create, update, delete) so the
* next read refreshes from ConnectWise.
*/
export async function invalidateNotesCache(
cwOpportunityId: number,
): Promise<void> {
await redis.del(notesCacheKey(cwOpportunityId));
}
/**
* Invalidate cached contacts for an opportunity.
*
* Call this after any contact mutation so the next read refreshes
* from ConnectWise.
*/
export async function invalidateContactsCache(
cwOpportunityId: number,
): Promise<void> {
await redis.del(contactsCacheKey(cwOpportunityId));
}
/**
* Fetch opportunity products (forecast + procurement) from CW and cache.
*
* Stores both the forecast response and procurement products together
* so that `fetchProducts()` can reconstruct ForecastProductControllers
* from a single cache hit.
*
* @returns `{ forecast, procProducts }` blob.
*/
export async function fetchAndCacheProducts(
cwOpportunityId: number,
ttlMs: number,
): Promise<{ forecast: any; procProducts: any[] }> {
try {
const [forecast, procProducts] = await Promise.all([
opportunityCw.fetchProducts(cwOpportunityId),
opportunityCw.fetchProcurementProducts(cwOpportunityId),
]);
const blob = { forecast, procProducts };
await redis.set(
productsCacheKey(cwOpportunityId),
JSON.stringify(blob),
"PX",
ttlMs,
);
return blob;
} catch (err) {
if (isNotFoundError(err))
return { forecast: { forecastItems: [] }, procProducts: [] };
if (isTransientError(err)) {
console.warn(
`[cache] products opp#${cwOpportunityId}: ${describeError(err)}`,
);
return { forecast: { forecastItems: [] }, procProducts: [] };
}
throw err;
}
}
/**
* Invalidate cached products for an opportunity.
*
* Call this after any product mutation (add, update, resequence) so the
* next read refreshes from ConnectWise.
*/
export async function invalidateProductsCache(
cwOpportunityId: number,
): Promise<void> {
await redis.del(productsCacheKey(cwOpportunityId));
}
/**
* Invalidate all cached data for an opportunity.
*
* Removes activities, notes, contacts, products, and CW data cache keys.
* Call this when an opportunity is deleted.
*/
export async function invalidateAllOpportunityCaches(
cwOpportunityId: number,
): Promise<void> {
await redis.del(
activityCacheKey(cwOpportunityId),
notesCacheKey(cwOpportunityId),
contactsCacheKey(cwOpportunityId),
productsCacheKey(cwOpportunityId),
oppCwDataCacheKey(cwOpportunityId),
);
}
/**
* Site TTL 20 minutes. Site/address data rarely changes so we cache
* aggressively. The background refresh does NOT proactively warm site keys;
* they are populated lazily on the first detail-view request.
*/
const SITE_TTL_MS = 1_200_000;
/**
* Fetch a CW company site from ConnectWise and cache the result.
*
* @returns The raw CW site object.
*/
export async function fetchAndCacheSite(
cwCompanyId: number,
cwSiteId: number,
): Promise<any> {
try {
const site = await fetchCompanySite(cwCompanyId, cwSiteId);
await redis.set(
siteCacheKey(cwCompanyId, cwSiteId),
JSON.stringify(site),
"PX",
SITE_TTL_MS,
);
return site;
} catch (err) {
if (isNotFoundError(err)) return null;
if (isTransientError(err)) {
console.warn(
`[cache] site company#${cwCompanyId}/site#${cwSiteId}: ${describeError(err)}`,
);
return null;
}
throw err;
}
}
/**
* Fetch the raw CW opportunity response from ConnectWise and cache it.
*
* Used by `fetchItem()` in the manager to avoid a CW roundtrip when
* the detail view is reloaded within the cache TTL window.
*
* @param cwOpportunityId - The CW opportunity ID
* @param ttlMs - Cache TTL in milliseconds
* @returns The raw CW opportunity response object.
*/
export async function fetchAndCacheOppCwData(
cwOpportunityId: number,
ttlMs: number,
): Promise<any> {
try {
const cwData = await opportunityCw.fetch(cwOpportunityId);
await redis.set(
oppCwDataCacheKey(cwOpportunityId),
JSON.stringify(cwData),
"PX",
ttlMs,
);
return cwData;
} catch (err) {
if (isNotFoundError(err)) return null;
if (isTransientError(err)) {
console.warn(`[cache] opp#${cwOpportunityId}: ${describeError(err)}`);
return null;
}
throw err;
}
}
// ---------------------------------------------------------------------------
// Background refresh
// ---------------------------------------------------------------------------
/**
* Fixed 24-hour TTL used for archived (closed > 30 days) opportunity cache entries.
* These opportunities are outside the adaptive-TTL window and are rebuilt once per
* day at midnight via {@link refreshArchivedOpportunityCache}.
*/
export const TTL_ARCHIVED_MS = 24 * 60 * 60 * 1000; // 24 hours
/**
* Cache opportunities that fall outside the adaptive-TTL window i.e. those
* closed **more than 30 days ago** with a fixed 24-hour TTL.
*
* These opportunities are excluded by {@link computeCacheTTL} (returns `null`)
* and are therefore never warmed by {@link refreshOpportunityCache}. This
* function fills that gap so archived deals are still served from cache on
* the rare occasion they are accessed.
*
* ## Scheduling
*
* Designed to be triggered once per day at midnight from `src/index.ts`. At
* midnight `force` is `true` so every key is unconditionally overwritten,
* ensuring data is no more than 24 hours stale.
*
* On startup `force` defaults to `false` so only truly missing keys are
* populated; this avoids a large CW burst on every process restart.
*
* @param force - When `true`, overwrite every cache key without checking
* whether it already exists. Defaults to `false`.
*/
/**
* TODO: This function has been moved to a worker at
* `src/modules/workers/cache/refreshArchivedOpportunities.ts`
*
* Wire up the worker to run daily at midnight with force=true to ensure
* archived opportunities (closed > 30 days) have fresh cache entries.
*
* @deprecated - Use refreshArchivedOpportunitiesWorker from the worker module
*/
export async function refreshArchivedOpportunityCache(
force = false,
): Promise<void> {
throw new Error(
"refreshArchivedOpportunityCache has been moved to a worker. " +
"Use refreshArchivedOpportunitiesWorker from src/modules/workers/cache/refreshArchivedOpportunities.ts",
);
}
/**
* TODO: This function has been moved to a worker at
* `src/modules/workers/cache/refreshActiveOpportunities.ts`
*
* Wire up the worker to run every 30 seconds to refresh cache for active
* and recently-closed (within 30 days) opportunities.
*
* @deprecated - Use refreshActiveOpportunitiesWorker from the worker module
*/
export async function refreshOpportunityCache(): Promise<void> {
throw new Error(
"refreshOpportunityCache has been moved to a worker. " +
"Use refreshActiveOpportunitiesWorker from src/modules/workers/cache/refreshActiveOpportunities.ts",
);
}
+36 -138
View File
@@ -1,6 +1,4 @@
import { prisma, redis } from "../../constants"; import { prisma, redis } from "../../constants";
import { getCachedOppCwData, getCachedProducts } from "./opportunityCache";
import { OpportunityStatus } from "../../workflows/wf.opportunity";
import { events } from "../globalEvents"; import { events } from "../globalEvents";
import { opportunities } from "../../managers/opportunities"; import { opportunities } from "../../managers/opportunities";
import { normalizeProbabilityRatio } from "../sales-utils/normalizeProbability"; import { normalizeProbabilityRatio } from "../sales-utils/normalizeProbability";
@@ -101,13 +99,16 @@ interface CachedOpportunityRevenue {
} }
interface OpportunityRow { interface OpportunityRow {
id: string; id: number;
cwOpportunityId: number; uid: string;
name: string; name: string;
primarySalesRepIdentifier: string | null; primarySalesRepId: string | null;
secondarySalesRepIdentifier: string | null; secondarySalesRepId: string | null;
statusCwId: number | null; status: {
statusName: string | null; wonFlag: boolean;
lostFlag: boolean;
closeFlag: boolean;
} | null;
closedFlag: boolean; closedFlag: boolean;
dateBecameLead: Date | null; dateBecameLead: Date | null;
closedDate: Date | null; closedDate: Date | null;
@@ -137,107 +138,23 @@ const toFinite = (value: unknown): number => {
return n; return n;
}; };
const isWon = (opp: { const isWon = (opp: { status: { wonFlag: boolean } | null }) =>
statusCwId: number | null; Boolean(opp.status?.wonFlag);
statusName: string | null;
closedFlag: boolean;
}) => {
if (opp.statusCwId === OpportunityStatus.Won) return true;
if (opp.statusName?.toLowerCase().includes("won")) return true;
if (opp.closedFlag && opp.statusName?.toLowerCase().includes("won"))
return true;
return false;
};
const isLost = (opp: { const isLost = (opp: { status: { lostFlag: boolean } | null }) =>
statusCwId: number | null; Boolean(opp.status?.lostFlag);
statusName: string | null;
closedFlag: boolean;
}) => {
if (opp.statusCwId === OpportunityStatus.Lost) return true;
if (opp.statusName?.toLowerCase().includes("lost")) return true;
if (opp.closedFlag && opp.statusName?.toLowerCase().includes("lost"))
return true;
return false;
};
const isClosedOpportunity = (opp: { const isClosedOpportunity = (opp: {
statusCwId: number | null; status: { wonFlag: boolean; lostFlag: boolean; closeFlag: boolean } | null;
statusName: string | null;
closedFlag: boolean; closedFlag: boolean;
}) => { }) => {
if (opp.closedFlag) return true; if (opp.closedFlag) return true;
if (opp.status?.closeFlag) return true;
if (isWon(opp)) return true; if (isWon(opp)) return true;
if (isLost(opp)) return true; if (isLost(opp)) return true;
return false; return false;
}; };
const buildCancellationMap = (procProducts: any[]) => {
const map = new Map<number, any>();
for (const pp of procProducts) {
const rawForecastDetailId = pp?.forecastDetailId;
const forecastDetailId =
typeof rawForecastDetailId === "number"
? rawForecastDetailId
: Number(rawForecastDetailId);
if (Number.isFinite(forecastDetailId) && forecastDetailId > 0) {
map.set(forecastDetailId, pp);
}
}
return map;
};
const computeRevenueFromProductsBlob = (
blob: any,
): Omit<OpportunityRevenue, "cacheHit"> => {
const forecastItems = Array.isArray(blob?.forecast?.forecastItems)
? blob.forecast.forecastItems
: [];
const procProducts = Array.isArray(blob?.procProducts)
? blob.procProducts
: [];
const cancellationMap = buildCancellationMap(procProducts);
let totalRevenue = 0;
let taxableRevenue = 0;
for (const item of forecastItems) {
if (!cancellationMap.has(item?.id)) continue;
if (!item?.includeFlag) continue;
const quantity = Math.max(0, toFinite(item?.quantity));
const revenue = toFinite(item?.revenue);
const cancellation = cancellationMap.get(item.id);
const cancelledFlag = Boolean(cancellation?.cancelledFlag);
const quantityCancelled = Math.max(
0,
toFinite(cancellation?.quantityCancelled),
);
if (cancelledFlag && quantity > 0 && quantityCancelled >= quantity)
continue;
const ratio =
quantity > 0 ? Math.max(0, (quantity - quantityCancelled) / quantity) : 1;
const effectiveRevenue = revenue * ratio;
totalRevenue += effectiveRevenue;
if (item?.taxableFlag) taxableRevenue += effectiveRevenue;
}
const nonTaxableRevenue = totalRevenue - taxableRevenue;
return {
totalRevenue: roundCurrency(totalRevenue),
taxableRevenue: roundCurrency(taxableRevenue),
nonTaxableRevenue: roundCurrency(nonTaxableRevenue),
};
};
const computeRevenueFromControllers = ( const computeRevenueFromControllers = (
products: Array<{ products: Array<{
@@ -298,20 +215,8 @@ const writeCachedOpportunityRevenue = async (
); );
}; };
const resolveProbabilityRatio = async (opp: { const resolveProbabilityRatio = (opp: { probability: number }): number =>
cwOpportunityId: number; normalizeProbabilityRatio(opp.probability);
probability: number;
}): Promise<number> => {
const fromDb = normalizeProbabilityRatio(opp.probability);
if (fromDb > 0) return fromDb;
const cachedCwOpp = await getCachedOppCwData(opp.cwOpportunityId);
if (!cachedCwOpp) return 0;
const rawProbability =
cachedCwOpp?.probability?.name ?? cachedCwOpp?.probability ?? 0;
return normalizeProbabilityRatio(rawProbability);
};
const getOpportunityRevenueCacheFirst = async ( const getOpportunityRevenueCacheFirst = async (
cwOpportunityId: number, cwOpportunityId: number,
@@ -327,18 +232,6 @@ const getOpportunityRevenueCacheFirst = async (
} }
} }
if (!opts?.forceColdLoad) {
const cachedProducts = await getCachedProducts(cwOpportunityId);
if (cachedProducts) {
const computed = computeRevenueFromProductsBlob(cachedProducts);
await writeCachedOpportunityRevenue(cwOpportunityId, computed);
return {
...computed,
cacheHit: true,
};
}
}
try { try {
const opportunity = await opportunities.fetchRecord(cwOpportunityId); const opportunity = await opportunities.fetchRecord(cwOpportunityId);
const products = await opportunity.fetchProducts({ const products = await opportunity.fetchProducts({
@@ -489,8 +382,8 @@ export async function refreshSalesOpportunityMetricsCache(
AND: [ AND: [
{ {
OR: [ OR: [
{ primarySalesRepIdentifier: { in: memberIdentifiers } }, { primarySalesRepId: { in: memberIdentifiers } },
{ secondarySalesRepIdentifier: { in: memberIdentifiers } }, { secondarySalesRepId: { in: memberIdentifiers } },
], ],
}, },
{ dateBecameLead: { gte: yearStart } }, { dateBecameLead: { gte: yearStart } },
@@ -501,12 +394,17 @@ export async function refreshSalesOpportunityMetricsCache(
}, },
select: { select: {
id: true, id: true,
cwOpportunityId: true, uid: true,
name: true, name: true,
primarySalesRepIdentifier: true, primarySalesRepId: true,
secondarySalesRepIdentifier: true, secondarySalesRepId: true,
statusCwId: true, status: {
statusName: true, select: {
wonFlag: true,
lostFlag: true,
closeFlag: true,
},
},
closedFlag: true, closedFlag: true,
dateBecameLead: true, dateBecameLead: true,
closedDate: true, closedDate: true,
@@ -565,7 +463,7 @@ export async function refreshSalesOpportunityMetricsCache(
async (opp) => { async (opp) => {
const [revenue, probabilityRatio] = await Promise.all([ const [revenue, probabilityRatio] = await Promise.all([
withTimeout( withTimeout(
getOpportunityRevenueCacheFirst(opp.cwOpportunityId, { getOpportunityRevenueCacheFirst(opp.id, {
forceColdLoad, forceColdLoad,
}), }),
PRODUCT_LOOKUP_TIMEOUT_MS, PRODUCT_LOOKUP_TIMEOUT_MS,
@@ -619,10 +517,10 @@ export async function refreshSalesOpportunityMetricsCache(
for (const opp of opportunityRows) { for (const opp of opportunityRows) {
const assigned = new Set<string>(); const assigned = new Set<string>();
if (opp.primarySalesRepIdentifier) if (opp.primarySalesRepId)
assigned.add(opp.primarySalesRepIdentifier); assigned.add(opp.primarySalesRepId);
if (opp.secondarySalesRepIdentifier) if (opp.secondarySalesRepId)
assigned.add(opp.secondarySalesRepIdentifier); assigned.add(opp.secondarySalesRepId);
for (const identifier of assigned) { for (const identifier of assigned) {
const bucket = opportunitiesByMember.get(identifier); const bucket = opportunitiesByMember.get(identifier);
@@ -665,8 +563,8 @@ export async function refreshSalesOpportunityMetricsCache(
); );
const breakdownEntry: OpportunityBreakdownEntry = { const breakdownEntry: OpportunityBreakdownEntry = {
id: opp.id, id: opp.uid,
cwId: opp.cwOpportunityId, cwId: opp.id,
name: opp.name, name: opp.name,
revenue: revenue.totalRevenue, revenue: revenue.totalRevenue,
taxableRevenue: revenue.taxableRevenue, taxableRevenue: revenue.taxableRevenue,
@@ -1,103 +0,0 @@
import { collectorSocket } from "../../constants";
export type CollectorQueryOptions = {
select?: string[];
include?: string[];
[key: string]: unknown;
};
type CollectorSuccessResponse<T> = {
success: true;
data: T;
};
type CollectorErrorResponse = {
success: false;
error: string;
};
type CollectorResponse<T> =
| CollectorSuccessResponse<T>
| CollectorErrorResponse;
const DEFAULT_ACK_TIMEOUT_MS = Number(
Bun.env.COLLECTOR_ACK_TIMEOUT_MS ?? "15000",
);
const DEFAULT_CONNECT_TIMEOUT_MS = Number(
Bun.env.COLLECTOR_CONNECT_TIMEOUT_MS ?? "5000",
);
const ensureCollectorConnected = async (
timeoutMs = DEFAULT_CONNECT_TIMEOUT_MS,
): Promise<void> => {
if (collectorSocket.connected) {
return;
}
collectorSocket.connect();
await new Promise<void>((resolve, reject) => {
const timeout = setTimeout(() => {
cleanup();
reject(new Error("Collector socket connection timeout"));
}, timeoutMs);
const onConnect = () => {
cleanup();
resolve();
};
const onConnectError = (err: Error) => {
cleanup();
reject(err);
};
const cleanup = () => {
clearTimeout(timeout);
collectorSocket.off("connect", onConnect);
collectorSocket.off("connect_error", onConnectError);
};
collectorSocket.on("connect", onConnect);
collectorSocket.on("connect_error", onConnectError);
});
};
export const runCollector = async <T = unknown>(
collector: string,
opts?: CollectorQueryOptions,
): Promise<T> => {
await ensureCollectorConnected();
const response = await new Promise<CollectorResponse<T>>(
(resolve, reject) => {
collectorSocket
.timeout(DEFAULT_ACK_TIMEOUT_MS)
.emit(
collector,
opts,
(err: Error | null, payload?: CollectorResponse<T>) => {
if (err) {
reject(err);
return;
}
if (!payload) {
reject(
new Error(`Collector '${collector}' returned an empty payload`),
);
return;
}
resolve(payload);
},
);
},
);
if (!response.success) {
throw new Error(`Collector '${collector}' failed: ${response.error}`);
}
return response.data;
};
@@ -1,27 +0,0 @@
import GenericError from "../../../Errors/GenericError";
import { activityCw } from "./activities";
import { CWActivity } from "./activity.types";
/**
* Fetch a single activity by its ConnectWise ID.
*
* @param cwActivityId - The ConnectWise activity ID
* @returns The full CW activity object
* @throws GenericError if the fetch fails
*/
export const fetchActivity = async (
cwActivityId: number,
): Promise<CWActivity> => {
try {
return await activityCw.fetch(cwActivityId);
} catch (error) {
const errBody = (error as any).response?.data || error;
console.error(`Error fetching activity with ID ${cwActivityId}:`, errBody);
throw new GenericError({
name: "FetchActivityError",
message: `Failed to fetch activity ${cwActivityId}`,
cause: typeof errBody === "string" ? errBody : JSON.stringify(errBody),
status: 502,
});
}
};
@@ -1,28 +0,0 @@
import { Collection } from "@discordjs/collection";
import GenericError from "../../../Errors/GenericError";
import { activityCw } from "./activities";
import { CWActivity } from "./activity.types";
/**
* Fetch all activities from ConnectWise with optional conditions.
*
* @param conditions - Optional CW conditions string for filtering
* @returns A Collection of CW activities keyed by their ID
* @throws GenericError if the fetch fails
*/
export const fetchAllActivities = async (
conditions?: string,
): Promise<Collection<number, CWActivity>> => {
try {
return await activityCw.fetchAll(conditions);
} catch (error) {
const errBody = (error as any).response?.data || error;
console.error("Error fetching all activities:", errBody);
throw new GenericError({
name: "FetchAllActivitiesError",
message: "Failed to fetch activities from ConnectWise",
cause: typeof errBody === "string" ? errBody : JSON.stringify(errBody),
status: 502,
});
}
};
+2 -37
View File
@@ -1,41 +1,10 @@
import { Company } from "../../types/ConnectWiseTypes"; import { Company } from "../../types/ConnectWiseTypes";
import { import {
CollectorCompanyRecord,
CompanySourceRecord, CompanySourceRecord,
NormalizedCompanyRecord, NormalizedCompanyRecord,
} from "../../types/CompanySourceTypes"; } from "../../types/CompanySourceTypes";
export const isCollectorCompanyRecord = ( const normalizeCompany = (
value: unknown,
): value is CollectorCompanyRecord => {
if (!value || typeof value !== "object") {
return false;
}
const candidate = value as Partial<CollectorCompanyRecord>;
return (
typeof candidate.companyRecId === "number" &&
"companyId" in candidate &&
"companyName" in candidate
);
};
const normalizeFromCollector = (
company: CollectorCompanyRecord,
): NormalizedCompanyRecord | null => {
if (!company.companyId || !company.companyName) {
return null;
}
return {
id: company.companyRecId,
identifier: company.companyId,
name: company.companyName,
};
};
const normalizeFromCwApi = (
company: Company, company: Company,
): NormalizedCompanyRecord | null => { ): NormalizedCompanyRecord | null => {
if (!company.identifier || !company.name) { if (!company.identifier || !company.name) {
@@ -52,11 +21,7 @@ const normalizeFromCwApi = (
export const normalizeCompanyRecord = ( export const normalizeCompanyRecord = (
source: CompanySourceRecord, source: CompanySourceRecord,
): NormalizedCompanyRecord | null => { ): NormalizedCompanyRecord | null => {
if (isCollectorCompanyRecord(source)) { return normalizeCompany(source);
return normalizeFromCollector(source);
}
return normalizeFromCwApi(source);
}; };
export const normalizeCompanyRecords = ( export const normalizeCompanyRecords = (

Some files were not shown because too many files have changed in this diff Show More