all the haul
This commit is contained in:
@@ -26,7 +26,7 @@ services:
|
||||
image: adminer
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- 8080:8080
|
||||
- 8081:8080
|
||||
depends_on:
|
||||
- pgsql
|
||||
redisinsight:
|
||||
@@ -1,2 +0,0 @@
|
||||
node_modules
|
||||
daemon
|
||||
+13
-5
@@ -1,4 +1,4 @@
|
||||
name: Build and Publish
|
||||
name: API - Build and Publish
|
||||
|
||||
on:
|
||||
release:
|
||||
@@ -8,6 +8,9 @@ jobs:
|
||||
test:
|
||||
name: Test
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: api
|
||||
steps:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v4
|
||||
@@ -47,6 +50,7 @@ jobs:
|
||||
- name: Build and push the Docker image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: ./api
|
||||
push: true
|
||||
target: runtime
|
||||
tags: |
|
||||
@@ -56,6 +60,7 @@ jobs:
|
||||
- name: Build and push the migration image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: ./api
|
||||
push: true
|
||||
target: migration
|
||||
tags: |
|
||||
@@ -66,6 +71,9 @@ jobs:
|
||||
name: Run Migrations
|
||||
needs: [build]
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: api
|
||||
steps:
|
||||
- name: Set the Kubernetes context
|
||||
uses: azure/k8s-set-context@v2
|
||||
@@ -108,8 +116,8 @@ jobs:
|
||||
with:
|
||||
lintType: dryrun
|
||||
manifests: |
|
||||
kubernetes/deployment.yaml
|
||||
kubernetes/ingress.yaml
|
||||
api/kubernetes/deployment.yaml
|
||||
api/kubernetes/ingress.yaml
|
||||
namespace: optima
|
||||
|
||||
- name: Deploy to the Kubernetes cluster
|
||||
@@ -119,7 +127,7 @@ jobs:
|
||||
force: true
|
||||
skip-tls-verify: true
|
||||
manifests: |
|
||||
kubernetes/deployment.yaml
|
||||
kubernetes/ingress.yaml
|
||||
api/kubernetes/deployment.yaml
|
||||
api/kubernetes/ingress.yaml
|
||||
images: |
|
||||
ghcr.io/project-optima/ttscm-api:${{ github.event.release.tag_name }}
|
||||
@@ -1,4 +1,4 @@
|
||||
name: Tests
|
||||
name: API - Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -8,6 +8,9 @@ jobs:
|
||||
test:
|
||||
name: Test
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: api
|
||||
steps:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v4
|
||||
+11
-42
@@ -1,4 +1,4 @@
|
||||
name: Build and Publish
|
||||
name: UI - Build and Publish
|
||||
|
||||
on:
|
||||
release:
|
||||
@@ -28,7 +28,7 @@ jobs:
|
||||
- name: Build and push the Docker image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
context: ./ui
|
||||
push: true
|
||||
build-args: |
|
||||
PUBLIC_API_URL=https://opt-api.osdci.net
|
||||
@@ -41,6 +41,9 @@ jobs:
|
||||
runs-on: macos-latest
|
||||
permissions:
|
||||
contents: write
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ui
|
||||
steps:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v4
|
||||
@@ -68,14 +71,17 @@ jobs:
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: |
|
||||
out/make/**/*.dmg
|
||||
out/make/**/*.zip
|
||||
ui/out/make/**/*.dmg
|
||||
ui/out/make/**/*.zip
|
||||
|
||||
build-desktop-windows:
|
||||
name: Build Desktop (Windows)
|
||||
runs-on: windows-latest
|
||||
permissions:
|
||||
contents: write
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ui
|
||||
steps:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v4
|
||||
@@ -97,41 +103,4 @@ jobs:
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: |
|
||||
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 }}
|
||||
ui/out/make/**/*.exe
|
||||
+53
-123
@@ -1,139 +1,69 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
# macOS metadata
|
||||
__MACOSX/
|
||||
.DS_Store
|
||||
.AppleDouble
|
||||
.LSOverride
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
# Windows
|
||||
Thumbs.db
|
||||
ehthumbs.db
|
||||
Desktop.ini
|
||||
|
||||
# Runtime data
|
||||
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
|
||||
# Dependencies
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# Snowpack dependency directory (https://snowpack.dev/)
|
||||
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
|
||||
# Environment variables
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
!.env.test
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
.parcel-cache
|
||||
# Logs
|
||||
*.log
|
||||
*.jsonl
|
||||
logs/
|
||||
|
||||
# Next.js build output
|
||||
.next
|
||||
out
|
||||
# TypeScript
|
||||
*.tsbuildinfo
|
||||
|
||||
# Nuxt.js build / generate output
|
||||
.nuxt
|
||||
dist
|
||||
# Bun
|
||||
.bun/
|
||||
|
||||
# Gatsby files
|
||||
.cache/
|
||||
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||
# public
|
||||
# Build outputs
|
||||
dist/
|
||||
build/
|
||||
out/
|
||||
.output/
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# vuepress v2.x temp and cache directory
|
||||
.temp
|
||||
.cache
|
||||
|
||||
# Sveltekit cache directory
|
||||
# SvelteKit
|
||||
.svelte-kit/
|
||||
|
||||
# vitepress build output
|
||||
**/.vitepress/dist
|
||||
|
||||
# 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
|
||||
.vite/
|
||||
vite.config.js.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 +0,0 @@
|
||||
_
|
||||
@@ -1,2 +0,0 @@
|
||||
node_modules/
|
||||
daemon
|
||||
@@ -1,8 +0,0 @@
|
||||
module.exports = {
|
||||
trailingComma: "es5",
|
||||
tabWidth: 2,
|
||||
semi: true,
|
||||
singleQuote: false,
|
||||
arrowParens: "always",
|
||||
useTabs: false,
|
||||
};
|
||||
@@ -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
|
||||
@@ -17,6 +17,16 @@ import * as Prisma from './internal/prismaNamespaceBrowser.ts'
|
||||
export { Prisma }
|
||||
export * as $Enums from './enums.ts'
|
||||
export * from './enums.ts';
|
||||
/**
|
||||
* Model SyncJobRun
|
||||
*
|
||||
*/
|
||||
export type SyncJobRun = Prisma.SyncJobRunModel
|
||||
/**
|
||||
* Model SyncStepLog
|
||||
*
|
||||
*/
|
||||
export type SyncStepLog = Prisma.SyncStepLogModel
|
||||
/**
|
||||
* Model Session
|
||||
*
|
||||
@@ -32,6 +42,16 @@ export type User = Prisma.UserModel
|
||||
*
|
||||
*/
|
||||
export type Role = Prisma.RoleModel
|
||||
/**
|
||||
* Model CorporateLocation
|
||||
*
|
||||
*/
|
||||
export type CorporateLocation = Prisma.CorporateLocationModel
|
||||
/**
|
||||
* Model InternalDepartment
|
||||
*
|
||||
*/
|
||||
export type InternalDepartment = Prisma.InternalDepartmentModel
|
||||
/**
|
||||
* Model UnifiSite
|
||||
*
|
||||
@@ -42,16 +62,156 @@ export type UnifiSite = Prisma.UnifiSiteModel
|
||||
*
|
||||
*/
|
||||
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
|
||||
*
|
||||
*/
|
||||
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
|
||||
*
|
||||
*/
|
||||
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
|
||||
*
|
||||
@@ -72,6 +232,11 @@ export type Credential = Prisma.CredentialModel
|
||||
*
|
||||
*/
|
||||
export type GeneratedQuotes = Prisma.GeneratedQuotesModel
|
||||
/**
|
||||
* Model TaxCode
|
||||
*
|
||||
*/
|
||||
export type TaxCode = Prisma.TaxCodeModel
|
||||
/**
|
||||
* Model CwMember
|
||||
*
|
||||
|
||||
@@ -28,9 +28,11 @@ export * from "./enums.ts"
|
||||
* Type-safe database client for TypeScript
|
||||
* @example
|
||||
* ```
|
||||
* const prisma = new PrismaClient()
|
||||
* // Fetch zero or more Sessions
|
||||
* const sessions = await prisma.session.findMany()
|
||||
* const prisma = new PrismaClient({
|
||||
* adapter: new PrismaPg({ connectionString: process.env.DATABASE_URL })
|
||||
* })
|
||||
* // Fetch zero or more SyncJobRuns
|
||||
* const syncJobRuns = await prisma.syncJobRun.findMany()
|
||||
* ```
|
||||
*
|
||||
* 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 { Prisma }
|
||||
|
||||
/**
|
||||
* Model SyncJobRun
|
||||
*
|
||||
*/
|
||||
export type SyncJobRun = Prisma.SyncJobRunModel
|
||||
/**
|
||||
* Model SyncStepLog
|
||||
*
|
||||
*/
|
||||
export type SyncStepLog = Prisma.SyncStepLogModel
|
||||
/**
|
||||
* Model Session
|
||||
*
|
||||
@@ -54,6 +66,16 @@ export type User = Prisma.UserModel
|
||||
*
|
||||
*/
|
||||
export type Role = Prisma.RoleModel
|
||||
/**
|
||||
* Model CorporateLocation
|
||||
*
|
||||
*/
|
||||
export type CorporateLocation = Prisma.CorporateLocationModel
|
||||
/**
|
||||
* Model InternalDepartment
|
||||
*
|
||||
*/
|
||||
export type InternalDepartment = Prisma.InternalDepartmentModel
|
||||
/**
|
||||
* Model UnifiSite
|
||||
*
|
||||
@@ -64,16 +86,156 @@ export type UnifiSite = Prisma.UnifiSiteModel
|
||||
*
|
||||
*/
|
||||
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
|
||||
*
|
||||
*/
|
||||
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
|
||||
*
|
||||
*/
|
||||
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
|
||||
*
|
||||
@@ -94,6 +256,11 @@ export type Credential = Prisma.CredentialModel
|
||||
*
|
||||
*/
|
||||
export type GeneratedQuotes = Prisma.GeneratedQuotesModel
|
||||
/**
|
||||
* Model TaxCode
|
||||
*
|
||||
*/
|
||||
export type TaxCode = Prisma.TaxCodeModel
|
||||
/**
|
||||
* Model CwMember
|
||||
*
|
||||
|
||||
@@ -29,20 +29,18 @@ export type StringFilter<$PrismaModel = never> = {
|
||||
not?: Prisma.NestedStringFilter<$PrismaModel> | string
|
||||
}
|
||||
|
||||
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 EnumSyncJobTypeFilter<$PrismaModel = never> = {
|
||||
equals?: $Enums.SyncJobType | Prisma.EnumSyncJobTypeFieldRefInput<$PrismaModel>
|
||||
in?: $Enums.SyncJobType[] | Prisma.ListEnumSyncJobTypeFieldRefInput<$PrismaModel>
|
||||
notIn?: $Enums.SyncJobType[] | Prisma.ListEnumSyncJobTypeFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedEnumSyncJobTypeFilter<$PrismaModel> | $Enums.SyncJobType
|
||||
}
|
||||
|
||||
export type BoolFilter<$PrismaModel = never> = {
|
||||
equals?: boolean | Prisma.BooleanFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedBoolFilter<$PrismaModel> | boolean
|
||||
export type EnumSyncJobStatusFilter<$PrismaModel = never> = {
|
||||
equals?: $Enums.SyncJobStatus | Prisma.EnumSyncJobStatusFieldRefInput<$PrismaModel>
|
||||
in?: $Enums.SyncJobStatus[] | Prisma.ListEnumSyncJobStatusFieldRefInput<$PrismaModel>
|
||||
notIn?: $Enums.SyncJobStatus[] | Prisma.ListEnumSyncJobStatusFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedEnumSyncJobStatusFilter<$PrismaModel> | $Enums.SyncJobStatus
|
||||
}
|
||||
|
||||
export type DateTimeNullableFilter<$PrismaModel = never> = {
|
||||
@@ -56,6 +54,32 @@ export type DateTimeNullableFilter<$PrismaModel = never> = {
|
||||
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 = {
|
||||
sort: Prisma.SortOrder
|
||||
nulls?: Prisma.NullsOrder
|
||||
@@ -79,26 +103,24 @@ export type StringWithAggregatesFilter<$PrismaModel = never> = {
|
||||
_max?: Prisma.NestedStringFilter<$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
|
||||
export type EnumSyncJobTypeWithAggregatesFilter<$PrismaModel = never> = {
|
||||
equals?: $Enums.SyncJobType | Prisma.EnumSyncJobTypeFieldRefInput<$PrismaModel>
|
||||
in?: $Enums.SyncJobType[] | Prisma.ListEnumSyncJobTypeFieldRefInput<$PrismaModel>
|
||||
notIn?: $Enums.SyncJobType[] | Prisma.ListEnumSyncJobTypeFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedEnumSyncJobTypeWithAggregatesFilter<$PrismaModel> | $Enums.SyncJobType
|
||||
_count?: Prisma.NestedIntFilter<$PrismaModel>
|
||||
_min?: Prisma.NestedDateTimeFilter<$PrismaModel>
|
||||
_max?: Prisma.NestedDateTimeFilter<$PrismaModel>
|
||||
_min?: Prisma.NestedEnumSyncJobTypeFilter<$PrismaModel>
|
||||
_max?: Prisma.NestedEnumSyncJobTypeFilter<$PrismaModel>
|
||||
}
|
||||
|
||||
export type BoolWithAggregatesFilter<$PrismaModel = never> = {
|
||||
equals?: boolean | Prisma.BooleanFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedBoolWithAggregatesFilter<$PrismaModel> | boolean
|
||||
export type EnumSyncJobStatusWithAggregatesFilter<$PrismaModel = never> = {
|
||||
equals?: $Enums.SyncJobStatus | Prisma.EnumSyncJobStatusFieldRefInput<$PrismaModel>
|
||||
in?: $Enums.SyncJobStatus[] | Prisma.ListEnumSyncJobStatusFieldRefInput<$PrismaModel>
|
||||
notIn?: $Enums.SyncJobStatus[] | Prisma.ListEnumSyncJobStatusFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedEnumSyncJobStatusWithAggregatesFilter<$PrismaModel> | $Enums.SyncJobStatus
|
||||
_count?: Prisma.NestedIntFilter<$PrismaModel>
|
||||
_min?: Prisma.NestedBoolFilter<$PrismaModel>
|
||||
_max?: Prisma.NestedBoolFilter<$PrismaModel>
|
||||
_min?: Prisma.NestedEnumSyncJobStatusFilter<$PrismaModel>
|
||||
_max?: Prisma.NestedEnumSyncJobStatusFilter<$PrismaModel>
|
||||
}
|
||||
|
||||
export type DateTimeNullableWithAggregatesFilter<$PrismaModel = never> = {
|
||||
@@ -115,21 +137,6 @@ export type DateTimeNullableWithAggregatesFilter<$PrismaModel = never> = {
|
||||
_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> = {
|
||||
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
|
||||
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
|
||||
@@ -148,6 +155,20 @@ export type StringNullableWithAggregatesFilter<$PrismaModel = never> = {
|
||||
_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> = {
|
||||
equals?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel>
|
||||
@@ -159,76 +180,6 @@ export type IntFilter<$PrismaModel = never> = {
|
||||
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> =
|
||||
| Prisma.PatchUndefined<
|
||||
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
|
||||
}
|
||||
|
||||
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> =
|
||||
| Prisma.PatchUndefined<
|
||||
Prisma.Either<Required<JsonWithAggregatesFilterBase<$PrismaModel>>, Exclude<keyof Required<JsonWithAggregatesFilterBase<$PrismaModel>>, 'path'>>,
|
||||
@@ -280,6 +247,219 @@ export type JsonWithAggregatesFilterBase<$PrismaModel = never> = {
|
||||
_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> = {
|
||||
equals?: runtime.Bytes | Prisma.BytesFieldRefInput<$PrismaModel>
|
||||
in?: runtime.Bytes[] | Prisma.ListBytesFieldRefInput<$PrismaModel>
|
||||
@@ -311,20 +491,18 @@ export type NestedStringFilter<$PrismaModel = never> = {
|
||||
not?: Prisma.NestedStringFilter<$PrismaModel> | string
|
||||
}
|
||||
|
||||
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 NestedEnumSyncJobTypeFilter<$PrismaModel = never> = {
|
||||
equals?: $Enums.SyncJobType | Prisma.EnumSyncJobTypeFieldRefInput<$PrismaModel>
|
||||
in?: $Enums.SyncJobType[] | Prisma.ListEnumSyncJobTypeFieldRefInput<$PrismaModel>
|
||||
notIn?: $Enums.SyncJobType[] | Prisma.ListEnumSyncJobTypeFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedEnumSyncJobTypeFilter<$PrismaModel> | $Enums.SyncJobType
|
||||
}
|
||||
|
||||
export type NestedBoolFilter<$PrismaModel = never> = {
|
||||
equals?: boolean | Prisma.BooleanFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedBoolFilter<$PrismaModel> | boolean
|
||||
export type NestedEnumSyncJobStatusFilter<$PrismaModel = never> = {
|
||||
equals?: $Enums.SyncJobStatus | Prisma.EnumSyncJobStatusFieldRefInput<$PrismaModel>
|
||||
in?: $Enums.SyncJobStatus[] | Prisma.ListEnumSyncJobStatusFieldRefInput<$PrismaModel>
|
||||
notIn?: $Enums.SyncJobStatus[] | Prisma.ListEnumSyncJobStatusFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedEnumSyncJobStatusFilter<$PrismaModel> | $Enums.SyncJobStatus
|
||||
}
|
||||
|
||||
export type NestedDateTimeNullableFilter<$PrismaModel = never> = {
|
||||
@@ -338,6 +516,31 @@ export type NestedDateTimeNullableFilter<$PrismaModel = never> = {
|
||||
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> = {
|
||||
equals?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel>
|
||||
@@ -366,26 +569,24 @@ export type NestedIntFilter<$PrismaModel = never> = {
|
||||
not?: Prisma.NestedIntFilter<$PrismaModel> | number
|
||||
}
|
||||
|
||||
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
|
||||
export type NestedEnumSyncJobTypeWithAggregatesFilter<$PrismaModel = never> = {
|
||||
equals?: $Enums.SyncJobType | Prisma.EnumSyncJobTypeFieldRefInput<$PrismaModel>
|
||||
in?: $Enums.SyncJobType[] | Prisma.ListEnumSyncJobTypeFieldRefInput<$PrismaModel>
|
||||
notIn?: $Enums.SyncJobType[] | Prisma.ListEnumSyncJobTypeFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedEnumSyncJobTypeWithAggregatesFilter<$PrismaModel> | $Enums.SyncJobType
|
||||
_count?: Prisma.NestedIntFilter<$PrismaModel>
|
||||
_min?: Prisma.NestedDateTimeFilter<$PrismaModel>
|
||||
_max?: Prisma.NestedDateTimeFilter<$PrismaModel>
|
||||
_min?: Prisma.NestedEnumSyncJobTypeFilter<$PrismaModel>
|
||||
_max?: Prisma.NestedEnumSyncJobTypeFilter<$PrismaModel>
|
||||
}
|
||||
|
||||
export type NestedBoolWithAggregatesFilter<$PrismaModel = never> = {
|
||||
equals?: boolean | Prisma.BooleanFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedBoolWithAggregatesFilter<$PrismaModel> | boolean
|
||||
export type NestedEnumSyncJobStatusWithAggregatesFilter<$PrismaModel = never> = {
|
||||
equals?: $Enums.SyncJobStatus | Prisma.EnumSyncJobStatusFieldRefInput<$PrismaModel>
|
||||
in?: $Enums.SyncJobStatus[] | Prisma.ListEnumSyncJobStatusFieldRefInput<$PrismaModel>
|
||||
notIn?: $Enums.SyncJobStatus[] | Prisma.ListEnumSyncJobStatusFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedEnumSyncJobStatusWithAggregatesFilter<$PrismaModel> | $Enums.SyncJobStatus
|
||||
_count?: Prisma.NestedIntFilter<$PrismaModel>
|
||||
_min?: Prisma.NestedBoolFilter<$PrismaModel>
|
||||
_max?: Prisma.NestedBoolFilter<$PrismaModel>
|
||||
_min?: Prisma.NestedEnumSyncJobStatusFilter<$PrismaModel>
|
||||
_max?: Prisma.NestedEnumSyncJobStatusFilter<$PrismaModel>
|
||||
}
|
||||
|
||||
export type NestedDateTimeNullableWithAggregatesFilter<$PrismaModel = never> = {
|
||||
@@ -413,20 +614,6 @@ export type NestedIntNullableFilter<$PrismaModel = never> = {
|
||||
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> = {
|
||||
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
|
||||
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
|
||||
@@ -444,6 +631,20 @@ export type NestedStringNullableWithAggregatesFilter<$PrismaModel = never> = {
|
||||
_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> = {
|
||||
equals?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel>
|
||||
@@ -471,6 +672,43 @@ export type NestedFloatFilter<$PrismaModel = never> = {
|
||||
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> = {
|
||||
equals?: number | Prisma.IntFieldRefInput<$PrismaModel> | null
|
||||
in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> | null
|
||||
@@ -498,6 +736,74 @@ export type NestedFloatNullableFilter<$PrismaModel = never> = {
|
||||
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> = {
|
||||
equals?: number | Prisma.FloatFieldRefInput<$PrismaModel>
|
||||
in?: number[] | Prisma.ListFloatFieldRefInput<$PrismaModel>
|
||||
@@ -514,28 +820,71 @@ export type NestedFloatWithAggregatesFilter<$PrismaModel = never> = {
|
||||
_max?: Prisma.NestedFloatFilter<$PrismaModel>
|
||||
}
|
||||
|
||||
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 NestedFloatNullableWithAggregatesFilter<$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 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 NestedEnumBillingMethodFilter<$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 NestedEnumBillingTypeFilter<$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 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> = {
|
||||
|
||||
@@ -9,7 +9,138 @@
|
||||
* 🟢 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 {}
|
||||
export const FaxType = {
|
||||
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 = {
|
||||
SyncJobRun: 'SyncJobRun',
|
||||
SyncStepLog: 'SyncStepLog',
|
||||
Session: 'Session',
|
||||
User: 'User',
|
||||
Role: 'Role',
|
||||
CorporateLocation: 'CorporateLocation',
|
||||
InternalDepartment: 'InternalDepartment',
|
||||
UnifiSite: 'UnifiSite',
|
||||
Company: 'Company',
|
||||
CompanyAddress: 'CompanyAddress',
|
||||
Contact: 'Contact',
|
||||
CatalogItemType: 'CatalogItemType',
|
||||
CatalogCategory: 'CatalogCategory',
|
||||
CatalogSubcategory: 'CatalogSubcategory',
|
||||
CatalogManufacturer: 'CatalogManufacturer',
|
||||
WarehouseBin: 'WarehouseBin',
|
||||
ProductInventory: 'ProductInventory',
|
||||
Warehouse: 'Warehouse',
|
||||
MinimumStockByWarehouse: 'MinimumStockByWarehouse',
|
||||
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',
|
||||
ScheduleStatus: 'ScheduleStatus',
|
||||
ScheduleType: 'ScheduleType',
|
||||
ScheduleSpan: 'ScheduleSpan',
|
||||
Schedule: 'Schedule',
|
||||
CredentialType: 'CredentialType',
|
||||
SecureValue: 'SecureValue',
|
||||
Credential: 'Credential',
|
||||
GeneratedQuotes: 'GeneratedQuotes',
|
||||
TaxCode: 'TaxCode',
|
||||
CwMember: 'CwMember'
|
||||
} as const
|
||||
|
||||
@@ -81,6 +114,39 @@ export const TransactionIsolationLevel = runtime.makeStrictEnum({
|
||||
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 = {
|
||||
id: 'id',
|
||||
sessionKey: 'sessionKey',
|
||||
@@ -98,13 +164,18 @@ export const UserScalarFieldEnum = {
|
||||
id: 'id',
|
||||
permissions: 'permissions',
|
||||
login: 'login',
|
||||
name: 'name',
|
||||
firstName: 'firstName',
|
||||
lastName: 'lastName',
|
||||
email: 'email',
|
||||
emailVerified: 'emailVerified',
|
||||
image: 'image',
|
||||
title: 'title',
|
||||
active: 'active',
|
||||
hidden: 'hidden',
|
||||
cwIdentifier: 'cwIdentifier',
|
||||
cwMemberId: 'cwMemberId',
|
||||
userId: 'userId',
|
||||
token: 'token',
|
||||
updatedBy: 'updatedBy',
|
||||
createdAt: 'createdAt',
|
||||
updatedAt: 'updatedAt'
|
||||
} as const
|
||||
@@ -124,6 +195,40 @@ export const 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 = {
|
||||
id: 'id',
|
||||
name: 'name',
|
||||
@@ -138,9 +243,17 @@ export type UnifiSiteScalarFieldEnum = (typeof UnifiSiteScalarFieldEnum)[keyof t
|
||||
|
||||
export const CompanyScalarFieldEnum = {
|
||||
id: 'id',
|
||||
uid: 'uid',
|
||||
name: 'name',
|
||||
cw_CompanyId: 'cw_CompanyId',
|
||||
cw_Identifier: 'cw_Identifier',
|
||||
phone: 'phone',
|
||||
website: 'website',
|
||||
deleteFlag: 'deleteFlag',
|
||||
dateDeleted: 'dateDeleted',
|
||||
taxId: 'taxId',
|
||||
taxExempt: 'taxExempt',
|
||||
enteredById: 'enteredById',
|
||||
deletedById: 'deletedById',
|
||||
deletedAt: 'deletedAt',
|
||||
createdAt: 'createdAt',
|
||||
updatedAt: 'updatedAt'
|
||||
} as const
|
||||
@@ -148,20 +261,195 @@ export const 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 = {
|
||||
id: 'id',
|
||||
cwCatalogId: 'cwCatalogId',
|
||||
uid: 'uid',
|
||||
identifier: 'identifier',
|
||||
name: 'name',
|
||||
description: 'description',
|
||||
customerDescription: 'customerDescription',
|
||||
internalNotes: 'internalNotes',
|
||||
category: 'category',
|
||||
categoryCwId: 'categoryCwId',
|
||||
subcategory: 'subcategory',
|
||||
subcategoryCwId: 'subcategoryCwId',
|
||||
manufacturer: 'manufacturer',
|
||||
manufactureCwId: 'manufactureCwId',
|
||||
subcategoryId: 'subcategoryId',
|
||||
manufacturerId: 'manufacturerId',
|
||||
partNumber: 'partNumber',
|
||||
vendorName: 'vendorName',
|
||||
vendorSku: 'vendorSku',
|
||||
@@ -172,6 +460,7 @@ export const CatalogItemScalarFieldEnum = {
|
||||
salesTaxable: 'salesTaxable',
|
||||
onHand: 'onHand',
|
||||
cwLastUpdated: 'cwLastUpdated',
|
||||
classId: 'classId',
|
||||
createdAt: 'createdAt',
|
||||
updatedAt: 'updatedAt'
|
||||
} as const
|
||||
@@ -179,54 +468,338 @@ export const 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 = {
|
||||
id: 'id',
|
||||
cwOpportunityId: 'cwOpportunityId',
|
||||
uid: 'uid',
|
||||
name: 'name',
|
||||
notes: 'notes',
|
||||
typeName: 'typeName',
|
||||
typeCwId: 'typeCwId',
|
||||
stageName: 'stageName',
|
||||
stageCwId: 'stageCwId',
|
||||
statusName: 'statusName',
|
||||
statusCwId: 'statusCwId',
|
||||
priorityName: 'priorityName',
|
||||
priorityCwId: 'priorityCwId',
|
||||
ratingName: 'ratingName',
|
||||
ratingCwId: 'ratingCwId',
|
||||
source: 'source',
|
||||
campaignName: 'campaignName',
|
||||
campaignCwId: 'campaignCwId',
|
||||
primarySalesRepName: 'primarySalesRepName',
|
||||
primarySalesRepIdentifier: 'primarySalesRepIdentifier',
|
||||
primarySalesRepCwId: 'primarySalesRepCwId',
|
||||
secondarySalesRepName: 'secondarySalesRepName',
|
||||
secondarySalesRepIdentifier: 'secondarySalesRepIdentifier',
|
||||
secondarySalesRepCwId: 'secondarySalesRepCwId',
|
||||
companyCwId: 'companyCwId',
|
||||
companyName: 'companyName',
|
||||
contactCwId: 'contactCwId',
|
||||
contactName: 'contactName',
|
||||
siteCwId: 'siteCwId',
|
||||
siteName: 'siteName',
|
||||
customerPO: 'customerPO',
|
||||
totalSalesTax: 'totalSalesTax',
|
||||
oppNarrative: 'oppNarrative',
|
||||
typeId: 'typeId',
|
||||
stageId: 'stageId',
|
||||
statusId: 'statusId',
|
||||
taxCodeId: 'taxCodeId',
|
||||
interest: 'interest',
|
||||
probability: 'probability',
|
||||
locationName: 'locationName',
|
||||
locationCwId: 'locationCwId',
|
||||
departmentName: 'departmentName',
|
||||
departmentCwId: 'departmentCwId',
|
||||
source: 'source',
|
||||
primarySalesRepId: 'primarySalesRepId',
|
||||
secondarySalesRepId: 'secondarySalesRepId',
|
||||
companyId: 'companyId',
|
||||
contactId: 'contactId',
|
||||
siteId: 'siteId',
|
||||
customerPO: 'customerPO',
|
||||
locationId: 'locationId',
|
||||
departmentId: 'departmentId',
|
||||
expectedCloseDate: 'expectedCloseDate',
|
||||
pipelineChangeDate: 'pipelineChangeDate',
|
||||
dateBecameLead: 'dateBecameLead',
|
||||
closedDate: 'closedDate',
|
||||
closedFlag: 'closedFlag',
|
||||
closedByName: 'closedByName',
|
||||
closedByCwId: 'closedByCwId',
|
||||
companyId: 'companyId',
|
||||
closedById: 'closedById',
|
||||
productSequence: 'productSequence',
|
||||
cwLastUpdated: 'cwLastUpdated',
|
||||
cwDateEntered: 'cwDateEntered',
|
||||
updatedBy: 'updatedBy',
|
||||
eneteredBy: 'eneteredBy',
|
||||
createdAt: 'createdAt',
|
||||
updatedAt: 'updatedAt'
|
||||
} as const
|
||||
@@ -234,6 +807,87 @@ export const 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 = {
|
||||
id: 'id',
|
||||
name: 'name',
|
||||
@@ -292,6 +946,23 @@ export const 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 = {
|
||||
id: 'id',
|
||||
cwMemberId: 'cwMemberId',
|
||||
|
||||
@@ -8,16 +8,49 @@
|
||||
*
|
||||
* 🟢 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/User.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/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/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/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/SecureValue.ts'
|
||||
export type * from './models/Credential.ts'
|
||||
export type * from './models/GeneratedQuotes.ts'
|
||||
export type * from './models/TaxCode.ts'
|
||||
export type * from './models/CwMember.ts'
|
||||
export type * from './commonInputTypes.ts'
|
||||
File diff suppressed because it is too large
Load Diff
+1641
-124
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?: 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[]
|
||||
}
|
||||
|
||||
|
||||
@@ -1146,6 +1146,11 @@ export type CredentialTypeFindManyArgs<ExtArgs extends runtime.Types.Extensions.
|
||||
* Skip the first `n` CredentialTypes.
|
||||
*/
|
||||
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[]
|
||||
}
|
||||
|
||||
|
||||
@@ -264,6 +264,7 @@ export type CwMemberWhereInput = {
|
||||
cwLastUpdated?: Prisma.DateTimeNullableFilter<"CwMember"> | Date | string | null
|
||||
createdAt?: Prisma.DateTimeFilter<"CwMember"> | Date | string
|
||||
updatedAt?: Prisma.DateTimeFilter<"CwMember"> | Date | string
|
||||
user?: Prisma.XOR<Prisma.UserNullableScalarRelationFilter, Prisma.UserWhereInput> | null
|
||||
}
|
||||
|
||||
export type CwMemberOrderByWithRelationInput = {
|
||||
@@ -278,6 +279,7 @@ export type CwMemberOrderByWithRelationInput = {
|
||||
cwLastUpdated?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||
createdAt?: Prisma.SortOrder
|
||||
updatedAt?: Prisma.SortOrder
|
||||
user?: Prisma.UserOrderByWithRelationInput
|
||||
}
|
||||
|
||||
export type CwMemberWhereUniqueInput = Prisma.AtLeast<{
|
||||
@@ -295,6 +297,7 @@ export type CwMemberWhereUniqueInput = Prisma.AtLeast<{
|
||||
cwLastUpdated?: Prisma.DateTimeNullableFilter<"CwMember"> | Date | string | null
|
||||
createdAt?: Prisma.DateTimeFilter<"CwMember"> | Date | string
|
||||
updatedAt?: Prisma.DateTimeFilter<"CwMember"> | Date | string
|
||||
user?: Prisma.XOR<Prisma.UserNullableScalarRelationFilter, Prisma.UserWhereInput> | null
|
||||
}, "id" | "cwMemberId" | "identifier">
|
||||
|
||||
export type CwMemberOrderByWithAggregationInput = {
|
||||
@@ -345,6 +348,7 @@ export type CwMemberCreateInput = {
|
||||
cwLastUpdated?: Date | string | null
|
||||
createdAt?: Date | string
|
||||
updatedAt?: Date | string
|
||||
user?: Prisma.UserCreateNestedOneWithoutCwMemberInput
|
||||
}
|
||||
|
||||
export type CwMemberUncheckedCreateInput = {
|
||||
@@ -359,6 +363,7 @@ export type CwMemberUncheckedCreateInput = {
|
||||
cwLastUpdated?: Date | string | null
|
||||
createdAt?: Date | string
|
||||
updatedAt?: Date | string
|
||||
user?: Prisma.UserUncheckedCreateNestedOneWithoutCwMemberInput
|
||||
}
|
||||
|
||||
export type CwMemberUpdateInput = {
|
||||
@@ -373,6 +378,7 @@ export type CwMemberUpdateInput = {
|
||||
cwLastUpdated?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
user?: Prisma.UserUpdateOneWithoutCwMemberNestedInput
|
||||
}
|
||||
|
||||
export type CwMemberUncheckedUpdateInput = {
|
||||
@@ -387,6 +393,7 @@ export type CwMemberUncheckedUpdateInput = {
|
||||
cwLastUpdated?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
user?: Prisma.UserUncheckedUpdateOneWithoutCwMemberNestedInput
|
||||
}
|
||||
|
||||
export type CwMemberCreateManyInput = {
|
||||
@@ -431,6 +438,11 @@ export type CwMemberUncheckedUpdateManyInput = {
|
||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
}
|
||||
|
||||
export type CwMemberNullableScalarRelationFilter = {
|
||||
is?: Prisma.CwMemberWhereInput | null
|
||||
isNot?: Prisma.CwMemberWhereInput | null
|
||||
}
|
||||
|
||||
export type CwMemberCountOrderByAggregateInput = {
|
||||
id?: Prisma.SortOrder
|
||||
cwMemberId?: Prisma.SortOrder
|
||||
@@ -481,6 +493,94 @@ export type CwMemberSumOrderByAggregateInput = {
|
||||
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<{
|
||||
@@ -495,6 +595,7 @@ export type CwMemberSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs
|
||||
cwLastUpdated?: boolean
|
||||
createdAt?: boolean
|
||||
updatedAt?: boolean
|
||||
user?: boolean | Prisma.CwMember$userArgs<ExtArgs>
|
||||
}, ExtArgs["result"]["cwMember"]>
|
||||
|
||||
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 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> = {
|
||||
name: "CwMember"
|
||||
objects: {}
|
||||
objects: {
|
||||
user: Prisma.$UserPayload<ExtArgs> | null
|
||||
}
|
||||
scalars: runtime.Types.Extensions.GetPayloadResult<{
|
||||
id: string
|
||||
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> {
|
||||
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.
|
||||
* @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?: Prisma.CwMemberOmit<ExtArgs> | null
|
||||
/**
|
||||
* Choose, which related nodes to fetch as well
|
||||
*/
|
||||
include?: Prisma.CwMemberInclude<ExtArgs> | null
|
||||
/**
|
||||
* Filter, which CwMember to fetch.
|
||||
*/
|
||||
@@ -1024,6 +1137,10 @@ export type CwMemberFindUniqueOrThrowArgs<ExtArgs extends runtime.Types.Extensio
|
||||
* Omit specific fields from the CwMember
|
||||
*/
|
||||
omit?: Prisma.CwMemberOmit<ExtArgs> | null
|
||||
/**
|
||||
* Choose, which related nodes to fetch as well
|
||||
*/
|
||||
include?: Prisma.CwMemberInclude<ExtArgs> | null
|
||||
/**
|
||||
* 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?: Prisma.CwMemberOmit<ExtArgs> | null
|
||||
/**
|
||||
* Choose, which related nodes to fetch as well
|
||||
*/
|
||||
include?: Prisma.CwMemberInclude<ExtArgs> | null
|
||||
/**
|
||||
* Filter, which CwMember to fetch.
|
||||
*/
|
||||
@@ -1090,6 +1211,10 @@ export type CwMemberFindFirstOrThrowArgs<ExtArgs extends runtime.Types.Extension
|
||||
* Omit specific fields from the CwMember
|
||||
*/
|
||||
omit?: Prisma.CwMemberOmit<ExtArgs> | null
|
||||
/**
|
||||
* Choose, which related nodes to fetch as well
|
||||
*/
|
||||
include?: Prisma.CwMemberInclude<ExtArgs> | null
|
||||
/**
|
||||
* 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?: Prisma.CwMemberOmit<ExtArgs> | null
|
||||
/**
|
||||
* Choose, which related nodes to fetch as well
|
||||
*/
|
||||
include?: Prisma.CwMemberInclude<ExtArgs> | null
|
||||
/**
|
||||
* Filter, which CwMembers to fetch.
|
||||
*/
|
||||
@@ -1166,6 +1295,11 @@ export type CwMemberFindManyArgs<ExtArgs extends runtime.Types.Extensions.Intern
|
||||
* Skip the first `n` CwMembers.
|
||||
*/
|
||||
skip?: number
|
||||
/**
|
||||
* {@link https://www.prisma.io/docs/concepts/components/prisma-client/distinct Distinct Docs}
|
||||
*
|
||||
* Filter by unique combinations of CwMembers.
|
||||
*/
|
||||
distinct?: Prisma.CwMemberScalarFieldEnum | Prisma.CwMemberScalarFieldEnum[]
|
||||
}
|
||||
|
||||
@@ -1181,6 +1315,10 @@ export type CwMemberCreateArgs<ExtArgs extends runtime.Types.Extensions.Internal
|
||||
* Omit specific fields from the CwMember
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
@@ -1229,6 +1367,10 @@ export type CwMemberUpdateArgs<ExtArgs extends runtime.Types.Extensions.Internal
|
||||
* Omit specific fields from the CwMember
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
@@ -1295,6 +1437,10 @@ export type CwMemberUpsertArgs<ExtArgs extends runtime.Types.Extensions.Internal
|
||||
* Omit specific fields from the CwMember
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
@@ -1321,6 +1467,10 @@ export type CwMemberDeleteArgs<ExtArgs extends runtime.Types.Extensions.Internal
|
||||
* Omit specific fields from the CwMember
|
||||
*/
|
||||
omit?: Prisma.CwMemberOmit<ExtArgs> | null
|
||||
/**
|
||||
* Choose, which related nodes to fetch as well
|
||||
*/
|
||||
include?: Prisma.CwMemberInclude<ExtArgs> | null
|
||||
/**
|
||||
* Filter which CwMember to delete.
|
||||
*/
|
||||
@@ -1341,6 +1491,25 @@ export type CwMemberDeleteManyArgs<ExtArgs extends runtime.Types.Extensions.Inte
|
||||
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
|
||||
*/
|
||||
@@ -1353,4 +1522,8 @@ export type CwMemberDefaultArgs<ExtArgs extends runtime.Types.Extensions.Interna
|
||||
* Omit specific fields from the CwMember
|
||||
*/
|
||||
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?: 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[]
|
||||
}
|
||||
|
||||
|
||||
+4085
-1574
File diff suppressed because it is too large
Load Diff
@@ -1175,6 +1175,11 @@ export type RoleFindManyArgs<ExtArgs extends runtime.Types.Extensions.InternalAr
|
||||
* Skip the first `n` Roles.
|
||||
*/
|
||||
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[]
|
||||
}
|
||||
|
||||
|
||||
@@ -1192,6 +1192,11 @@ export type SecureValueFindManyArgs<ExtArgs extends runtime.Types.Extensions.Int
|
||||
* Skip the first `n` SecureValues.
|
||||
*/
|
||||
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[]
|
||||
}
|
||||
|
||||
|
||||
@@ -361,22 +361,10 @@ export type SessionOrderByRelationAggregateInput = {
|
||||
_count?: Prisma.SortOrder
|
||||
}
|
||||
|
||||
export type StringFieldUpdateOperationsInput = {
|
||||
set?: string
|
||||
}
|
||||
|
||||
export type DateTimeFieldUpdateOperationsInput = {
|
||||
set?: Date | string
|
||||
}
|
||||
|
||||
export type BoolFieldUpdateOperationsInput = {
|
||||
set?: boolean
|
||||
}
|
||||
|
||||
export type NullableDateTimeFieldUpdateOperationsInput = {
|
||||
set?: Date | string | null
|
||||
}
|
||||
|
||||
export type SessionCreateNestedManyWithoutUserInput = {
|
||||
create?: Prisma.XOR<Prisma.SessionCreateWithoutUserInput, Prisma.SessionUncheckedCreateWithoutUserInput> | Prisma.SessionCreateWithoutUserInput[] | Prisma.SessionUncheckedCreateWithoutUserInput[]
|
||||
connectOrCreate?: Prisma.SessionCreateOrConnectWithoutUserInput | Prisma.SessionCreateOrConnectWithoutUserInput[]
|
||||
@@ -1208,6 +1196,11 @@ export type SessionFindManyArgs<ExtArgs extends runtime.Types.Extensions.Interna
|
||||
* Skip the first `n` Sessions.
|
||||
*/
|
||||
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[]
|
||||
}
|
||||
|
||||
|
||||
@@ -20,15 +20,25 @@ export type UnifiSiteModel = runtime.Types.Result.DefaultSelection<Prisma.$Unifi
|
||||
|
||||
export type AggregateUnifiSite = {
|
||||
_count: UnifiSiteCountAggregateOutputType | null
|
||||
_avg: UnifiSiteAvgAggregateOutputType | null
|
||||
_sum: UnifiSiteSumAggregateOutputType | null
|
||||
_min: UnifiSiteMinAggregateOutputType | null
|
||||
_max: UnifiSiteMaxAggregateOutputType | null
|
||||
}
|
||||
|
||||
export type UnifiSiteAvgAggregateOutputType = {
|
||||
companyId: number | null
|
||||
}
|
||||
|
||||
export type UnifiSiteSumAggregateOutputType = {
|
||||
companyId: number | null
|
||||
}
|
||||
|
||||
export type UnifiSiteMinAggregateOutputType = {
|
||||
id: string | null
|
||||
name: string | null
|
||||
siteId: string | null
|
||||
companyId: string | null
|
||||
companyId: number | null
|
||||
createdAt: Date | null
|
||||
updatedAt: Date | null
|
||||
}
|
||||
@@ -37,7 +47,7 @@ export type UnifiSiteMaxAggregateOutputType = {
|
||||
id: string | null
|
||||
name: string | null
|
||||
siteId: string | null
|
||||
companyId: string | null
|
||||
companyId: number | null
|
||||
createdAt: 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 = {
|
||||
id?: true
|
||||
name?: true
|
||||
@@ -116,6 +134,18 @@ export type UnifiSiteAggregateArgs<ExtArgs extends runtime.Types.Extensions.Inte
|
||||
* Count returned UnifiSites
|
||||
**/
|
||||
_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}
|
||||
*
|
||||
@@ -149,6 +179,8 @@ export type UnifiSiteGroupByArgs<ExtArgs extends runtime.Types.Extensions.Intern
|
||||
take?: number
|
||||
skip?: number
|
||||
_count?: UnifiSiteCountAggregateInputType | true
|
||||
_avg?: UnifiSiteAvgAggregateInputType
|
||||
_sum?: UnifiSiteSumAggregateInputType
|
||||
_min?: UnifiSiteMinAggregateInputType
|
||||
_max?: UnifiSiteMaxAggregateInputType
|
||||
}
|
||||
@@ -157,10 +189,12 @@ export type UnifiSiteGroupByOutputType = {
|
||||
id: string
|
||||
name: string
|
||||
siteId: string
|
||||
companyId: string | null
|
||||
companyId: number | null
|
||||
createdAt: Date
|
||||
updatedAt: Date
|
||||
_count: UnifiSiteCountAggregateOutputType | null
|
||||
_avg: UnifiSiteAvgAggregateOutputType | null
|
||||
_sum: UnifiSiteSumAggregateOutputType | null
|
||||
_min: UnifiSiteMinAggregateOutputType | null
|
||||
_max: UnifiSiteMaxAggregateOutputType | null
|
||||
}
|
||||
@@ -187,7 +221,7 @@ export type UnifiSiteWhereInput = {
|
||||
id?: Prisma.StringFilter<"UnifiSite"> | string
|
||||
name?: 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
|
||||
updatedAt?: Prisma.DateTimeFilter<"UnifiSite"> | Date | string
|
||||
company?: Prisma.XOR<Prisma.CompanyNullableScalarRelationFilter, Prisma.CompanyWhereInput> | null
|
||||
@@ -210,7 +244,7 @@ export type UnifiSiteWhereUniqueInput = Prisma.AtLeast<{
|
||||
OR?: Prisma.UnifiSiteWhereInput[]
|
||||
NOT?: Prisma.UnifiSiteWhereInput | Prisma.UnifiSiteWhereInput[]
|
||||
name?: Prisma.StringFilter<"UnifiSite"> | string
|
||||
companyId?: Prisma.StringNullableFilter<"UnifiSite"> | string | null
|
||||
companyId?: Prisma.IntNullableFilter<"UnifiSite"> | number | null
|
||||
createdAt?: Prisma.DateTimeFilter<"UnifiSite"> | Date | string
|
||||
updatedAt?: Prisma.DateTimeFilter<"UnifiSite"> | Date | string
|
||||
company?: Prisma.XOR<Prisma.CompanyNullableScalarRelationFilter, Prisma.CompanyWhereInput> | null
|
||||
@@ -224,8 +258,10 @@ export type UnifiSiteOrderByWithAggregationInput = {
|
||||
createdAt?: Prisma.SortOrder
|
||||
updatedAt?: Prisma.SortOrder
|
||||
_count?: Prisma.UnifiSiteCountOrderByAggregateInput
|
||||
_avg?: Prisma.UnifiSiteAvgOrderByAggregateInput
|
||||
_max?: Prisma.UnifiSiteMaxOrderByAggregateInput
|
||||
_min?: Prisma.UnifiSiteMinOrderByAggregateInput
|
||||
_sum?: Prisma.UnifiSiteSumOrderByAggregateInput
|
||||
}
|
||||
|
||||
export type UnifiSiteScalarWhereWithAggregatesInput = {
|
||||
@@ -235,7 +271,7 @@ export type UnifiSiteScalarWhereWithAggregatesInput = {
|
||||
id?: Prisma.StringWithAggregatesFilter<"UnifiSite"> | string
|
||||
name?: 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
|
||||
updatedAt?: Prisma.DateTimeWithAggregatesFilter<"UnifiSite"> | Date | string
|
||||
}
|
||||
@@ -253,7 +289,7 @@ export type UnifiSiteUncheckedCreateInput = {
|
||||
id?: string
|
||||
name: string
|
||||
siteId: string
|
||||
companyId?: string | null
|
||||
companyId?: number | null
|
||||
createdAt?: Date | string
|
||||
updatedAt?: Date | string
|
||||
}
|
||||
@@ -271,7 +307,7 @@ export type UnifiSiteUncheckedUpdateInput = {
|
||||
id?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
siteId?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
companyId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
companyId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
}
|
||||
@@ -280,7 +316,7 @@ export type UnifiSiteCreateManyInput = {
|
||||
id?: string
|
||||
name: string
|
||||
siteId: string
|
||||
companyId?: string | null
|
||||
companyId?: number | null
|
||||
createdAt?: Date | string
|
||||
updatedAt?: Date | string
|
||||
}
|
||||
@@ -297,7 +333,7 @@ export type UnifiSiteUncheckedUpdateManyInput = {
|
||||
id?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
siteId?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
companyId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
companyId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
}
|
||||
@@ -311,6 +347,10 @@ export type UnifiSiteCountOrderByAggregateInput = {
|
||||
updatedAt?: Prisma.SortOrder
|
||||
}
|
||||
|
||||
export type UnifiSiteAvgOrderByAggregateInput = {
|
||||
companyId?: Prisma.SortOrder
|
||||
}
|
||||
|
||||
export type UnifiSiteMaxOrderByAggregateInput = {
|
||||
id?: Prisma.SortOrder
|
||||
name?: Prisma.SortOrder
|
||||
@@ -329,6 +369,10 @@ export type UnifiSiteMinOrderByAggregateInput = {
|
||||
updatedAt?: Prisma.SortOrder
|
||||
}
|
||||
|
||||
export type UnifiSiteSumOrderByAggregateInput = {
|
||||
companyId?: Prisma.SortOrder
|
||||
}
|
||||
|
||||
export type UnifiSiteListRelationFilter = {
|
||||
every?: Prisma.UnifiSiteWhereInput
|
||||
some?: Prisma.UnifiSiteWhereInput
|
||||
@@ -430,7 +474,7 @@ export type UnifiSiteScalarWhereInput = {
|
||||
id?: Prisma.StringFilter<"UnifiSite"> | string
|
||||
name?: 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
|
||||
updatedAt?: Prisma.DateTimeFilter<"UnifiSite"> | Date | string
|
||||
}
|
||||
@@ -528,7 +572,7 @@ export type $UnifiSitePayload<ExtArgs extends runtime.Types.Extensions.InternalA
|
||||
id: string
|
||||
name: string
|
||||
siteId: string
|
||||
companyId: string | null
|
||||
companyId: number | null
|
||||
createdAt: Date
|
||||
updatedAt: Date
|
||||
}, ExtArgs["result"]["unifiSite"]>
|
||||
@@ -958,7 +1002,7 @@ export interface UnifiSiteFieldRefs {
|
||||
readonly id: Prisma.FieldRef<"UnifiSite", 'String'>
|
||||
readonly name: 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 updatedAt: Prisma.FieldRef<"UnifiSite", 'DateTime'>
|
||||
}
|
||||
@@ -1157,6 +1201,11 @@ export type UnifiSiteFindManyArgs<ExtArgs extends runtime.Types.Extensions.Inter
|
||||
* Skip the first `n` UnifiSites.
|
||||
*/
|
||||
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[]
|
||||
}
|
||||
|
||||
|
||||
+2754
-118
File diff suppressed because it is too large
Load Diff
+2
-1
@@ -29,6 +29,7 @@
|
||||
"utils:gen_private_keys": "bun ./utils/genPrivateKeys",
|
||||
"utils:create_admin_role": "bun ./utils/createAdminRole",
|
||||
"utils:assign_user_role": "bun ./utils/assignUserRole",
|
||||
"utils:gen_access_token": "bun ./utils/generate24HourAccessToken.ts",
|
||||
"utils:test_webserver": "bun ./utils/testWebserver.ts",
|
||||
"utils:test_adjustments_poll": "bun ./utils/testAdjustmentsPoll.ts",
|
||||
"utils:analyze_cw": "python3 debug-scripts/analyze-cw-calls.py",
|
||||
@@ -45,6 +46,7 @@
|
||||
"blakets": "^0.1.12",
|
||||
"cors": "^2.8.6",
|
||||
"cuid": "^3.0.0",
|
||||
"dalpuri": "workspace:*",
|
||||
"hono": "^4.11.5",
|
||||
"ioredis": "^5.10.0",
|
||||
"jsonwebtoken": "^9.0.3",
|
||||
@@ -54,7 +56,6 @@
|
||||
"pg-boss": "^12.14.0",
|
||||
"prisma": "^7.3.0",
|
||||
"socket.io": "^4.8.3",
|
||||
"socket.io-client": "^4.8.3",
|
||||
"zod": "^4.3.6",
|
||||
"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 $$;
|
||||
+793
-25
@@ -27,6 +27,18 @@ enum FaxType {
|
||||
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.
|
||||
enum GenderType {
|
||||
MALE
|
||||
@@ -96,6 +108,59 @@ enum OpportunityInterest {
|
||||
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 {
|
||||
id String @id @default(uuid())
|
||||
sessionKey String @unique @default(cuid())
|
||||
@@ -108,21 +173,25 @@ model Session {
|
||||
}
|
||||
|
||||
model User {
|
||||
id String @id @default(cuid())
|
||||
roles Role[]
|
||||
permissions String?
|
||||
login String @unique
|
||||
name String?
|
||||
email String @unique
|
||||
emailVerified DateTime?
|
||||
image String?
|
||||
id String @id @default(uuid())
|
||||
roles Role[]
|
||||
permissions String?
|
||||
login String @unique
|
||||
firstName String?
|
||||
lastName String?
|
||||
email String @unique
|
||||
image String?
|
||||
|
||||
title String?
|
||||
|
||||
active Boolean @default(true)
|
||||
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?
|
||||
|
||||
sessions Session[]
|
||||
@@ -136,8 +205,16 @@ model User {
|
||||
generatedQuotes GeneratedQuotes[]
|
||||
companyAddresses CompanyAddress[]
|
||||
|
||||
updatedBy String?
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
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 {
|
||||
@@ -170,7 +247,9 @@ model CorporateLocation {
|
||||
|
||||
inactiveFlag Boolean @default(false) // Optima Only field, not synced to CW
|
||||
|
||||
opportunities Opportunity[]
|
||||
opportunities Opportunity[]
|
||||
serviceBoards ServiceTicketBoard[]
|
||||
productDataRecords ProductData[]
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
@@ -232,11 +311,13 @@ model Company {
|
||||
deletedBy User? @relation("DeletedBy", fields: [deletedById], references: [cwIdentifier])
|
||||
enteredBy User? @relation("EnteredBy", fields: [enteredById], references: [cwIdentifier])
|
||||
|
||||
enteredAt DateTime @default(now())
|
||||
deletedAt DateTime?
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
serviceTickets ServiceTicket[]
|
||||
billingServiceTickets ServiceTicket[] @relation("BillingCompany")
|
||||
}
|
||||
|
||||
model CompanyAddress {
|
||||
@@ -270,6 +351,8 @@ model CompanyAddress {
|
||||
|
||||
contacts Contact[]
|
||||
oppportunities Opportunity[]
|
||||
serviceTickets ServiceTicket[]
|
||||
billingTickets ServiceTicket[] @relation("BillingAddress")
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
@@ -304,29 +387,190 @@ model Contact {
|
||||
company Company? @relation(fields: [companyId], references: [id])
|
||||
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())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
model CatalogItem {
|
||||
id String @id @default(cuid())
|
||||
cwCatalogId Int @unique
|
||||
identifier String? @unique
|
||||
name String
|
||||
description String?
|
||||
id Int @unique
|
||||
uid String @id @default(uuid())
|
||||
|
||||
identifier String? @unique
|
||||
name String
|
||||
description String?
|
||||
|
||||
customerDescription String?
|
||||
internalNotes String?
|
||||
|
||||
linkedItems CatalogItem[] @relation("LinkedItems")
|
||||
linkedTo CatalogItem[] @relation("LinkedItems")
|
||||
|
||||
category String?
|
||||
categoryCwId Int?
|
||||
subcategory String?
|
||||
subcategoryCwId Int?
|
||||
subcategoryId Int
|
||||
subcategory CatalogSubcategory @relation(fields: [subcategoryId], references: [id])
|
||||
|
||||
manufacturer String?
|
||||
manufactureCwId Int?
|
||||
manufacturerId Int?
|
||||
manufacturer CatalogManufacturer? @relation(fields: [manufacturerId], references: [id])
|
||||
|
||||
partNumber String?
|
||||
|
||||
@@ -337,12 +581,403 @@ model CatalogItem {
|
||||
price Float
|
||||
cost Float
|
||||
|
||||
inventory ProductInventory[]
|
||||
minimumStockByWarehouses MinimumStockByWarehouse[]
|
||||
|
||||
productDataRecords ProductData[]
|
||||
|
||||
inactive Boolean @default(false)
|
||||
salesTaxable Boolean @default(true)
|
||||
|
||||
onHand Int @default(0)
|
||||
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())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
@@ -393,17 +1028,22 @@ model Opportunity {
|
||||
name 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[]
|
||||
|
||||
typeId Int
|
||||
type OpportunityType @relation(fields: [typeId], references: [id])
|
||||
|
||||
stageName String?
|
||||
stageCwId Int?
|
||||
stageId Int?
|
||||
stage OpportunityStage? @relation(fields: [stageId], references: [id])
|
||||
|
||||
statusId Int?
|
||||
status OpportunityStatus? @relation(fields: [statusId], references: [id])
|
||||
|
||||
taxCodeId Int?
|
||||
taxCode TaxCode? @relation(fields: [taxCodeId], references: [id])
|
||||
|
||||
interest OpportunityInterest?
|
||||
probability Float @default(0)
|
||||
|
||||
@@ -447,6 +1087,111 @@ model Opportunity {
|
||||
// When present, fetchProducts() uses this order instead of CW sequenceNumber.
|
||||
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())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
@@ -523,6 +1268,27 @@ model GeneratedQuotes {
|
||||
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 {
|
||||
id String @id @default(cuid())
|
||||
|
||||
@@ -535,6 +1301,8 @@ model CwMember {
|
||||
|
||||
apiKey String?
|
||||
|
||||
user User?
|
||||
|
||||
cwLastUpdated DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@ -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,3 +1,4 @@
|
||||
export { default as redirect } from "./redirect";
|
||||
export { default as refresh } from "./refresh";
|
||||
export { default as uri } from "./uri";
|
||||
export { default as callback } from "./callback";
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { Hono } from "hono/tiny";
|
||||
import { createRoute } from "../../modules/api-utils/createRoute";
|
||||
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";
|
||||
|
||||
const AUTH_CALLBACK_TTL_SECONDS = 300; // 5 minutes
|
||||
|
||||
/* /v1/auth/redirect */
|
||||
export default createRoute("get", ["/redirect"], async (c) => {
|
||||
c.status(200);
|
||||
@@ -18,10 +20,13 @@ export default createRoute("get", ["/redirect"], async (c) => {
|
||||
const callbackKey = c.req.query().state as string;
|
||||
const tokens = await users.authenticate(authResult);
|
||||
|
||||
io.of(`/auth_callback`).emit(`auth:login:callback:${callbackKey}`, {
|
||||
accessToken: tokens.accessToken,
|
||||
refreshToken: tokens.refreshToken,
|
||||
});
|
||||
// Store tokens in Redis so the UI can poll for them
|
||||
await redis.set(
|
||||
`auth:cb:${callbackKey}`,
|
||||
JSON.stringify({ accessToken: tokens.accessToken, refreshToken: tokens.refreshToken }),
|
||||
"EX",
|
||||
AUTH_CALLBACK_TTL_SECONDS,
|
||||
);
|
||||
|
||||
// Close the window because duh
|
||||
return c.html(
|
||||
@@ -29,11 +34,4 @@ export default createRoute("get", ["/redirect"], async (c) => {
|
||||
window.close();
|
||||
</script>`,
|
||||
);
|
||||
|
||||
return c.json({
|
||||
status: 200,
|
||||
message: "Auth Redirect Endpoint",
|
||||
data: authResult,
|
||||
successful: true,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,7 +4,6 @@ import { companies } from "../../../managers/companies";
|
||||
import { apiResponse } from "../../../modules/api-utils/apiResponse";
|
||||
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||
import { authMiddleware } from "../../middleware/authorization";
|
||||
import GenericError from "../../../Errors/GenericError";
|
||||
import { processObjectValuePerms } from "../../../modules/permission-utils/processObjectPermissions";
|
||||
|
||||
/* /v1/company/companies/[id] */
|
||||
@@ -14,34 +13,17 @@ export default createRoute(
|
||||
|
||||
async (c) => {
|
||||
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 =
|
||||
c.req.query("includePrimaryContact") === "true";
|
||||
const includeAllContacts = c.req.query("includeAllContacts") === "true";
|
||||
|
||||
// Check for address-specific permission if includeAddress is requested
|
||||
if (includeAddress) {
|
||||
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 includeAllContacts =
|
||||
c.req.query("includeAllContacts") === "true" &&
|
||||
!!user &&
|
||||
(await user.hasPermission("company.fetch.contacts"));
|
||||
|
||||
const companyData = company.toJson({
|
||||
includeAddress,
|
||||
@@ -54,6 +36,13 @@ export default createRoute(
|
||||
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(
|
||||
"Company Fetched Successfully!",
|
||||
gatedData,
|
||||
|
||||
@@ -2,36 +2,35 @@ import { createRoute } from "../../modules/api-utils/createRoute";
|
||||
import { apiResponse } from "../../modules/api-utils/apiResponse";
|
||||
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||
import { authMiddleware } from "../middleware/authorization";
|
||||
import { getMemberCache } from "../../modules/cw-utils/members/memberCache";
|
||||
import { prisma } from "../../constants";
|
||||
|
||||
/* GET /v1/cw/members */
|
||||
export default createRoute(
|
||||
"get",
|
||||
["/members"],
|
||||
async (c) => {
|
||||
const cache = await getMemberCache();
|
||||
|
||||
const activeOnly = c.req.query("active") !== "false";
|
||||
|
||||
const members = cache
|
||||
.filter((m) => (activeOnly ? !m.inactiveFlag : true))
|
||||
.map((m) => ({
|
||||
id: m.id,
|
||||
identifier: m.identifier,
|
||||
firstName: m.firstName,
|
||||
lastName: m.lastName,
|
||||
name: `${m.firstName} ${m.lastName}`.trim(),
|
||||
officeEmail: m.officeEmail,
|
||||
inactive: m.inactiveFlag,
|
||||
}));
|
||||
const dbMembers = await prisma.cwMember.findMany({
|
||||
where: activeOnly ? { inactiveFlag: false } : undefined,
|
||||
orderBy: [{ firstName: "asc" }, { lastName: "asc" }],
|
||||
});
|
||||
|
||||
const sorted = members.sort((a, b) => a.name.localeCompare(b.name));
|
||||
const members = dbMembers.map((m) => ({
|
||||
id: m.cwMemberId,
|
||||
identifier: m.identifier,
|
||||
firstName: m.firstName,
|
||||
lastName: m.lastName,
|
||||
name: `${m.firstName} ${m.lastName}`.trim(),
|
||||
officeEmail: m.officeEmail,
|
||||
inactive: m.inactiveFlag,
|
||||
}));
|
||||
|
||||
const response = apiResponse.successful(
|
||||
"CW members fetched successfully!",
|
||||
sorted,
|
||||
members
|
||||
);
|
||||
return c.json(response, response.status as ContentfulStatusCode);
|
||||
},
|
||||
authMiddleware(),
|
||||
authMiddleware()
|
||||
);
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { default as callback } from "./callback";
|
||||
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 };
|
||||
|
||||
@@ -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()
|
||||
);
|
||||
@@ -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()
|
||||
);
|
||||
@@ -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;
|
||||
@@ -4,20 +4,6 @@ import { apiResponse } from "../../../modules/api-utils/apiResponse";
|
||||
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||
import { authMiddleware } from "../../middleware/authorization";
|
||||
import { processObjectValuePerms } from "../../../modules/permission-utils/processObjectPermissions";
|
||||
import 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";
|
||||
|
||||
/* GET /v1/sales/opportunities/opportunity/:identifier?include=notes,contacts,products,quotes */
|
||||
@@ -31,102 +17,13 @@ export default createRoute(
|
||||
includeParam
|
||||
.split(",")
|
||||
.map((s) => s.trim().toLowerCase())
|
||||
.filter(Boolean),
|
||||
.filter(Boolean)
|
||||
);
|
||||
|
||||
// ── Quick DB lookup (≈3ms) to get cwOpportunityId for pre-warming ──
|
||||
const isNumeric = /^\d+$/.test(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,
|
||||
},
|
||||
});
|
||||
// Fetch the opportunity from local DB
|
||||
const item = await opportunities.fetchItem(identifier);
|
||||
|
||||
if (!dbRecord) {
|
||||
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)
|
||||
// Fetch sub-resources as requested
|
||||
const subResourcePromises: Record<string, Promise<any>> = {
|
||||
_site: item.fetchSite(),
|
||||
};
|
||||
@@ -154,7 +51,7 @@ export default createRoute(
|
||||
const gatedData = await processObjectValuePerms(
|
||||
item.toJson(),
|
||||
"obj.opportunity",
|
||||
c.get("user"),
|
||||
c.get("user")
|
||||
);
|
||||
|
||||
const originalOpportunityNoteText = (gatedData as any).notes;
|
||||
@@ -175,9 +72,9 @@ export default createRoute(
|
||||
|
||||
const response = apiResponse.successful(
|
||||
"Opportunity fetched successfully!",
|
||||
gatedData,
|
||||
gatedData
|
||||
);
|
||||
return c.json(response, response.status as ContentfulStatusCode);
|
||||
},
|
||||
authMiddleware({ permissions: ["sales.opportunity.fetch"] }),
|
||||
authMiddleware({ permissions: ["sales.opportunity.fetch"] })
|
||||
);
|
||||
|
||||
@@ -2,7 +2,7 @@ import { createRoute } from "../../modules/api-utils/createRoute";
|
||||
import { apiResponse } from "../../modules/api-utils/apiResponse";
|
||||
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||
import { authMiddleware } from "../middleware/authorization";
|
||||
import { QUOTE_STATUSES } from "../../types/QuoteStatuses";
|
||||
import { prisma } from "../../constants";
|
||||
|
||||
/* GET /v1/sales/opportunity-types */
|
||||
export default createRoute(
|
||||
@@ -10,9 +10,14 @@ export default createRoute(
|
||||
["/opportunity-types"],
|
||||
|
||||
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(
|
||||
"Opportunity Types Fetched Successfully!",
|
||||
QUOTE_STATUSES,
|
||||
types,
|
||||
);
|
||||
return c.json(response, response.status as ContentfulStatusCode);
|
||||
},
|
||||
|
||||
@@ -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 downloadQuote } from "./opportunities/[id]/quotes/download";
|
||||
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 fetchByUserId } from "./opportunities/fetchByUserId";
|
||||
import { default as workflowDispatch } from "./opportunities/[id]/workflow/dispatch";
|
||||
@@ -63,6 +65,8 @@ export {
|
||||
previewQuote,
|
||||
downloadQuote,
|
||||
fetchDownloads,
|
||||
fetchNarrative,
|
||||
updateNarrative,
|
||||
refresh,
|
||||
updateOpportunity,
|
||||
workflowDispatch,
|
||||
|
||||
@@ -5,19 +5,6 @@ import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||
import { authMiddleware } from "../../../middleware/authorization";
|
||||
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";
|
||||
|
||||
/* GET /v1/sales/opportunities/opportunity/:identifier?include=notes,contacts,products,quotes */
|
||||
@@ -31,102 +18,15 @@ export default createRoute(
|
||||
includeParam
|
||||
.split(",")
|
||||
.map((s) => s.trim().toLowerCase())
|
||||
.filter(Boolean),
|
||||
.filter(Boolean)
|
||||
);
|
||||
|
||||
// ── Quick DB lookup (≈3ms) to get cwOpportunityId for pre-warming ──
|
||||
const isNumeric = /^\d+$/.test(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,
|
||||
},
|
||||
});
|
||||
// Fetch the opportunity from local DB
|
||||
const item = await opportunities.fetchItem(identifier);
|
||||
|
||||
if (!dbRecord) {
|
||||
throw new GenericError({
|
||||
message: "Opportunity not found",
|
||||
name: "OpportunityNotFound",
|
||||
cause: `No opportunity exists with identifier '${identifier}'`,
|
||||
status: 404,
|
||||
});
|
||||
}
|
||||
console.log("Fetched opportunity:", item ? item.toJson() : null);
|
||||
|
||||
// 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)
|
||||
// Fetch sub-resources as requested
|
||||
const subResourcePromises: Record<string, Promise<any>> = {
|
||||
_site: item.fetchSite(),
|
||||
};
|
||||
@@ -147,7 +47,7 @@ export default createRoute(
|
||||
subResourcePromises.quotes = generatedQuotes
|
||||
.fetchByOpportunity(item.id)
|
||||
.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(
|
||||
item.toJson(),
|
||||
"obj.opportunity",
|
||||
c.get("user"),
|
||||
c.get("user")
|
||||
);
|
||||
|
||||
const originalOpportunityNoteText = (gatedData as any).notes;
|
||||
@@ -179,9 +79,9 @@ export default createRoute(
|
||||
|
||||
const response = apiResponse.successful(
|
||||
"Opportunity fetched successfully!",
|
||||
gatedData,
|
||||
gatedData
|
||||
);
|
||||
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 { ContentfulStatusCode } from "hono/utils/http-status";
|
||||
import { authMiddleware } from "../../../../middleware/authorization";
|
||||
import { resolveMember } from "../../../../../modules/cw-utils/members/memberCache";
|
||||
import { prisma } from "../../../../../constants";
|
||||
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 */
|
||||
export default createRoute(
|
||||
"post",
|
||||
@@ -38,10 +52,10 @@ export default createRoute(
|
||||
: null,
|
||||
flagged: created.flagged,
|
||||
enteredBy: await resolveMember(created.enteredBy),
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
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 { authMiddleware } from "../../../../middleware/authorization";
|
||||
import GenericError from "../../../../../Errors/GenericError";
|
||||
import { resolveMember } from "../../../../../modules/cw-utils/members/memberCache";
|
||||
import { prisma } from "../../../../../constants";
|
||||
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 */
|
||||
export default createRoute(
|
||||
"patch",
|
||||
@@ -48,10 +62,10 @@ export default createRoute(
|
||||
: null,
|
||||
flagged: updated.flagged,
|
||||
enteredBy: await resolveMember(updated.enteredBy),
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
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 { authMiddleware } from "../../../../middleware/authorization";
|
||||
import { processObjectValuePerms } from "../../../../../modules/permission-utils/processObjectPermissions";
|
||||
import { ForecastProductController } from "../../../../../controllers/ForecastProductController";
|
||||
import { opportunityCw } from "../../../../../modules/cw-utils/opportunities/opportunities";
|
||||
import { z } from "zod";
|
||||
|
||||
const productItemSchema = z
|
||||
@@ -26,6 +28,8 @@ const productItemSchema = z
|
||||
recurringCost: z.number().optional(),
|
||||
cycles: z.number().int().min(0).optional(),
|
||||
sequenceNumber: z.number().int().min(0).optional(),
|
||||
procurementNotes: z.string().optional(),
|
||||
productNarrative: z.string().optional(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
@@ -34,12 +38,25 @@ const addProductSchema = z.union([
|
||||
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 */
|
||||
export default createRoute(
|
||||
"post",
|
||||
["/opportunities/opportunity/:identifier/products"],
|
||||
async (c) => {
|
||||
const identifier = c.req.param("identifier");
|
||||
const identifier = c.req.param("identifier") as string;
|
||||
const body = await c.req.json();
|
||||
|
||||
const validated = addProductSchema.parse(body);
|
||||
@@ -56,46 +73,182 @@ export default createRoute(
|
||||
|
||||
const item = await opportunities.fetchRecord(identifier);
|
||||
|
||||
// Strip customerDescription from forecast payloads — CW only accepts
|
||||
// it on procurement products, not forecast items.
|
||||
const customerDescriptions = gatedItems.map(
|
||||
(g: any) => g.customerDescription,
|
||||
);
|
||||
const forecastPayloads = gatedItems.map(
|
||||
({ customerDescription, ...rest }: any) => rest,
|
||||
);
|
||||
// Procurement-first: when catalogItem is available after field-level
|
||||
// permission gating, create through procurement.
|
||||
// Fallback: if catalogItem is gated/missing, use forecast creation so
|
||||
// callers without catalog permissions can still add products.
|
||||
const procurementInputs: Array<{
|
||||
index: number;
|
||||
payload: z.infer<typeof procurementCreateSchema>;
|
||||
}> = [];
|
||||
const forecastInputs: Array<{
|
||||
index: number;
|
||||
payload: Record<string, unknown>;
|
||||
customerDescription?: string;
|
||||
}> = [];
|
||||
|
||||
const created = await item.addProducts(forecastPayloads);
|
||||
for (const [index, g] of gatedItems.entries()) {
|
||||
const hasCatalogItem =
|
||||
typeof g?.catalogItem?.id === "number" && g.catalogItem.id > 0;
|
||||
|
||||
// If any items included customerDescription, patch the linked
|
||||
// procurement products after creation. This is best-effort since
|
||||
// newly created forecast items may not have a linked procurement
|
||||
// product yet.
|
||||
const procurementUpdates = created
|
||||
.map((product, idx) => ({
|
||||
product,
|
||||
customerDescription: customerDescriptions[idx],
|
||||
}))
|
||||
.filter((entry) => entry.customerDescription != null);
|
||||
if (!hasCatalogItem) {
|
||||
const { customerDescription, ...forecastPayload } = g as any;
|
||||
const normalizedForecastPayload: Record<string, unknown> = {
|
||||
...forecastPayload,
|
||||
};
|
||||
|
||||
if (procurementUpdates.length > 0) {
|
||||
await Promise.all(
|
||||
procurementUpdates.map(({ product, customerDescription }) =>
|
||||
item
|
||||
.updateProcurementProductByForecastItem(product.cwForecastId, {
|
||||
customerDescription,
|
||||
})
|
||||
.catch(() => null),
|
||||
),
|
||||
);
|
||||
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 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 (indexByForecastId.size > 0) {
|
||||
const createdForecastItems =
|
||||
(await opportunityCw.fetchProducts(item.cwOpportunityId)).forecastItems?.filter(
|
||||
(fi) => indexByForecastId.has(fi.id),
|
||||
) ?? [];
|
||||
|
||||
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) => ({
|
||||
product,
|
||||
customerDescription: forecastInputs[idx]?.customerDescription,
|
||||
}))
|
||||
.filter((entry) => entry.customerDescription != null);
|
||||
|
||||
if (procurementUpdates.length > 0) {
|
||||
await Promise.all(
|
||||
procurementUpdates.map(({ product, customerDescription }) =>
|
||||
item
|
||||
.updateProcurementProductByForecastItem(product.cwForecastId, {
|
||||
customerDescription,
|
||||
})
|
||||
.catch(() => null),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const created = inputItems
|
||||
.map((_, index) => createdByIndex.get(index))
|
||||
.filter(
|
||||
(entry): entry is ForecastProductController => entry !== undefined,
|
||||
);
|
||||
|
||||
const isBatch = Array.isArray(body);
|
||||
const response = apiResponse.created(
|
||||
isBatch
|
||||
? `${created.length} product(s) added to opportunity successfully!`
|
||||
: "Product added to opportunity successfully!",
|
||||
isBatch ? created.map((p) => p.toJson()) : created[0]!.toJson(),
|
||||
isBatch
|
||||
? created.map((p) => p.toJson())
|
||||
: created[0]?.toJson() ?? null,
|
||||
);
|
||||
return c.json(response, response.status as ContentfulStatusCode);
|
||||
},
|
||||
|
||||
@@ -8,7 +8,7 @@ import { z } from "zod";
|
||||
|
||||
const cancelProductSchema = z
|
||||
.object({
|
||||
quantityCancelled: z.number().int().min(0),
|
||||
quantityCancelled: z.number().min(0),
|
||||
cancellationReason: z.string().nullable().optional(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
@@ -4,6 +4,9 @@ import { apiResponse } from "../../../../../modules/api-utils/apiResponse";
|
||||
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||
import { authMiddleware } from "../../../../middleware/authorization";
|
||||
import GenericError from "../../../../../Errors/GenericError";
|
||||
import { ForecastProductController } from "../../../../../controllers/ForecastProductController";
|
||||
import { opportunityCw } from "../../../../../modules/cw-utils/opportunities/opportunities";
|
||||
import { prisma } from "../../../../../constants";
|
||||
import { z } from "zod";
|
||||
|
||||
const PRODUCT_NARRATIVE_FIELD_ID = 46;
|
||||
@@ -24,14 +27,14 @@ const updateProductSchema = z
|
||||
.refine(
|
||||
(value) =>
|
||||
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 = (
|
||||
fields: Array<Record<string, unknown>>,
|
||||
fieldId: number,
|
||||
caption: string,
|
||||
value: string,
|
||||
value: string
|
||||
) => {
|
||||
const next = [...fields];
|
||||
const idx = next.findIndex((f) => Number(f.id) === fieldId);
|
||||
@@ -76,12 +79,47 @@ export default createRoute(
|
||||
const input = updateProductSchema.parse(body);
|
||||
const opportunity = await opportunities.fetchRecord(identifier);
|
||||
|
||||
const forecastItems = await opportunity.fetchProducts();
|
||||
const forecastItem = forecastItems.find(
|
||||
(item) => item.cwForecastId === productId,
|
||||
const cwForecast = await opportunityCw.fetchProducts(
|
||||
opportunity.cwOpportunityId
|
||||
);
|
||||
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
|
||||
);
|
||||
|
||||
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 (!forecastItem) {
|
||||
if (!rawForecastItem) {
|
||||
throw new GenericError({
|
||||
status: 404,
|
||||
name: "ForecastItemNotFound",
|
||||
@@ -89,6 +127,7 @@ export default createRoute(
|
||||
});
|
||||
}
|
||||
|
||||
const forecastItem = new ForecastProductController(rawForecastItem);
|
||||
const forecastJson = forecastItem.toJson();
|
||||
const effectiveQuantity = input.quantity ?? forecastJson.quantity ?? 1;
|
||||
|
||||
@@ -101,12 +140,12 @@ export default createRoute(
|
||||
}
|
||||
if (input.unitPrice !== undefined) {
|
||||
forecastPatch.revenue = Number(
|
||||
(input.unitPrice * effectiveQuantity).toFixed(2),
|
||||
(input.unitPrice * effectiveQuantity).toFixed(2)
|
||||
);
|
||||
}
|
||||
if (input.unitCost !== undefined) {
|
||||
forecastPatch.cost = Number(
|
||||
(input.unitCost * effectiveQuantity).toFixed(2),
|
||||
(input.unitCost * effectiveQuantity).toFixed(2)
|
||||
);
|
||||
}
|
||||
if (input.taxableFlag !== undefined) {
|
||||
@@ -114,19 +153,22 @@ export default createRoute(
|
||||
}
|
||||
|
||||
const existingProcurement =
|
||||
await opportunity.fetchProcurementProductByForecastItem(productId);
|
||||
await opportunity.fetchProcurementProductByForecastItem(forecastItemId);
|
||||
|
||||
if (
|
||||
(input.productNarrative !== undefined ||
|
||||
input.procurementNotes !== undefined) &&
|
||||
!existingProcurement
|
||||
) {
|
||||
throw new GenericError({
|
||||
status: 400,
|
||||
name: "ProcurementLinkRequired",
|
||||
message:
|
||||
"Product Narrative and Procurement Notes can only be updated on products linked to a procurement record",
|
||||
});
|
||||
const hasNarrativeOrNotesValue =
|
||||
(input.productNarrative !== undefined &&
|
||||
input.productNarrative !== null) ||
|
||||
(input.procurementNotes !== undefined && input.procurementNotes !== null);
|
||||
|
||||
if (hasNarrativeOrNotesValue && !existingProcurement) {
|
||||
console.warn(
|
||||
"[ProductUpdate] Ignoring procurement-only narrative fields for non-linked product",
|
||||
{
|
||||
opportunity: identifier,
|
||||
productId: forecastItemId,
|
||||
requestedProductId: productId,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
let updatedProcurement = existingProcurement;
|
||||
@@ -164,7 +206,7 @@ export default createRoute(
|
||||
updatedFields,
|
||||
PROCUREMENT_NOTES_FIELD_ID,
|
||||
"Procurement Notes",
|
||||
input.procurementNotes,
|
||||
input.procurementNotes
|
||||
);
|
||||
}
|
||||
if (
|
||||
@@ -175,7 +217,7 @@ export default createRoute(
|
||||
updatedFields,
|
||||
PRODUCT_NARRATIVE_FIELD_ID,
|
||||
"Product Narrative",
|
||||
input.productNarrative,
|
||||
input.productNarrative
|
||||
);
|
||||
}
|
||||
if (
|
||||
@@ -190,28 +232,47 @@ export default createRoute(
|
||||
if (Object.keys(procurementPatch).length > 0) {
|
||||
updatedProcurement =
|
||||
await opportunity.updateProcurementProductByForecastItem(
|
||||
productId,
|
||||
procurementPatch,
|
||||
forecastItemId,
|
||||
procurementPatch
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let updatedForecast = forecastJson;
|
||||
if (Object.keys(forecastPatch).length > 0) {
|
||||
const patched = await opportunity.updateProduct(productId, forecastPatch);
|
||||
const patched = await opportunity.updateProduct(
|
||||
forecastItemId,
|
||||
forecastPatch
|
||||
);
|
||||
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)
|
||||
? updatedProcurement.customFields
|
||||
: [];
|
||||
const procurementNotes =
|
||||
updatedFields.find(
|
||||
(field: any) => field?.id === PROCUREMENT_NOTES_FIELD_ID,
|
||||
(field: any) => field?.id === PROCUREMENT_NOTES_FIELD_ID
|
||||
)?.value ?? null;
|
||||
const productNarrative =
|
||||
updatedFields.find(
|
||||
(field: any) => field?.id === PRODUCT_NARRATIVE_FIELD_ID,
|
||||
(field: any) => field?.id === PRODUCT_NARRATIVE_FIELD_ID
|
||||
)?.value ?? null;
|
||||
|
||||
const quantity =
|
||||
@@ -230,11 +291,13 @@ export default createRoute(
|
||||
quantity,
|
||||
unitPrice,
|
||||
unitCost,
|
||||
requestedProductId: productId,
|
||||
forecastItemId,
|
||||
procurementNotes,
|
||||
productNarrative,
|
||||
});
|
||||
|
||||
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",
|
||||
contentBase64: previewBase64,
|
||||
},
|
||||
}
|
||||
);
|
||||
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({
|
||||
name: z.string().min(1).optional(),
|
||||
notes: z.string().optional(),
|
||||
interest: z.enum(["HOT", "WARM", "COLD"]).nullable().optional(),
|
||||
rating: z.object({ id: z.number() }).optional(),
|
||||
type: z.object({ id: z.number() }).optional(),
|
||||
stage: z.object({ id: z.number() }).optional(),
|
||||
@@ -50,7 +51,7 @@ export default createRoute(
|
||||
|
||||
const response = apiResponse.successful(
|
||||
"Opportunity updated successfully!",
|
||||
updated.toJson(),
|
||||
updated.toJson()
|
||||
);
|
||||
|
||||
return c.json(response, response.status as ContentfulStatusCode);
|
||||
@@ -77,7 +78,7 @@ export default createRoute(
|
||||
errors: cwErrors,
|
||||
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"] })
|
||||
);
|
||||
|
||||
@@ -6,6 +6,7 @@ import { authMiddleware } from "../../middleware/authorization";
|
||||
import GenericError from "../../../Errors/GenericError";
|
||||
import { z } from "zod";
|
||||
import { cwMembers } from "../../../managers/cwMembers";
|
||||
import { prisma } from "../../../constants";
|
||||
import {
|
||||
createWorkflowActivity,
|
||||
OptimaType,
|
||||
@@ -18,6 +19,7 @@ const createSchema = z.object({
|
||||
.min(1)
|
||||
.transform((v) => new Date(v).toISOString().replace(/\.\d{3}Z$/, "Z")),
|
||||
notes: z.string().optional(),
|
||||
interest: z.enum(["HOT", "WARM", "COLD"]).nullable().optional(),
|
||||
rating: z.object({ id: z.number() }).optional(),
|
||||
type: z.object({ id: z.number() }).optional(),
|
||||
stage: z.object({ id: z.number() }).optional(),
|
||||
@@ -42,9 +44,18 @@ export default createRoute(
|
||||
async (c) => {
|
||||
const body = await c.req.json();
|
||||
const data = createSchema.parse(body);
|
||||
const { interest, ...cwCreateData } = data;
|
||||
|
||||
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
|
||||
try {
|
||||
@@ -69,14 +80,14 @@ export default createRoute(
|
||||
} catch (activityErr) {
|
||||
console.error(
|
||||
"[Opportunity Create] Failed to create workflow activity:",
|
||||
activityErr,
|
||||
activityErr
|
||||
);
|
||||
// Don't fail the opportunity creation if the activity fails
|
||||
}
|
||||
|
||||
const response = apiResponse.created(
|
||||
"Opportunity created successfully!",
|
||||
item.toJson(),
|
||||
item.toJson()
|
||||
);
|
||||
|
||||
return c.json(response, response.status as ContentfulStatusCode);
|
||||
@@ -103,7 +114,7 @@ export default createRoute(
|
||||
errors: cwErrors,
|
||||
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"] })
|
||||
);
|
||||
|
||||
@@ -2,12 +2,8 @@ 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 GenericError from "../../../Errors/GenericError";
|
||||
import {
|
||||
getSalesOpportunityMetricsAll,
|
||||
getSalesOpportunityMetricsForMember,
|
||||
refreshSalesOpportunityMetricsCache,
|
||||
} from "../../../modules/cache/salesOpportunityMetricsCache";
|
||||
import { getSalesOpportunityMetricsForMember, getSalesOpportunityMetricsAll } from "../../../modules/cache/salesOpportunityMetricsCache";
|
||||
import { prisma } from "../../../constants";
|
||||
|
||||
/* GET /v1/sales/opportunities/metrics */
|
||||
export default createRoute(
|
||||
@@ -15,60 +11,39 @@ export default createRoute(
|
||||
["/opportunities/metrics"],
|
||||
async (c) => {
|
||||
const user = c.get("user");
|
||||
const scope = (c.req.query("scope") ?? "me").toLowerCase();
|
||||
const requestedIdentifier = c.req.query("identifier")?.trim().toLowerCase();
|
||||
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();
|
||||
};
|
||||
const scope = c.req.query("scope");
|
||||
const identifierOverride = c.req.query("identifier");
|
||||
|
||||
// scope=all — return metrics for all active members (requires permission)
|
||||
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(
|
||||
"Sales opportunity metrics fetched successfully!",
|
||||
all,
|
||||
allMetrics,
|
||||
);
|
||||
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(
|
||||
"Sales opportunity metrics fetched successfully!",
|
||||
null,
|
||||
@@ -76,18 +51,10 @@ export default createRoute(
|
||||
return c.json(response, response.status as ContentfulStatusCode);
|
||||
}
|
||||
|
||||
let metrics = await getSalesOpportunityMetricsForMember(targetIdentifier);
|
||||
if (!metrics) {
|
||||
await refreshSalesOpportunityMetricsCache();
|
||||
metrics = await getSalesOpportunityMetricsForMember(targetIdentifier);
|
||||
}
|
||||
|
||||
const metrics = await getSalesOpportunityMetricsForMember(cwIdentifier);
|
||||
const response = apiResponse.successful(
|
||||
"Sales opportunity metrics fetched successfully!",
|
||||
{
|
||||
identifier: targetIdentifier,
|
||||
metrics,
|
||||
},
|
||||
metrics,
|
||||
);
|
||||
return c.json(response, response.status as ContentfulStatusCode);
|
||||
},
|
||||
|
||||
@@ -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"] }),
|
||||
);
|
||||
@@ -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"] }),
|
||||
);
|
||||
@@ -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"] }),
|
||||
);
|
||||
@@ -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
@@ -4,6 +4,18 @@ import { ZodError } from "zod";
|
||||
import { cors } from "hono/cors";
|
||||
import GenericError from "../Errors/GenericError";
|
||||
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 v1 = new Hono();
|
||||
@@ -24,7 +36,7 @@ app.onError((err, ctx) => {
|
||||
return ctx.json(
|
||||
apiResponse.zodError(err),
|
||||
//@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}`,
|
||||
status: 404,
|
||||
cause: "Unknown",
|
||||
}),
|
||||
})
|
||||
);
|
||||
return c.json(response, response.status);
|
||||
});
|
||||
|
||||
v1.route("/teapot", teapot);
|
||||
v1.route("/auth", require("./routers/authRouter").default);
|
||||
v1.route("/user", require("./routers/user").default);
|
||||
v1.route("/company", require("./routers/companyRouter").default);
|
||||
v1.route("/credential", require("./routers/credentialRouter").default);
|
||||
v1.route("/credential-type", require("./routers/credentialTypeRouter").default);
|
||||
v1.route("/role", require("./routers/roleRouter").default);
|
||||
v1.route("/permissions", require("./routers/permissionRouter").default);
|
||||
v1.route("/unifi", require("./routers/unifiRouter").default);
|
||||
v1.route("/procurement", require("./routers/procurementRouter").default);
|
||||
v1.route("/sales", require("./routers/salesRouter").default);
|
||||
v1.route("/cw", require("./routers/cwRouter").default);
|
||||
v1.route("/auth", authRouter);
|
||||
v1.route("/user", userRouter);
|
||||
v1.route("/company", companyRouter);
|
||||
v1.route("/credential", credentialRouter);
|
||||
v1.route("/credential-type", credentialTypeRouter);
|
||||
v1.route("/role", roleRouter);
|
||||
v1.route("/permissions", permissionRouter);
|
||||
v1.route("/unifi", unifiRouter);
|
||||
v1.route("/procurement", procurementRouter);
|
||||
v1.route("/sales", salesRouter);
|
||||
v1.route("/cw", cwRouter);
|
||||
v1.route("/schedule", scheduleRouter);
|
||||
app.route("/v1", v1);
|
||||
|
||||
export default app;
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Socket } from "socket.io";
|
||||
import { attachSocketEventPermissions } from "../middleware/authorization";
|
||||
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) => {
|
||||
attachSocketEventPermissions(socket, {
|
||||
@@ -15,7 +15,7 @@ export const registerLiveQuotePreviewHandlers = (socket: Socket) => {
|
||||
"opp:live_quote_preview",
|
||||
async (
|
||||
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 normalizedId =
|
||||
@@ -97,6 +97,6 @@ export const registerLiveQuotePreviewHandlers = (socket: Socket) => {
|
||||
id: normalizedId,
|
||||
event: dataEvent,
|
||||
});
|
||||
},
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
@@ -15,17 +15,15 @@ export default createRoute(
|
||||
|
||||
const processWlans = await Promise.all(
|
||||
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(
|
||||
"UniFi WiFi Networks Fetched Successfully!",
|
||||
processWlans,
|
||||
processWlans
|
||||
);
|
||||
return c.json(response, response.status as ContentfulStatusCode);
|
||||
},
|
||||
authMiddleware({ permissions: ["unifi.access", "unifi.site.wifi"] }),
|
||||
authMiddleware({ permissions: ["unifi.access", "unifi.site.wifi"] })
|
||||
);
|
||||
|
||||
@@ -7,6 +7,8 @@ import { authMiddleware } from "../../middleware/authorization";
|
||||
const updateSchema = z
|
||||
.object({
|
||||
name: z.string().optional(),
|
||||
firstName: z.string().nullable().optional(),
|
||||
lastName: z.string().nullable().optional(),
|
||||
image: z.string().optional(),
|
||||
})
|
||||
.strict();
|
||||
@@ -19,10 +21,10 @@ export default createRoute(
|
||||
const updatedUser = await c.get("user")?.update(body);
|
||||
const response = apiResponse.successful(
|
||||
"Successfully updated user.",
|
||||
updatedUser?.toJson(),
|
||||
updatedUser?.toJson()
|
||||
);
|
||||
|
||||
return c.json(response, response.status as ContentfulStatusCode);
|
||||
},
|
||||
authMiddleware({ scopes: ["user.write"] }),
|
||||
authMiddleware({ scopes: ["user.write"] })
|
||||
);
|
||||
|
||||
@@ -9,6 +9,8 @@ import GenericError from "../../Errors/GenericError";
|
||||
const updateSchema = z
|
||||
.object({
|
||||
name: z.string().optional(),
|
||||
firstName: z.string().nullable().optional(),
|
||||
lastName: z.string().nullable().optional(),
|
||||
image: z.string().optional(),
|
||||
roles: z.array(z.string()).optional(),
|
||||
permissions: z.array(z.string()).optional(),
|
||||
@@ -60,9 +62,9 @@ export default createRoute(
|
||||
|
||||
const response = apiResponse.successful(
|
||||
"User Updated Successfully!",
|
||||
user.toJson(),
|
||||
user.toJson()
|
||||
);
|
||||
return c.json(response, response.status as ContentfulStatusCode);
|
||||
},
|
||||
authMiddleware({ permissions: ["user.write.other"] }),
|
||||
authMiddleware({ permissions: ["user.write.other"] })
|
||||
);
|
||||
|
||||
@@ -4,7 +4,6 @@ import { Prisma, PrismaClient } from "../generated/prisma/client";
|
||||
import * as msal from "@azure/msal-node";
|
||||
import { Server } from "socket.io";
|
||||
import { Server as Engine } from "@socket.io/bun-engine";
|
||||
import { io as createSocketClient } from "socket.io-client";
|
||||
import axios from "axios";
|
||||
import { UnifiClient } from "./modules/unifi-api/UnifiClient";
|
||||
import { attachCwApiLogger } from "./modules/cw-utils/cwApiLogger";
|
||||
@@ -13,18 +12,11 @@ import Redis from "ioredis";
|
||||
const connectionString = `${process.env.DATABASE_URL}`;
|
||||
const adapter = new PrismaPg({ connectionString });
|
||||
|
||||
interface EnvKey {
|
||||
PORT: number;
|
||||
}
|
||||
|
||||
// ENV CONSTANTS
|
||||
|
||||
export const PORT = process.env.PORT;
|
||||
export const API_BASE_URL =
|
||||
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 });
|
||||
|
||||
@@ -77,23 +69,6 @@ const engine = new Engine();
|
||||
io.bind(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
|
||||
|
||||
const connectWiseApi = axios.create({
|
||||
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
CWCreateActivity,
|
||||
} from "../modules/cw-utils/activities/activity.types";
|
||||
import { activityCw } from "../modules/cw-utils/activities/activities";
|
||||
import { fetchActivity } from "../modules/cw-utils/activities/fetchActivity";
|
||||
|
||||
/**
|
||||
* Activity Controller
|
||||
@@ -127,26 +126,6 @@ export class ActivityController {
|
||||
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
|
||||
*
|
||||
|
||||
@@ -1,9 +1,27 @@
|
||||
import { CatalogItem } from "../../generated/prisma/client";
|
||||
import {
|
||||
CatalogItem,
|
||||
CatalogCategory,
|
||||
CatalogSubcategory,
|
||||
CatalogManufacturer,
|
||||
} from "../../generated/prisma/client";
|
||||
import { prisma } from "../constants";
|
||||
import { catalogCw } from "../modules/cw-utils/procurement/catalog";
|
||||
import { CatalogItem as CWCatalogItem } from "../modules/cw-utils/procurement/catalog.types";
|
||||
import GenericError from "../Errors/GenericError";
|
||||
|
||||
/**
|
||||
* 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
|
||||
*
|
||||
@@ -12,13 +30,15 @@ import GenericError from "../Errors/GenericError";
|
||||
* the internal database representation with ConnectWise catalog data.
|
||||
*/
|
||||
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 name: string;
|
||||
public description: string | null;
|
||||
public customerDescription: string | null;
|
||||
public internalNotes: string | null;
|
||||
|
||||
public readonly cwCatalogId: number;
|
||||
public readonly identifier: string | null;
|
||||
|
||||
public category: string | null;
|
||||
@@ -48,24 +68,29 @@ export class CatalogItemController {
|
||||
public readonly createdAt: Date;
|
||||
public updatedAt: Date;
|
||||
|
||||
constructor(
|
||||
itemData: CatalogItem & {
|
||||
linkedItems?: CatalogItem[];
|
||||
},
|
||||
) {
|
||||
this.id = itemData.id;
|
||||
constructor(itemData: CatalogItemWithRelations) {
|
||||
// `id` (Int @unique) is the ConnectWise catalog record ID.
|
||||
// `uid` (String @id) is the Prisma primary key.
|
||||
this.cwCatalogId = itemData.id;
|
||||
this.id = itemData.uid;
|
||||
this.name = itemData.name;
|
||||
this.description = itemData.description;
|
||||
this.customerDescription = itemData.customerDescription;
|
||||
this.internalNotes = itemData.internalNotes;
|
||||
this.cwCatalogId = itemData.cwCatalogId;
|
||||
this.identifier = itemData.identifier;
|
||||
this.category = itemData.category;
|
||||
this.categoryCwId = itemData.categoryCwId;
|
||||
this.subcategory = itemData.subcategory;
|
||||
this.subcategoryCwId = itemData.subcategoryCwId;
|
||||
this.manufacturer = itemData.manufacturer;
|
||||
this.manufactureCwId = itemData.manufactureCwId;
|
||||
|
||||
// Extract relation data into flat fields
|
||||
const sub = itemData.subcategory;
|
||||
const cat = sub?.category;
|
||||
const mfr = itemData.manufacturer;
|
||||
|
||||
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.vendorName = itemData.vendorName;
|
||||
this.vendorSku = itemData.vendorSku;
|
||||
@@ -97,7 +122,7 @@ export class CatalogItemController {
|
||||
|
||||
if (onHand !== this.onHand) {
|
||||
await prisma.catalogItem.update({
|
||||
where: { id: this.id },
|
||||
where: { uid: this.id },
|
||||
data: { onHand },
|
||||
});
|
||||
this.onHand = onHand;
|
||||
@@ -137,7 +162,7 @@ export class CatalogItemController {
|
||||
}
|
||||
|
||||
const target = await prisma.catalogItem.findFirst({
|
||||
where: { id: targetId },
|
||||
where: { uid: targetId },
|
||||
});
|
||||
|
||||
if (!target) {
|
||||
@@ -150,9 +175,9 @@ export class CatalogItemController {
|
||||
}
|
||||
|
||||
const updated = await prisma.catalogItem.update({
|
||||
where: { id: this.id },
|
||||
where: { uid: this.id },
|
||||
data: {
|
||||
linkedItems: { connect: { id: targetId } },
|
||||
linkedItems: { connect: { uid: targetId } },
|
||||
},
|
||||
include: { linkedItems: true },
|
||||
});
|
||||
@@ -174,9 +199,9 @@ export class CatalogItemController {
|
||||
*/
|
||||
public async unlinkItem(targetId: string): Promise<CatalogItemController> {
|
||||
const updated = await prisma.catalogItem.update({
|
||||
where: { id: this.id },
|
||||
where: { uid: this.id },
|
||||
data: {
|
||||
linkedItems: { disconnect: { id: targetId } },
|
||||
linkedItems: { disconnect: { uid: targetId } },
|
||||
},
|
||||
include: { linkedItems: true },
|
||||
});
|
||||
|
||||
@@ -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 {
|
||||
fetchCompanySites,
|
||||
fetchCompanySite,
|
||||
serializeCwSite,
|
||||
} from "../modules/cw-utils/sites/companySites";
|
||||
import { Company as CWCompany, Contact } from "../types/ConnectWiseTypes";
|
||||
Company,
|
||||
CompanyAddress,
|
||||
Contact,
|
||||
} from "../../generated/prisma/client";
|
||||
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
|
||||
*
|
||||
* This class is for creating a controller that can manage company data,
|
||||
* synchronize with external systems, and provide methods for accessing
|
||||
* and updating company information within our internal system.
|
||||
* This class manages company data from the local database.
|
||||
* Data is synced from ConnectWise via the dalpuri service.
|
||||
*/
|
||||
export class CompanyController {
|
||||
public readonly id: string;
|
||||
public readonly id: number;
|
||||
public readonly uid: string;
|
||||
public name: string;
|
||||
public readonly cw_Identifier: string;
|
||||
public readonly cw_CompanyId: number;
|
||||
public cw_Data?: {
|
||||
company: CWCompany;
|
||||
defaultContact: Contact | null;
|
||||
allContacts: Contact[];
|
||||
};
|
||||
public phone: string | null;
|
||||
public website: string | null;
|
||||
|
||||
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.uid = companyData.uid;
|
||||
this.name = companyData.name;
|
||||
this.cw_Identifier = companyData.cw_Identifier;
|
||||
this.cw_CompanyId = companyData.cw_CompanyId;
|
||||
this.cw_Data = cwData;
|
||||
this.phone = companyData.phone;
|
||||
this.website = companyData.website;
|
||||
|
||||
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
|
||||
* (company, default contact, all contacts) if not already loaded.
|
||||
*
|
||||
* @returns {ThisType}
|
||||
* Loads contacts and addresses from the local database if not already loaded.
|
||||
*/
|
||||
public async hydrateCwData() {
|
||||
if (this.cw_Data) return this;
|
||||
public async hydrateData() {
|
||||
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 (!cwCompany) return this;
|
||||
|
||||
const allContactsData = await connectWiseApi.get(
|
||||
`${cwCompany._info.contacts_href}&pageSize=1000`,
|
||||
);
|
||||
|
||||
// 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,
|
||||
};
|
||||
if (this._addresses.length === 0) {
|
||||
this._addresses = await prisma.companyAddress.findMany({
|
||||
where: { companyId: this.id },
|
||||
});
|
||||
this._defaultAddress = this._addresses.find((a) => a.defaultFlag) ?? null;
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh Internal Company Data from ConnectWise
|
||||
* Refresh from DB
|
||||
*
|
||||
* This method fetches the latest company data from ConnectWise and updates
|
||||
* the internal company information accordingly.
|
||||
*
|
||||
* @returns {ThisType} - Updated Controller
|
||||
* Reloads the company data from the local database.
|
||||
*/
|
||||
public async refreshFromCW() {
|
||||
const data = await updateCwInternalCompany(this.cw_CompanyId);
|
||||
public async refreshFromDb() {
|
||||
const data = await prisma.company.findUnique({
|
||||
where: { id: this.id },
|
||||
});
|
||||
|
||||
if (data) {
|
||||
this.name = data.name;
|
||||
this.phone = data.phone;
|
||||
this.website = data.website;
|
||||
}
|
||||
|
||||
this.name = data?.name || this.name;
|
||||
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
|
||||
*
|
||||
* This method retrieves the configurations associated with
|
||||
* the company from ConnectWise.
|
||||
*
|
||||
* @returns {ProcessedConfiguration}
|
||||
* Fetches configurations directly from ConnectWise.
|
||||
*/
|
||||
public async fetchConfigurations() {
|
||||
const data = await fetchCompanyConfigurations(this.cw_CompanyId);
|
||||
return data;
|
||||
const pageSize = 1000;
|
||||
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
|
||||
*
|
||||
* Retrieves all sites for this company from ConnectWise
|
||||
* and returns them as serialized site objects.
|
||||
* Retrieves all sites (addresses) for this company from local DB.
|
||||
*/
|
||||
public async fetchSites() {
|
||||
const sites = await fetchCompanySites(this.cw_CompanyId);
|
||||
return sites.map(serializeCwSite);
|
||||
const sites = await prisma.companyAddress.findMany({
|
||||
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
|
||||
*
|
||||
* Retrieves a single site by its ConnectWise site ID
|
||||
* and returns a serialized site object.
|
||||
*
|
||||
* @param cwSiteId - The ConnectWise site ID
|
||||
* Retrieves a single site by its ID from local DB.
|
||||
*/
|
||||
public async fetchSite(cwSiteId: number) {
|
||||
const site = await fetchCompanySite(this.cw_CompanyId, cwSiteId);
|
||||
return serializeCwSite(site);
|
||||
public async fetchSite(siteId: number) {
|
||||
const site = await prisma.companyAddress.findFirst({
|
||||
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?: {
|
||||
includeAddress: boolean;
|
||||
includePrimaryContact: boolean;
|
||||
includeAddress?: boolean;
|
||||
includePrimaryContact?: boolean;
|
||||
includeAllContacts?: boolean;
|
||||
}) {
|
||||
const cw_Data: Record<string, unknown> = {};
|
||||
|
||||
if (opts?.includeAddress) {
|
||||
cw_Data.address = this._defaultAddress
|
||||
? {
|
||||
line1: this._defaultAddress.addressLine1,
|
||||
line2: this._defaultAddress.addressLine2,
|
||||
city: this._defaultAddress.city,
|
||||
state: this._defaultAddress.state,
|
||||
zip: this._defaultAddress.zipCode,
|
||||
country: this._defaultAddress.country ?? "US",
|
||||
}
|
||||
: null;
|
||||
}
|
||||
|
||||
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,
|
||||
lastName: contact.lastName,
|
||||
cwId: contact.id,
|
||||
inactive: !contact.active,
|
||||
title: contact.title,
|
||||
phone: contact.phone,
|
||||
email: contact.email,
|
||||
}));
|
||||
}
|
||||
|
||||
return {
|
||||
id: this.id,
|
||||
id: this.uid,
|
||||
name: this.name,
|
||||
cw_Identifier: this.cw_Identifier,
|
||||
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,
|
||||
lastName: this.cw_Data.defaultContact.lastName,
|
||||
cwId: this.cw_Data.defaultContact.id,
|
||||
inactive: this.cw_Data.defaultContact.inactiveFlag,
|
||||
title: this.cw_Data.defaultContact.title,
|
||||
phone: this.cw_Data.defaultContact.defaultPhoneNbr,
|
||||
email: (() => {
|
||||
if (!this.cw_Data?.defaultContact?.communicationItems)
|
||||
return null;
|
||||
return (
|
||||
this.cw_Data.defaultContact.communicationItems.find(
|
||||
(v) => v.type.name === "Email",
|
||||
)?.value ?? null
|
||||
);
|
||||
})(),
|
||||
}
|
||||
: null,
|
||||
allContacts: !opts?.includeAllContacts
|
||||
? undefined
|
||||
: this.cw_Data?.allContacts.map((contact) => ({
|
||||
firstName: contact.firstName,
|
||||
lastName: contact.lastName,
|
||||
cwId: contact.id,
|
||||
inactive: contact.inactiveFlag,
|
||||
title: contact.title,
|
||||
phone: contact.defaultPhoneNbr,
|
||||
email: (() => {
|
||||
if (!contact.communicationItems) return null;
|
||||
return (
|
||||
contact.communicationItems.find(
|
||||
(v) => v.type.name === "Email",
|
||||
)?.value ?? null
|
||||
);
|
||||
})(),
|
||||
})),
|
||||
},
|
||||
cw_CompanyId: this.id,
|
||||
cw_Data,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { CwMember } from "../../generated/prisma/client";
|
||||
import type { CWMember } from "../modules/cw-utils/members/fetchAllMembers";
|
||||
|
||||
/**
|
||||
* CW Member Controller
|
||||
@@ -44,24 +43,6 @@ export class CwMemberController {
|
||||
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
|
||||
*
|
||||
|
||||
@@ -8,6 +8,9 @@ import { CWForecastItem } from "../modules/cw-utils/opportunities/opportunity.ty
|
||||
* locally — all data is sourced directly from the ConnectWise API.
|
||||
*/
|
||||
export class ForecastProductController {
|
||||
private static readonly PROCUREMENT_NOTES_FIELD_ID = 29;
|
||||
private static readonly PRODUCT_NARRATIVE_FIELD_ID = 46;
|
||||
|
||||
public readonly cwForecastId: number;
|
||||
public forecastDescription: string;
|
||||
|
||||
@@ -24,6 +27,8 @@ export class ForecastProductController {
|
||||
|
||||
public productDescription: string;
|
||||
public customerDescription: string | null;
|
||||
public description: string | null;
|
||||
public procurementNotes: string | null;
|
||||
public productNarrative: string | null;
|
||||
public productClass: string;
|
||||
public forecastType: string;
|
||||
@@ -49,6 +54,11 @@ export class ForecastProductController {
|
||||
public cwLastUpdated: Date | 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)
|
||||
public cancelledFlag: boolean;
|
||||
public quantityCancelled: number;
|
||||
@@ -77,8 +87,23 @@ export class ForecastProductController {
|
||||
|
||||
this.productDescription = data.productDescription;
|
||||
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 =
|
||||
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.forecastType = data.forecastType;
|
||||
|
||||
@@ -105,6 +130,11 @@ export class ForecastProductController {
|
||||
: 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()
|
||||
this.cancelledFlag = false;
|
||||
this.quantityCancelled = 0;
|
||||
@@ -133,8 +163,19 @@ export class ForecastProductController {
|
||||
public applyProcurementCustomFields(data: {
|
||||
customFields?: Array<{ id: number; value?: unknown }>;
|
||||
}): 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
|
||||
?.find((f) => f.id === 46)
|
||||
?.find(
|
||||
(f) => f.id === ForecastProductController.PRODUCT_NARRATIVE_FIELD_ID
|
||||
)
|
||||
?.value?.toString();
|
||||
if (narrative) {
|
||||
this.productNarrative = narrative;
|
||||
@@ -257,6 +298,7 @@ export class ForecastProductController {
|
||||
: null,
|
||||
productDescription: this.productDescription,
|
||||
customerDescription: this.customerDescription,
|
||||
procurementNotes: this.procurementNotes,
|
||||
productNarrative: this.productNarrative,
|
||||
productClass: this.productClass,
|
||||
forecastType: this.forecastType,
|
||||
@@ -281,6 +323,25 @@ export class ForecastProductController {
|
||||
cwUpdatedBy: this.cwUpdatedBy,
|
||||
onHand: this.onHand,
|
||||
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 & {
|
||||
opportunity?: Opportunity | null;
|
||||
createdBy?: (User & { roles: Role[] }) | null;
|
||||
},
|
||||
}
|
||||
) {
|
||||
this.id = data.id;
|
||||
|
||||
@@ -67,7 +67,7 @@ export class GeneratedQuoteController {
|
||||
if (this._opportunity) return this._opportunity;
|
||||
|
||||
const opportunity = await prisma.opportunity.findFirst({
|
||||
where: { id: this.opportunityId },
|
||||
where: { uid: this.opportunityId },
|
||||
});
|
||||
|
||||
if (!opportunity) return null;
|
||||
@@ -114,8 +114,8 @@ export class GeneratedQuoteController {
|
||||
quoteFile: !opts?.includeFile
|
||||
? undefined
|
||||
: opts?.encodeFileAsBase64
|
||||
? Buffer.from(this.quoteFile).toString("base64")
|
||||
: this.quoteFile,
|
||||
? Buffer.from(this.quoteFile).toString("base64")
|
||||
: this.quoteFile,
|
||||
opportunity:
|
||||
opts?.includeOpportunity && this._opportunity
|
||||
? this._opportunity.toJson()
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -89,7 +89,7 @@ export class RoleController {
|
||||
});
|
||||
throw new PermissionsVerificationError(
|
||||
`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;
|
||||
moniker: string;
|
||||
permissions: string[];
|
||||
}>,
|
||||
}>
|
||||
) {
|
||||
const schema = z
|
||||
.object({
|
||||
title: z.string().min(1, "Title cannot be empty."),
|
||||
moniker: z.string().min(1, "Moniker cannot be empty."),
|
||||
permissions: z.array(
|
||||
z.string().min(1, "Permission node cannot be empty"),
|
||||
z.string().min(1, "Permission node cannot be empty")
|
||||
),
|
||||
})
|
||||
.partial()
|
||||
@@ -284,7 +284,7 @@ export class RoleController {
|
||||
if (checkMoniker && checkMoniker.moniker !== this.moniker)
|
||||
throw new RoleError(
|
||||
"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
|
||||
? this._users.map((v) => ({
|
||||
id: v.id,
|
||||
name: v.name,
|
||||
name:
|
||||
`${v.firstName ?? ""} ${v.lastName ?? ""}`.trim() ||
|
||||
v.login ||
|
||||
v.email,
|
||||
login: v.login,
|
||||
roles: v.roles.map((r: any) => r.id),
|
||||
}))
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Collection } from "@discordjs/collection";
|
||||
import { Role } from "../../generated/prisma/client";
|
||||
import { User } from "../../generated/prisma/browser";
|
||||
import { Role, User } from "../../generated/prisma/client";
|
||||
import { SessionTokensObject } from "./SessionController";
|
||||
import { sessions } from "../managers/sessions";
|
||||
import BodyError from "../Errors/BodyError";
|
||||
@@ -15,11 +14,13 @@ import { permissionsPrivateKey } from "../constants";
|
||||
|
||||
export default class UserController {
|
||||
public id: string;
|
||||
public name: string | null;
|
||||
public firstName: string | null;
|
||||
public lastName: string | null;
|
||||
public login: string;
|
||||
public email: string;
|
||||
public image: string | null;
|
||||
public cwIdentifier: string | null;
|
||||
public cwMemberId: number | null;
|
||||
|
||||
private _roles: Collection<string, Role>;
|
||||
private _permissions: string | null;
|
||||
@@ -33,13 +34,38 @@ export default class UserController {
|
||||
|
||||
public createdAt: 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[] }) {
|
||||
this.id = userdata.id;
|
||||
this.name = userdata.name;
|
||||
this.firstName = userdata.firstName ?? null;
|
||||
this.lastName = userdata.lastName ?? null;
|
||||
this.login = userdata.login;
|
||||
this.email = userdata.email;
|
||||
this.image = userdata.image;
|
||||
this.cwIdentifier = userdata.cwIdentifier ?? null;
|
||||
this.cwMemberId = userdata.cwMemberId ?? null;
|
||||
this.updatedAt = userdata.updatedAt;
|
||||
this.createdAt = userdata.createdAt;
|
||||
this._permissions = userdata.permissions ?? null;
|
||||
@@ -62,11 +88,13 @@ export default class UserController {
|
||||
*/
|
||||
private _updateInternalValues(userdata: User) {
|
||||
this.id = userdata.id;
|
||||
this.name = userdata.name;
|
||||
this.firstName = userdata.firstName ?? null;
|
||||
this.lastName = userdata.lastName ?? null;
|
||||
this.login = userdata.login;
|
||||
this.email = userdata.email;
|
||||
this.image = userdata.image;
|
||||
this.cwIdentifier = userdata.cwIdentifier ?? null;
|
||||
this.cwMemberId = userdata.cwMemberId ?? null;
|
||||
this.updatedAt = userdata.updatedAt;
|
||||
this.createdAt = userdata.createdAt;
|
||||
}
|
||||
@@ -92,17 +120,33 @@ export default class UserController {
|
||||
* @param data - A partial of the user data
|
||||
* @returns {Promise<UserController>} - The updated user controller
|
||||
*/
|
||||
public async update(data: Partial<Pick<User, "name" | "image">>) {
|
||||
if (Object.keys(data).length == 0)
|
||||
public async update(
|
||||
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.");
|
||||
|
||||
const updatedUser = await prisma.user.update({
|
||||
where: { id: this.id },
|
||||
data,
|
||||
data: updateData,
|
||||
});
|
||||
|
||||
this._updateInternalValues(updatedUser);
|
||||
events.emit("user:updated", { user: this, updatedValues: data });
|
||||
events.emit("user:updated", { user: this, updatedValues: updateData });
|
||||
|
||||
return this;
|
||||
}
|
||||
@@ -118,7 +162,7 @@ export default class UserController {
|
||||
*/
|
||||
public async setRoles(roleIdentifiers: string[]): Promise<UserController> {
|
||||
const resolvedRoles = await Promise.all(
|
||||
roleIdentifiers.map((identifier) => roles.fetch(identifier)),
|
||||
roleIdentifiers.map((identifier) => roles.fetch(identifier))
|
||||
);
|
||||
|
||||
const updatedUser = await prisma.user.update({
|
||||
@@ -242,8 +286,8 @@ export default class UserController {
|
||||
|
||||
await Promise.all(
|
||||
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;
|
||||
@@ -307,11 +351,13 @@ export default class UserController {
|
||||
return {
|
||||
id: this.id,
|
||||
name: this.name,
|
||||
firstName: this.firstName,
|
||||
lastName: this.lastName,
|
||||
roles: opts?.safeReturn
|
||||
? undefined
|
||||
: this._roles.size > 0
|
||||
? this._roles.map((v) => v.moniker)
|
||||
: undefined,
|
||||
? this._roles.map((v) => v.moniker)
|
||||
: undefined,
|
||||
permissions: opts?.safeReturn
|
||||
? undefined
|
||||
: (() => {
|
||||
@@ -325,6 +371,7 @@ export default class UserController {
|
||||
login: opts?.safeReturn ? undefined : this.login,
|
||||
email: opts?.safeReturn ? undefined : this.email,
|
||||
cwIdentifier: opts?.safeReturn ? undefined : this.cwIdentifier,
|
||||
cwMemberId: this.cwMemberId,
|
||||
image: this.image,
|
||||
createdAt: this.createdAt,
|
||||
updatedAt: this.updatedAt,
|
||||
|
||||
+52
-168
@@ -1,61 +1,32 @@
|
||||
import { refresh } from "./api/auth";
|
||||
import app from "./api/server";
|
||||
import { setupSockets } from "./api/sockets";
|
||||
import {
|
||||
COLLECTOR_WS_URL,
|
||||
connectCollectorSocket,
|
||||
collectorSocket,
|
||||
engine,
|
||||
PORT,
|
||||
prisma,
|
||||
unifi,
|
||||
unifiPassword,
|
||||
unifiUsername,
|
||||
} from "./constants";
|
||||
import { engine, PORT, prisma } from "./constants";
|
||||
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 { setupEventDebugger } from "./modules/logging/eventDebugger";
|
||||
import { signPermissions } from "./modules/permission-utils/signPermissions";
|
||||
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 {
|
||||
enqueueActiveOpportunityRefreshJob,
|
||||
startOpportunityCacheWorkers,
|
||||
} from "./workert";
|
||||
import cuid from "cuid";
|
||||
|
||||
const startupArgs = new Set(Bun.argv.slice(2));
|
||||
const simpleTerminalMode =
|
||||
startupArgs.has("-st") || startupArgs.has("--simple-terminal");
|
||||
|
||||
// Setup global event debugger in non-production environments
|
||||
if (Bun.env.NODE_ENV == "development") {
|
||||
if (Bun.env.NODE_ENV == "development" && !simpleTerminalMode) {
|
||||
setupEventDebugger({ processLabel: "API" });
|
||||
}
|
||||
|
||||
/** Concise error message for interval logs — avoids dumping full Axios error objects. */
|
||||
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.
|
||||
// Helper to run a startup task safely — failures are logged but never crash the process.
|
||||
const safeStartup = async (label: string, fn: () => Promise<void>) => {
|
||||
try {
|
||||
await fn();
|
||||
} catch (err) {
|
||||
console.error(
|
||||
`[startup] ${label} failed — will retry on next interval`,
|
||||
err,
|
||||
);
|
||||
console.error(`[startup] ${label} failed`, err);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -83,33 +54,30 @@ console.log(`[startup] Server listening on port ${PORT}`);
|
||||
setupSockets();
|
||||
console.log("[startup] Socket namespaces initialized");
|
||||
|
||||
collectorSocket.on("connect", () => {
|
||||
console.log(
|
||||
`[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");
|
||||
// Initialize worker system (PgBoss connection)
|
||||
await safeStartup("initializeWorkerSystem", () => initializeWorkerSystem());
|
||||
|
||||
// Start the inter-process comms server so the worker can connect on :8671
|
||||
startCommsServer();
|
||||
console.log("[startup] Worker comms server initialized");
|
||||
console.log("[startup] Comms server listening on :8671");
|
||||
|
||||
const embeddedWorkersEnabled = Bun.env.START_EMBEDDED_WORKERS === "true";
|
||||
if (embeddedWorkersEnabled) {
|
||||
await safeStartup(
|
||||
"startOpportunityCacheWorkers",
|
||||
startOpportunityCacheWorkers,
|
||||
// Enqueue a full dalpuri sync on startup
|
||||
await safeStartup("enqueueDalpuriFullSync", async () => {
|
||||
const jobId = await getBoss().send(WorkerQueue.DALPURI_FULL_SYNC, {}, { singletonKey: `startup-${Date.now()}` });
|
||||
if (jobId) {
|
||||
console.log(`[startup] Dalpuri full sync enqueued: ${jobId}`);
|
||||
} else {
|
||||
console.warn("[startup] Dalpuri full sync send returned null — job may already be pending or PgBoss not ready");
|
||||
}
|
||||
});
|
||||
|
||||
// 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}`)
|
||||
);
|
||||
} else {
|
||||
console.log(
|
||||
"[startup] Embedded opportunity workers disabled on API node (set START_EMBEDDED_WORKERS=true to override)",
|
||||
);
|
||||
}
|
||||
}, 5_000);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Background initialisation — none of this blocks the server.
|
||||
@@ -141,114 +109,30 @@ await safeStartup("ensureAdminRole", async () => {
|
||||
}
|
||||
});
|
||||
|
||||
// Refresh the internal list of companies every minute
|
||||
await safeStartup("refreshCompanies", refreshCompanies);
|
||||
setInterval(() => {
|
||||
return refreshCompanies().catch((err) =>
|
||||
console.error(`[interval] refreshCompanies failed: ${briefErr(err)}`),
|
||||
// Enqueue an initial cold-load metrics refresh on startup
|
||||
await safeStartup("enqueueSalesMetricsRefresh", async () => {
|
||||
const jobId = await getBoss().send(
|
||||
WorkerQueue.REFRESH_SALES_METRICS,
|
||||
{ forceColdLoad: true },
|
||||
{ singletonKey: `startup-metrics-${Date.now()}` }
|
||||
);
|
||||
}, 60 * 1000);
|
||||
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");
|
||||
}
|
||||
});
|
||||
|
||||
// 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);
|
||||
// Enqueue a metrics refresh every 5 minutes
|
||||
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) =>
|
||||
console.error(`[interval] refreshUDFs failed: ${briefErr(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)}`,
|
||||
),
|
||||
getBoss()
|
||||
.send(WorkerQueue.REFRESH_SALES_METRICS, {}, { singletonKey: "metrics-interval" })
|
||||
.catch((err) =>
|
||||
console.error(`[interval] REFRESH_SALES_METRICS enqueue failed: ${err?.message ?? 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 unifiSites.syncSites();
|
||||
});
|
||||
@@ -256,6 +140,6 @@ setInterval(() => {
|
||||
return unifiSites
|
||||
.syncSites()
|
||||
.catch((err) =>
|
||||
console.error(`[interval] syncSites failed: ${briefErr(err)}`),
|
||||
console.error(`[interval] syncSites failed: ${err?.message ?? err}`)
|
||||
);
|
||||
}, 60 * 1000);
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { ActivityController } from "../controllers/ActivityController";
|
||||
import { connectWiseApi } from "../constants";
|
||||
import GenericError from "../Errors/GenericError";
|
||||
import { activityCw } from "../modules/cw-utils/activities/activities";
|
||||
import {
|
||||
@@ -18,28 +17,20 @@ export const activities = {
|
||||
* @returns {Promise<ActivityController>}
|
||||
*/
|
||||
async fetchItem(cwActivityId: number): Promise<ActivityController> {
|
||||
try {
|
||||
const cwData = await activityCw.fetch(cwActivityId);
|
||||
return new ActivityController(cwData);
|
||||
} catch (error) {
|
||||
const errBody = (error as any).response?.data || error;
|
||||
throw new GenericError({
|
||||
name: "FetchActivityError",
|
||||
message: `Failed to fetch activity ${cwActivityId}`,
|
||||
cause: typeof errBody === "string" ? errBody : JSON.stringify(errBody),
|
||||
status: (error as any).status ?? 502,
|
||||
});
|
||||
}
|
||||
// TODO: Query local Activity table when synced by dalpuri
|
||||
throw new GenericError({
|
||||
name: "NotAvailable",
|
||||
message: "Activity fetch from local DB not yet implemented",
|
||||
status: 501,
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Fetch All Activities (Paginated)
|
||||
*
|
||||
* Fetches activities from ConnectWise with optional conditions and pagination.
|
||||
*
|
||||
* @param page - Page number (1-based)
|
||||
* @param rpp - Records per page
|
||||
* @param conditions - Optional CW conditions string for filtering
|
||||
* @param conditions - Optional conditions string for filtering
|
||||
* @returns {Promise<ActivityController[]>}
|
||||
*/
|
||||
async fetchPages(
|
||||
@@ -47,73 +38,32 @@ export const activities = {
|
||||
rpp: number,
|
||||
conditions?: string,
|
||||
): Promise<ActivityController[]> {
|
||||
try {
|
||||
const pageNum = Math.max(page, 1);
|
||||
const conditionsParam = conditions
|
||||
? `&conditions=${encodeURIComponent(conditions)}`
|
||||
: "";
|
||||
const response = await connectWiseApi.get(
|
||||
`/sales/activities?page=${pageNum}&pageSize=${rpp}${conditionsParam}`,
|
||||
);
|
||||
const items = response.data;
|
||||
return items.map((item: any) => new ActivityController(item));
|
||||
} catch (error) {
|
||||
const errBody = (error as any).response?.data || error;
|
||||
throw new GenericError({
|
||||
name: "FetchActivitiesError",
|
||||
message: "Failed to fetch activities from ConnectWise",
|
||||
cause: typeof errBody === "string" ? errBody : JSON.stringify(errBody),
|
||||
status: 502,
|
||||
});
|
||||
}
|
||||
// TODO: Query local Activity table when synced by dalpuri
|
||||
return [];
|
||||
},
|
||||
|
||||
/**
|
||||
* Fetch Activities by Company
|
||||
*
|
||||
* Fetches all activities for a company by its ConnectWise company ID.
|
||||
*
|
||||
* @param cwCompanyId - The ConnectWise company ID
|
||||
* @returns {Promise<ActivityController[]>}
|
||||
*/
|
||||
async fetchByCompany(cwCompanyId: number): Promise<ActivityController[]> {
|
||||
try {
|
||||
const collection = await activityCw.fetchByCompany(cwCompanyId);
|
||||
return collection.map((item) => new ActivityController(item));
|
||||
} catch (error) {
|
||||
const errBody = (error as any).response?.data || error;
|
||||
throw new GenericError({
|
||||
name: "FetchCompanyActivitiesError",
|
||||
message: `Failed to fetch activities for company ${cwCompanyId}`,
|
||||
cause: typeof errBody === "string" ? errBody : JSON.stringify(errBody),
|
||||
status: 502,
|
||||
});
|
||||
}
|
||||
// TODO: Query local Activity table when synced by dalpuri
|
||||
return [];
|
||||
},
|
||||
|
||||
/**
|
||||
* Fetch Activities by Opportunity
|
||||
*
|
||||
* Fetches all activities for an opportunity by its ConnectWise opportunity ID.
|
||||
*
|
||||
* @param cwOpportunityId - The ConnectWise opportunity ID
|
||||
* @returns {Promise<ActivityController[]>}
|
||||
*/
|
||||
async fetchByOpportunity(
|
||||
cwOpportunityId: number,
|
||||
): Promise<ActivityController[]> {
|
||||
try {
|
||||
const collection = await activityCw.fetchByOpportunity(cwOpportunityId);
|
||||
return collection.map((item) => new ActivityController(item));
|
||||
} catch (error) {
|
||||
const errBody = (error as any).response?.data || error;
|
||||
throw new GenericError({
|
||||
name: "FetchOpportunityActivitiesError",
|
||||
message: `Failed to fetch activities for opportunity ${cwOpportunityId}`,
|
||||
cause: typeof errBody === "string" ? errBody : JSON.stringify(errBody),
|
||||
status: 502,
|
||||
});
|
||||
}
|
||||
// TODO: Query local Activity table when synced by dalpuri
|
||||
return [];
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -196,16 +146,7 @@ export const activities = {
|
||||
* @returns {Promise<number>}
|
||||
*/
|
||||
async count(conditions?: string): Promise<number> {
|
||||
try {
|
||||
return await activityCw.countItems(conditions);
|
||||
} catch (error) {
|
||||
const errBody = (error as any).response?.data || error;
|
||||
throw new GenericError({
|
||||
name: "CountActivitiesError",
|
||||
message: "Failed to count activities in ConnectWise",
|
||||
cause: typeof errBody === "string" ? errBody : JSON.stringify(errBody),
|
||||
status: 502,
|
||||
});
|
||||
}
|
||||
// TODO: Count from local Activity table when synced by dalpuri
|
||||
return 0;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,35 +1,23 @@
|
||||
import { connectWiseApi, prisma } from "../constants";
|
||||
import { prisma } from "../constants";
|
||||
import { CompanyController } from "../controllers/CompanyController";
|
||||
import { Company } from "../types/ConnectWiseTypes";
|
||||
|
||||
export const companies = {
|
||||
async fetch(identifier: string | number): Promise<CompanyController> {
|
||||
const search = await prisma.company.findFirst({
|
||||
where: {
|
||||
OR: [{ id: identifier as string }],
|
||||
},
|
||||
const isNumeric =
|
||||
typeof identifier === "number" || /^\d+$/.test(String(identifier));
|
||||
|
||||
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(
|
||||
`/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,
|
||||
});
|
||||
return new CompanyController(company);
|
||||
},
|
||||
|
||||
async count() {
|
||||
@@ -75,12 +63,14 @@ export const companies = {
|
||||
const skip = (page > 1 ? page : 0) * rpp;
|
||||
const take = rpp ?? 30;
|
||||
|
||||
const numericQuery = parseInt(query, 10);
|
||||
|
||||
const data = prisma.company.findMany({
|
||||
where: {
|
||||
OR: [
|
||||
{ cw_Identifier: { contains: query, mode: "insensitive" } },
|
||||
{ name: { contains: query, mode: "insensitive" } },
|
||||
{ id: { contains: query, mode: "insensitive" } },
|
||||
{ uid: { contains: query, mode: "insensitive" } },
|
||||
...(!isNaN(numericQuery) ? [{ id: numericQuery }] : []),
|
||||
],
|
||||
},
|
||||
skip,
|
||||
|
||||
@@ -2,6 +2,7 @@ import { prisma } from "../constants";
|
||||
import { CredentialTypeController } from "../controllers/CredentialTypeController";
|
||||
import { CredentialTypeField } from "../modules/credentials/credentialTypeDefs";
|
||||
import GenericError from "../Errors/GenericError";
|
||||
import { Prisma } from "@prisma/client";
|
||||
|
||||
export const credentialTypes = {
|
||||
/**
|
||||
@@ -83,7 +84,7 @@ export const credentialTypes = {
|
||||
data: {
|
||||
name: data.name,
|
||||
permissionScope: data.permissionScope,
|
||||
fields: data.fields as any,
|
||||
fields: data.fields as Prisma.JsonArray,
|
||||
icon: data.icon,
|
||||
},
|
||||
include: {
|
||||
@@ -91,8 +92,6 @@ export const credentialTypes = {
|
||||
},
|
||||
});
|
||||
|
||||
console.log(credentialType.fields);
|
||||
|
||||
return new CredentialTypeController(credentialType);
|
||||
},
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
} from "../modules/credentials/credentialTypeDefs";
|
||||
import { generateSecureValue } from "../modules/credentials/generateSecureValue";
|
||||
import GenericError from "../Errors/GenericError";
|
||||
import { Prisma } from "@prisma/client";
|
||||
|
||||
/**
|
||||
* 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
|
||||
// from value validation since they don't carry a direct value).
|
||||
@@ -129,16 +131,16 @@ export const credentials = {
|
||||
})) as CredentialTypeField[];
|
||||
|
||||
const validatedFields = await fieldValidator(
|
||||
data.fields as any as CredentialField[],
|
||||
acceptableFields,
|
||||
data.fields as Prisma.JsonArray as CredentialField[],
|
||||
acceptableFields
|
||||
);
|
||||
|
||||
// Separate secure, non-secure, and multi-credential fields
|
||||
const secureFields = validatedFields.filter(
|
||||
(f) => f.secure && !f.isMultiCredential,
|
||||
(f) => f.secure && !f.isMultiCredential
|
||||
);
|
||||
const nonSecureFields = validatedFields.filter(
|
||||
(f) => !f.secure && !f.isMultiCredential,
|
||||
(f) => !f.secure && !f.isMultiCredential
|
||||
);
|
||||
|
||||
// Build fields object for non-secure fields
|
||||
@@ -181,7 +183,7 @@ export const credentials = {
|
||||
// Create inline sub-credentials when provided
|
||||
if (data.subCredentials) {
|
||||
for (const [fieldId, subCredDataList] of Object.entries(
|
||||
data.subCredentials,
|
||||
data.subCredentials
|
||||
)) {
|
||||
const fieldDef = typeFields.find((f) => f.id === fieldId);
|
||||
|
||||
@@ -200,8 +202,8 @@ export const credentials = {
|
||||
|
||||
for (const subCredData of subCredDataList) {
|
||||
const validatedSubFields = await fieldValidator(
|
||||
subCredData.fields as any as CredentialField[],
|
||||
subFieldDefs,
|
||||
subCredData.fields as Prisma.JsonArray as CredentialField[],
|
||||
subFieldDefs
|
||||
);
|
||||
|
||||
const subSecure = validatedSubFields.filter((f) => f.secure);
|
||||
@@ -267,7 +269,7 @@ export const credentials = {
|
||||
data: {
|
||||
name: string;
|
||||
fields: { fieldId: string; value: string }[];
|
||||
},
|
||||
}
|
||||
): Promise<CredentialController> {
|
||||
const parent = await prisma.credential.findFirst({
|
||||
where: { id: parentId },
|
||||
@@ -297,8 +299,8 @@ export const credentials = {
|
||||
|
||||
const subFieldDefs = (fieldDef.subFields ?? []) as CredentialTypeField[];
|
||||
const validatedFields = await fieldValidator(
|
||||
data.fields as any as CredentialField[],
|
||||
subFieldDefs,
|
||||
data.fields as Prisma.JsonArray as CredentialField[],
|
||||
subFieldDefs
|
||||
);
|
||||
|
||||
const secureFields = validatedFields.filter((f) => f.secure);
|
||||
@@ -352,7 +354,7 @@ export const credentials = {
|
||||
*/
|
||||
async removeSubCredential(
|
||||
parentId: string,
|
||||
subCredentialId: string,
|
||||
subCredentialId: string
|
||||
): Promise<void> {
|
||||
const subCredential = await prisma.credential.findFirst({
|
||||
where: { id: subCredentialId, subCredentialOfId: parentId },
|
||||
@@ -382,7 +384,7 @@ export const credentials = {
|
||||
for (const key of Object.keys(parentFields)) {
|
||||
if (Array.isArray(parentFields[key])) {
|
||||
parentFields[key] = parentFields[key].filter(
|
||||
(id: string) => id !== subCredentialId,
|
||||
(id: string) => id !== subCredentialId
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ export const generatedQuotes = {
|
||||
},
|
||||
|
||||
async fetchByOpportunity(
|
||||
opportunityId: string,
|
||||
opportunityId: string
|
||||
): Promise<GeneratedQuoteController[]> {
|
||||
const rows = await prisma.generatedQuotes.findMany({
|
||||
where: { opportunityId },
|
||||
@@ -43,7 +43,7 @@ export const generatedQuotes = {
|
||||
},
|
||||
|
||||
async fetchByCreator(
|
||||
createdById: string,
|
||||
createdById: string
|
||||
): Promise<GeneratedQuoteController[]> {
|
||||
const rows = await prisma.generatedQuotes.findMany({
|
||||
where: { createdById },
|
||||
@@ -55,7 +55,7 @@ export const generatedQuotes = {
|
||||
},
|
||||
|
||||
async fetchByHash(
|
||||
quoteRegenHash: string,
|
||||
quoteRegenHash: string
|
||||
): Promise<GeneratedQuoteController | null> {
|
||||
const quote = await prisma.generatedQuotes.findUnique({
|
||||
where: { quoteRegenHash },
|
||||
@@ -76,15 +76,15 @@ export const generatedQuotes = {
|
||||
createdById: string;
|
||||
}): Promise<GeneratedQuoteController> {
|
||||
const opportunity = await prisma.opportunity.findFirst({
|
||||
where: { id: data.opportunityId },
|
||||
select: { id: true },
|
||||
where: { uid: data.opportunityId },
|
||||
select: { uid: true },
|
||||
});
|
||||
|
||||
if (!opportunity) {
|
||||
throw new GenericError({
|
||||
message: "Opportunity not found",
|
||||
name: "OpportunityNotFound",
|
||||
cause: `No opportunity exists with ID '${data.opportunityId}'`,
|
||||
cause: `No opportunity exists with uid '${data.opportunityId}'`,
|
||||
status: 404,
|
||||
});
|
||||
}
|
||||
@@ -147,7 +147,7 @@ export const generatedQuotes = {
|
||||
name?: string | null;
|
||||
email: string;
|
||||
fetchAction: string;
|
||||
},
|
||||
}
|
||||
): Promise<GeneratedQuoteController> {
|
||||
const existing = await prisma.generatedQuotes.findFirst({
|
||||
where: { id },
|
||||
|
||||
+226
-1048
File diff suppressed because it is too large
Load Diff
+114
-46
@@ -11,10 +11,14 @@ import {
|
||||
|
||||
/**
|
||||
* 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 = {
|
||||
linkedItems: true,
|
||||
manufacturer: true,
|
||||
subcategory: { include: { category: true } },
|
||||
} as const;
|
||||
|
||||
const LABOR_STYLE_CANDIDATES = {
|
||||
@@ -22,8 +26,22 @@ const LABOR_STYLE_CANDIDATES = {
|
||||
tech: ["LABOR & INSTALLATION - TECH", "LABOR - TECH", "LABOR TECH"],
|
||||
} 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(
|
||||
candidates: readonly string[],
|
||||
candidates: readonly string[]
|
||||
): Promise<CatalogItemController | null> {
|
||||
for (const candidate of candidates) {
|
||||
const item = await prisma.catalogItem.findFirst({
|
||||
@@ -44,7 +62,7 @@ async function findCatalogByExactCandidates(
|
||||
}
|
||||
|
||||
async function findCatalogByLaborStyle(
|
||||
style: "field" | "tech",
|
||||
style: "field" | "tech"
|
||||
): Promise<CatalogItemController | null> {
|
||||
const fallback = await prisma.catalogItem.findFirst({
|
||||
where: {
|
||||
@@ -92,6 +110,8 @@ export interface CatalogFilterOpts {
|
||||
*/
|
||||
function buildFilterWhere(opts: CatalogFilterOpts = {}) {
|
||||
const conditions: Record<string, unknown>[] = [];
|
||||
const minPrice = normalizeFiniteNumber(opts.minPrice);
|
||||
const maxPrice = normalizeFiniteNumber(opts.maxPrice);
|
||||
|
||||
const parseNumericId = (value?: string): number | null => {
|
||||
if (!value) return null;
|
||||
@@ -131,14 +151,14 @@ function buildFilterWhere(opts: CatalogFilterOpts = {}) {
|
||||
if (opts.category) {
|
||||
if (categoryId) {
|
||||
const categoryOr: Record<string, unknown>[] = [
|
||||
{ categoryCwId: categoryId },
|
||||
{ subcategory: { is: { category: { is: { id: categoryId } } } } },
|
||||
];
|
||||
if (resolvedCategoryName) {
|
||||
categoryOr.push({ category: resolvedCategoryName });
|
||||
categoryOr.push({ subcategory: { is: { category: { is: { name: resolvedCategoryName } } } } });
|
||||
}
|
||||
conditions.push({ OR: categoryOr });
|
||||
} 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) {
|
||||
const resolvedSubcategoryName = resolveSubcategoryNameById(subcategoryId);
|
||||
const subcategoryOr: Record<string, unknown>[] = [
|
||||
{ subcategoryCwId: subcategoryId },
|
||||
{ subcategory: { is: { id: subcategoryId } } },
|
||||
];
|
||||
if (resolvedSubcategoryName) {
|
||||
subcategoryOr.push({ subcategory: resolvedSubcategoryName });
|
||||
subcategoryOr.push({ subcategory: { is: { name: resolvedSubcategoryName } } });
|
||||
}
|
||||
conditions.push({ OR: subcategoryOr });
|
||||
} else {
|
||||
conditions.push({ subcategory: opts.subcategory });
|
||||
conditions.push({ subcategory: { is: { name: opts.subcategory } } });
|
||||
}
|
||||
}
|
||||
|
||||
if (opts.group && opts.category) {
|
||||
if (!resolvedCategoryName) {
|
||||
conditions.push({ category: "__unknown_category__" });
|
||||
conditions.push({ subcategory: { is: { category: { is: { name: "__unknown_category__" } } } } });
|
||||
}
|
||||
if (resolvedCategoryName) {
|
||||
const subcats = getSubcategoriesForGroup(
|
||||
resolvedCategoryName,
|
||||
opts.group,
|
||||
opts.group
|
||||
);
|
||||
if (subcats.length > 0) {
|
||||
conditions.push({ subcategory: { in: subcats } });
|
||||
conditions.push({ subcategory: { is: { name: { in: subcats } } } });
|
||||
}
|
||||
}
|
||||
} else if (opts.group && !opts.category) {
|
||||
// Try to find the group in any category
|
||||
const {
|
||||
CATEGORY_TREE,
|
||||
isCategoryGroup,
|
||||
} = require("../modules/catalog-categories/catalogCategories");
|
||||
for (const cat of CATEGORY_TREE) {
|
||||
const subcats = getSubcategoriesForGroup(cat.name, opts.group);
|
||||
if (subcats.length > 0) {
|
||||
conditions.push({ category: cat.name, subcategory: { in: subcats } });
|
||||
conditions.push({
|
||||
subcategory: {
|
||||
is: {
|
||||
name: { in: subcats },
|
||||
category: { is: { name: cat.name } },
|
||||
},
|
||||
},
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -187,20 +210,32 @@ function buildFilterWhere(opts: CatalogFilterOpts = {}) {
|
||||
|
||||
if (opts.manufacturer) {
|
||||
conditions.push({
|
||||
manufacturer: { contains: opts.manufacturer, mode: "insensitive" },
|
||||
manufacturer: {
|
||||
is: {
|
||||
name: { contains: opts.manufacturer, mode: "insensitive" },
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (opts.ecosystem) {
|
||||
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) {
|
||||
conditions.push({
|
||||
OR: eco.manufacturers.map((m) => ({
|
||||
manufacturer: { contains: m.name, mode: "insensitive" as const },
|
||||
subcategory: { startsWith: m.subcategoryPrefix },
|
||||
category: m.category,
|
||||
manufacturer: {
|
||||
is: {
|
||||
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 } });
|
||||
}
|
||||
|
||||
if (opts.minPrice !== undefined) {
|
||||
conditions.push({ price: { gte: opts.minPrice } });
|
||||
if (minPrice !== undefined) {
|
||||
conditions.push({ price: { gte: minPrice } });
|
||||
}
|
||||
|
||||
if (opts.maxPrice !== undefined) {
|
||||
conditions.push({ price: { lte: opts.maxPrice } });
|
||||
if (maxPrice !== undefined) {
|
||||
conditions.push({ price: { lte: maxPrice } });
|
||||
}
|
||||
|
||||
return conditions.length > 0 ? { AND: conditions } : undefined;
|
||||
@@ -237,10 +272,10 @@ export const procurement = {
|
||||
|
||||
const item = await prisma.catalogItem.findFirst({
|
||||
where: isNumeric
|
||||
? { cwCatalogId: Number(identifier) }
|
||||
? { id: Number(identifier) }
|
||||
: {
|
||||
OR: [
|
||||
{ id: identifier as string },
|
||||
{ uid: identifier as string },
|
||||
{ identifier: identifier as string },
|
||||
],
|
||||
},
|
||||
@@ -302,10 +337,12 @@ export const procurement = {
|
||||
async fetchPages(
|
||||
page: number,
|
||||
rpp: number,
|
||||
opts?: CatalogFilterOpts,
|
||||
opts?: CatalogFilterOpts
|
||||
): Promise<CatalogItemController[]> {
|
||||
const skip = (Math.max(page, 1) - 1) * rpp;
|
||||
const take = rpp;
|
||||
const safePage = normalizePositiveInt(page, 1);
|
||||
const safeRpp = normalizePositiveInt(rpp, DEFAULT_CATALOG_RPP);
|
||||
const skip = (safePage - 1) * safeRpp;
|
||||
const take = safeRpp;
|
||||
|
||||
const items = await prisma.catalogItem.findMany({
|
||||
where: buildFilterWhere(opts),
|
||||
@@ -334,10 +371,12 @@ export const procurement = {
|
||||
query: string,
|
||||
page: number,
|
||||
rpp: number,
|
||||
opts?: CatalogFilterOpts,
|
||||
opts?: CatalogFilterOpts
|
||||
): Promise<CatalogItemController[]> {
|
||||
const skip = (Math.max(page, 1) - 1) * rpp;
|
||||
const take = rpp;
|
||||
const safePage = normalizePositiveInt(page, 1);
|
||||
const safeRpp = normalizePositiveInt(rpp, DEFAULT_CATALOG_RPP);
|
||||
const skip = (safePage - 1) * safeRpp;
|
||||
const take = safeRpp;
|
||||
|
||||
const filterWhere = buildFilterWhere(opts) ?? {};
|
||||
|
||||
@@ -350,7 +389,11 @@ export const procurement = {
|
||||
{ description: { contains: query, mode: "insensitive" } },
|
||||
{ partNumber: { contains: query, mode: "insensitive" } },
|
||||
{ vendorSku: { contains: query, mode: "insensitive" } },
|
||||
{ manufacturer: { contains: query, mode: "insensitive" } },
|
||||
{
|
||||
manufacturer: {
|
||||
is: { name: { contains: query, mode: "insensitive" } },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
skip,
|
||||
@@ -371,7 +414,7 @@ export const procurement = {
|
||||
* @returns {Promise<number>} - Total count
|
||||
*/
|
||||
async count(
|
||||
opts?: CatalogFilterOpts & { activeOnly?: boolean },
|
||||
opts?: CatalogFilterOpts & { activeOnly?: boolean }
|
||||
): Promise<number> {
|
||||
// Support legacy `activeOnly` flag by mapping it to `includeInactive`
|
||||
const filterOpts: CatalogFilterOpts = {
|
||||
@@ -407,7 +450,11 @@ export const procurement = {
|
||||
{ description: { contains: query, mode: "insensitive" } },
|
||||
{ partNumber: { 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(
|
||||
field: "category" | "subcategory" | "manufacturer",
|
||||
opts?: CatalogFilterOpts,
|
||||
opts?: CatalogFilterOpts
|
||||
): Promise<string[]> {
|
||||
if (field === "manufacturer") {
|
||||
const items = await prisma.catalogItem.findMany({
|
||||
where: buildFilterWhere(opts),
|
||||
select: { manufacturer: { select: { name: true } } },
|
||||
});
|
||||
const names = items
|
||||
.map((item) => item.manufacturer?.name ?? 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: { [field]: true },
|
||||
distinct: [field],
|
||||
orderBy: { [field]: "asc" },
|
||||
select: { subcategory: { select: { category: { select: { name: true } } } } },
|
||||
});
|
||||
|
||||
return items
|
||||
.map((item: Record<string, unknown>) => item[field] as string | null)
|
||||
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(
|
||||
sourceIdentifier: string | number,
|
||||
targetIdentifier: string | number,
|
||||
targetIdentifier: string | number
|
||||
): Promise<CatalogItemController> {
|
||||
const source = await procurement.fetchItem(sourceIdentifier);
|
||||
const target = await procurement.fetchItem(targetIdentifier);
|
||||
@@ -469,7 +537,7 @@ export const procurement = {
|
||||
*/
|
||||
async unlinkItems(
|
||||
sourceIdentifier: string | number,
|
||||
targetIdentifier: string | number,
|
||||
targetIdentifier: string | number
|
||||
): Promise<CatalogItemController> {
|
||||
const source = await procurement.fetchItem(sourceIdentifier);
|
||||
const target = await procurement.fetchItem(targetIdentifier);
|
||||
|
||||
@@ -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));
|
||||
},
|
||||
};
|
||||
+40
-13
@@ -1,10 +1,8 @@
|
||||
import { ms } from "zod/locales";
|
||||
import { User } from "../../generated/prisma/client";
|
||||
import { prisma } from "../constants";
|
||||
import { SessionTokensObject } from "../controllers/SessionController";
|
||||
import UserController from "../controllers/UserController";
|
||||
import { fetchMicrosoftUser } from "../modules/fetchMicrosoftUser";
|
||||
import { findCwIdentifierByEmail } from "../modules/cw-utils/members/fetchAllMembers";
|
||||
import { events } from "../modules/globalEvents";
|
||||
import { sessions } from "./sessions";
|
||||
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
|
||||
*/
|
||||
async authenticate(
|
||||
authRequest: msal.AuthenticationResult,
|
||||
authRequest: msal.AuthenticationResult
|
||||
): Promise<SessionTokensObject> {
|
||||
let id = authRequest.uniqueId as string;
|
||||
|
||||
@@ -63,7 +61,7 @@ export const users = {
|
||||
email: string;
|
||||
login: string;
|
||||
userId: string;
|
||||
}>,
|
||||
}>
|
||||
) {
|
||||
if (Object.keys(identifier).length == 0) return null;
|
||||
const userData = await prisma.user.findFirst({
|
||||
@@ -90,18 +88,45 @@ export const users = {
|
||||
*/
|
||||
async createUser(token: string): Promise<UserController> {
|
||||
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
|
||||
const cwIdentifier = await findCwIdentifierByEmail(msData.mail).catch(
|
||||
() => null,
|
||||
);
|
||||
const cwIdentifier = await prisma.cwMember
|
||||
.findFirst({ where: { officeEmail: resolvedEmail } })
|
||||
.then((m) => m?.identifier ?? null)
|
||||
.catch(() => null);
|
||||
|
||||
const newUser = await prisma.user.create({
|
||||
data: {
|
||||
const existingUser = await prisma.user.findUnique({
|
||||
where: { email: resolvedEmail },
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
const newUser = await prisma.user.upsert({
|
||||
where: { email: resolvedEmail },
|
||||
create: {
|
||||
userId: msData.id,
|
||||
email: msData.mail ?? msData.userPrincipalName,
|
||||
name: `${msData.givenName} ${msData.surname}`,
|
||||
login: msData.userPrincipalName,
|
||||
email: resolvedEmail,
|
||||
firstName: msData.givenName ?? null,
|
||||
lastName: msData.surname ?? null,
|
||||
login: resolvedLogin,
|
||||
cwIdentifier,
|
||||
token,
|
||||
},
|
||||
update: {
|
||||
userId: msData.id,
|
||||
firstName: msData.givenName ?? null,
|
||||
lastName: msData.surname ?? null,
|
||||
login: resolvedLogin,
|
||||
cwIdentifier,
|
||||
token,
|
||||
},
|
||||
@@ -109,7 +134,9 @@ export const users = {
|
||||
});
|
||||
|
||||
let controller = new UserController(newUser);
|
||||
events.emit("user:created", controller);
|
||||
if (!existingUser) {
|
||||
events.emit("user:created", 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
@@ -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
@@ -1,6 +1,4 @@
|
||||
import { prisma, redis } from "../../constants";
|
||||
import { getCachedOppCwData, getCachedProducts } from "./opportunityCache";
|
||||
import { OpportunityStatus } from "../../workflows/wf.opportunity";
|
||||
import { events } from "../globalEvents";
|
||||
import { opportunities } from "../../managers/opportunities";
|
||||
import { normalizeProbabilityRatio } from "../sales-utils/normalizeProbability";
|
||||
@@ -101,13 +99,16 @@ interface CachedOpportunityRevenue {
|
||||
}
|
||||
|
||||
interface OpportunityRow {
|
||||
id: string;
|
||||
cwOpportunityId: number;
|
||||
id: number;
|
||||
uid: string;
|
||||
name: string;
|
||||
primarySalesRepIdentifier: string | null;
|
||||
secondarySalesRepIdentifier: string | null;
|
||||
statusCwId: number | null;
|
||||
statusName: string | null;
|
||||
primarySalesRepId: string | null;
|
||||
secondarySalesRepId: string | null;
|
||||
status: {
|
||||
wonFlag: boolean;
|
||||
lostFlag: boolean;
|
||||
closeFlag: boolean;
|
||||
} | null;
|
||||
closedFlag: boolean;
|
||||
dateBecameLead: Date | null;
|
||||
closedDate: Date | null;
|
||||
@@ -137,107 +138,23 @@ const toFinite = (value: unknown): number => {
|
||||
return n;
|
||||
};
|
||||
|
||||
const isWon = (opp: {
|
||||
statusCwId: number | null;
|
||||
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 isWon = (opp: { status: { wonFlag: boolean } | null }) =>
|
||||
Boolean(opp.status?.wonFlag);
|
||||
|
||||
const isLost = (opp: {
|
||||
statusCwId: number | null;
|
||||
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 isLost = (opp: { status: { lostFlag: boolean } | null }) =>
|
||||
Boolean(opp.status?.lostFlag);
|
||||
|
||||
const isClosedOpportunity = (opp: {
|
||||
statusCwId: number | null;
|
||||
statusName: string | null;
|
||||
status: { wonFlag: boolean; lostFlag: boolean; closeFlag: boolean } | null;
|
||||
closedFlag: boolean;
|
||||
}) => {
|
||||
if (opp.closedFlag) return true;
|
||||
if (opp.status?.closeFlag) return true;
|
||||
if (isWon(opp)) return true;
|
||||
if (isLost(opp)) return true;
|
||||
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 = (
|
||||
products: Array<{
|
||||
@@ -298,20 +215,8 @@ const writeCachedOpportunityRevenue = async (
|
||||
);
|
||||
};
|
||||
|
||||
const resolveProbabilityRatio = async (opp: {
|
||||
cwOpportunityId: number;
|
||||
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 resolveProbabilityRatio = (opp: { probability: number }): number =>
|
||||
normalizeProbabilityRatio(opp.probability);
|
||||
|
||||
const getOpportunityRevenueCacheFirst = async (
|
||||
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 {
|
||||
const opportunity = await opportunities.fetchRecord(cwOpportunityId);
|
||||
const products = await opportunity.fetchProducts({
|
||||
@@ -489,8 +382,8 @@ export async function refreshSalesOpportunityMetricsCache(
|
||||
AND: [
|
||||
{
|
||||
OR: [
|
||||
{ primarySalesRepIdentifier: { in: memberIdentifiers } },
|
||||
{ secondarySalesRepIdentifier: { in: memberIdentifiers } },
|
||||
{ primarySalesRepId: { in: memberIdentifiers } },
|
||||
{ secondarySalesRepId: { in: memberIdentifiers } },
|
||||
],
|
||||
},
|
||||
{ dateBecameLead: { gte: yearStart } },
|
||||
@@ -501,12 +394,17 @@ export async function refreshSalesOpportunityMetricsCache(
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
cwOpportunityId: true,
|
||||
uid: true,
|
||||
name: true,
|
||||
primarySalesRepIdentifier: true,
|
||||
secondarySalesRepIdentifier: true,
|
||||
statusCwId: true,
|
||||
statusName: true,
|
||||
primarySalesRepId: true,
|
||||
secondarySalesRepId: true,
|
||||
status: {
|
||||
select: {
|
||||
wonFlag: true,
|
||||
lostFlag: true,
|
||||
closeFlag: true,
|
||||
},
|
||||
},
|
||||
closedFlag: true,
|
||||
dateBecameLead: true,
|
||||
closedDate: true,
|
||||
@@ -565,7 +463,7 @@ export async function refreshSalesOpportunityMetricsCache(
|
||||
async (opp) => {
|
||||
const [revenue, probabilityRatio] = await Promise.all([
|
||||
withTimeout(
|
||||
getOpportunityRevenueCacheFirst(opp.cwOpportunityId, {
|
||||
getOpportunityRevenueCacheFirst(opp.id, {
|
||||
forceColdLoad,
|
||||
}),
|
||||
PRODUCT_LOOKUP_TIMEOUT_MS,
|
||||
@@ -619,10 +517,10 @@ export async function refreshSalesOpportunityMetricsCache(
|
||||
|
||||
for (const opp of opportunityRows) {
|
||||
const assigned = new Set<string>();
|
||||
if (opp.primarySalesRepIdentifier)
|
||||
assigned.add(opp.primarySalesRepIdentifier);
|
||||
if (opp.secondarySalesRepIdentifier)
|
||||
assigned.add(opp.secondarySalesRepIdentifier);
|
||||
if (opp.primarySalesRepId)
|
||||
assigned.add(opp.primarySalesRepId);
|
||||
if (opp.secondarySalesRepId)
|
||||
assigned.add(opp.secondarySalesRepId);
|
||||
|
||||
for (const identifier of assigned) {
|
||||
const bucket = opportunitiesByMember.get(identifier);
|
||||
@@ -665,8 +563,8 @@ export async function refreshSalesOpportunityMetricsCache(
|
||||
);
|
||||
|
||||
const breakdownEntry: OpportunityBreakdownEntry = {
|
||||
id: opp.id,
|
||||
cwId: opp.cwOpportunityId,
|
||||
id: opp.uid,
|
||||
cwId: opp.id,
|
||||
name: opp.name,
|
||||
revenue: revenue.totalRevenue,
|
||||
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,
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -1,41 +1,10 @@
|
||||
import { Company } from "../../types/ConnectWiseTypes";
|
||||
import {
|
||||
CollectorCompanyRecord,
|
||||
CompanySourceRecord,
|
||||
NormalizedCompanyRecord,
|
||||
} from "../../types/CompanySourceTypes";
|
||||
|
||||
export const isCollectorCompanyRecord = (
|
||||
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 = (
|
||||
const normalizeCompany = (
|
||||
company: Company,
|
||||
): NormalizedCompanyRecord | null => {
|
||||
if (!company.identifier || !company.name) {
|
||||
@@ -52,11 +21,7 @@ const normalizeFromCwApi = (
|
||||
export const normalizeCompanyRecord = (
|
||||
source: CompanySourceRecord,
|
||||
): NormalizedCompanyRecord | null => {
|
||||
if (isCollectorCompanyRecord(source)) {
|
||||
return normalizeFromCollector(source);
|
||||
}
|
||||
|
||||
return normalizeFromCwApi(source);
|
||||
return normalizeCompany(source);
|
||||
};
|
||||
|
||||
export const normalizeCompanyRecords = (
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user