Compare commits
30 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a7979eba73 | |||
| 5141ed20f9 | |||
| a8c48e8c75 | |||
| 051edb5f78 | |||
| f87f6dd336 | |||
| 2eb387811d | |||
| db27c9224d | |||
| 7f6e6fdfbc | |||
| 5f5f610060 | |||
| 809841d672 | |||
| 276eb563bf | |||
| 7624ba0bc0 | |||
| 1063231107 | |||
| 2cd5dee612 | |||
| 8ac1cbaf3e | |||
| bd7e6a37cd | |||
| 4e0799f9d9 | |||
| 223a06ba27 | |||
| 503657d168 | |||
| cf68e281e8 | |||
| 57b5763d41 | |||
| 2bd498a35d | |||
| 86d7426e8b | |||
| afe56393e7 | |||
| b2cd26af30 | |||
| 0594816ea4 | |||
| 71fe36c0b8 | |||
| e0d575454e | |||
| 32bba31e72 | |||
| 1233535b20 |
@@ -1,133 +0,0 @@
|
|||||||
name: API - Build and Publish
|
|
||||||
|
|
||||||
on:
|
|
||||||
release:
|
|
||||||
types: [created]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
test:
|
|
||||||
name: Test
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
defaults:
|
|
||||||
run:
|
|
||||||
working-directory: api
|
|
||||||
steps:x
|
|
||||||
- name: Checkout source code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Setup Bun
|
|
||||||
uses: oven-sh/setup-bun@v2
|
|
||||||
with:
|
|
||||||
bun-version: "1.3.6"
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: bun install --frozen-lockfile
|
|
||||||
|
|
||||||
- name: Generate Prisma client
|
|
||||||
run: DATABASE_URL="postgresql://dummy:dummy@localhost:5432/dummy" bunx prisma generate
|
|
||||||
|
|
||||||
- name: Run tests
|
|
||||||
run: bun test --preload ./tests/setup.ts
|
|
||||||
|
|
||||||
build:
|
|
||||||
name: Build
|
|
||||||
needs: [test]
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
packages: write
|
|
||||||
steps:
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v3
|
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
|
||||||
uses: docker/login-action@v3
|
|
||||||
with:
|
|
||||||
registry: ghcr.io
|
|
||||||
username: ${{ github.repository_owner }}
|
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Build and push the Docker image
|
|
||||||
uses: docker/build-push-action@v6
|
|
||||||
with:
|
|
||||||
context: ./api
|
|
||||||
push: true
|
|
||||||
target: runtime
|
|
||||||
tags: |
|
|
||||||
ghcr.io/project-optima/ttscm-api:latest
|
|
||||||
ghcr.io/project-optima/ttscm-api:${{ github.event.release.tag_name }}
|
|
||||||
|
|
||||||
- name: Build and push the migration image
|
|
||||||
uses: docker/build-push-action@v6
|
|
||||||
with:
|
|
||||||
context: ./api
|
|
||||||
push: true
|
|
||||||
target: migration
|
|
||||||
tags: |
|
|
||||||
ghcr.io/project-optima/ttscm-api-migrate:latest
|
|
||||||
ghcr.io/project-optima/ttscm-api-migrate:${{ github.event.release.tag_name }}
|
|
||||||
|
|
||||||
migrate:
|
|
||||||
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
|
|
||||||
with:
|
|
||||||
method: kubeconfig
|
|
||||||
kubeconfig: ${{ secrets.KUBECONFIG }}
|
|
||||||
|
|
||||||
- name: Checkout source code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Delete previous migration job if exists
|
|
||||||
run: kubectl delete job -n optima -l app=prisma-migrate --ignore-not-found
|
|
||||||
|
|
||||||
- name: Apply migration job
|
|
||||||
run: |
|
|
||||||
TAG=${{ github.event.release.tag_name }}
|
|
||||||
sed "s/RELEASE_TAG/${TAG}/g" kubernetes/migration-job.yaml | kubectl apply -f -
|
|
||||||
|
|
||||||
- name: Wait for migration to complete
|
|
||||||
run: |
|
|
||||||
TAG=${{ github.event.release.tag_name }}
|
|
||||||
kubectl wait --for=condition=complete --timeout=120s -n optima job/prisma-migrate-${TAG}
|
|
||||||
|
|
||||||
deploy:
|
|
||||||
name: Deploy
|
|
||||||
needs: [migrate]
|
|
||||||
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: |
|
|
||||||
api/kubernetes/deployment.yaml
|
|
||||||
api/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: |
|
|
||||||
api/kubernetes/deployment.yaml
|
|
||||||
api/kubernetes/ingress.yaml
|
|
||||||
images: |
|
|
||||||
ghcr.io/project-optima/ttscm-api:${{ github.event.release.tag_name }}
|
|
||||||
@@ -18,7 +18,7 @@ jobs:
|
|||||||
bun-version: "1.3.6"
|
bun-version: "1.3.6"
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: bun install --frozen-lockfile
|
run: bun install
|
||||||
|
|
||||||
- name: Generate API Prisma client
|
- name: Generate API Prisma client
|
||||||
run: DATABASE_URL="postgresql://dummy:dummy@localhost:5432/dummy" bunx prisma generate
|
run: DATABASE_URL="postgresql://dummy:dummy@localhost:5432/dummy" bunx prisma generate
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ jobs:
|
|||||||
bun-version: "1.3.6"
|
bun-version: "1.3.6"
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: bun install --frozen-lockfile
|
run: bun install
|
||||||
|
|
||||||
- name: Generate Dalpuri Prisma client (CW MSSQL)
|
- name: Generate Dalpuri Prisma client (CW MSSQL)
|
||||||
run: DATABASE_URL="sqlserver://localhost:1433;database=dummy;user=dummy;password=dummy;trustServerCertificate=true" bunx prisma generate
|
run: DATABASE_URL="sqlserver://localhost:1433;database=dummy;user=dummy;password=dummy;trustServerCertificate=true" bunx prisma generate
|
||||||
|
|||||||
@@ -231,9 +231,10 @@ jobs:
|
|||||||
run: bun install --frozen-lockfile
|
run: bun install --frozen-lockfile
|
||||||
|
|
||||||
- name: Rebuild native modules
|
- name: Rebuild native modules
|
||||||
run: npm rebuild
|
run: npm rebuild --ignore-scripts
|
||||||
env:
|
env:
|
||||||
HUSKY: "0"
|
HUSKY: "0"
|
||||||
|
HUSKY_SKIP_INSTALL: "1"
|
||||||
|
|
||||||
- name: Build macOS distributables
|
- name: Build macOS distributables
|
||||||
run: bun run make:macos
|
run: bun run make:macos
|
||||||
@@ -272,9 +273,10 @@ jobs:
|
|||||||
run: bun install --frozen-lockfile
|
run: bun install --frozen-lockfile
|
||||||
|
|
||||||
- name: Rebuild native modules
|
- name: Rebuild native modules
|
||||||
run: npm rebuild
|
run: npm rebuild --ignore-scripts
|
||||||
env:
|
env:
|
||||||
HUSKY: "0"
|
HUSKY: "0"
|
||||||
|
HUSKY_SKIP_INSTALL: "1"
|
||||||
|
|
||||||
- name: Build Windows distributables
|
- name: Build Windows distributables
|
||||||
run: bun run make -- --platform win32
|
run: bun run make -- --platform win32
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ jobs:
|
|||||||
bun-version: "1.3.11"
|
bun-version: "1.3.11"
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: bun install --frozen-lockfile
|
run: bun install
|
||||||
|
|
||||||
- name: Run unit tests
|
- name: Run unit tests
|
||||||
run: bun run test:unit -- --run
|
run: bun run test:unit -- --run
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
# optima
|
# optima
|
||||||
The primary repository for Optima.
|
The primary repository for Optima.
|
||||||
|
|
||||||
|
|||||||
+34
-8
@@ -17,7 +17,7 @@ COPY dalpuri/package.json ./dalpuri/package.json
|
|||||||
COPY ui/package.json ./ui/package.json
|
COPY ui/package.json ./ui/package.json
|
||||||
COPY patches ./patches
|
COPY patches ./patches
|
||||||
|
|
||||||
RUN bun install --frozen-lockfile --production
|
RUN bun install --production
|
||||||
|
|
||||||
# ---- Stage 2: Build ----
|
# ---- Stage 2: Build ----
|
||||||
FROM oven/bun:1.3.11 AS build
|
FROM oven/bun:1.3.11 AS build
|
||||||
@@ -32,7 +32,7 @@ COPY ui/package.json ./ui/package.json
|
|||||||
COPY patches ./patches
|
COPY patches ./patches
|
||||||
|
|
||||||
# Install all deps (including dev) for the full workspace
|
# Install all deps (including dev) for the full workspace
|
||||||
RUN bun install --frozen-lockfile
|
RUN bun install
|
||||||
|
|
||||||
# Copy API source and config
|
# Copy API source and config
|
||||||
COPY api/src/ ./api/src/
|
COPY api/src/ ./api/src/
|
||||||
@@ -90,6 +90,13 @@ COPY --from=build /app/dalpuri/generated/ ./dalpuri/generated/
|
|||||||
# Copy production node_modules (Prisma adapter needs native bindings)
|
# Copy production node_modules (Prisma adapter needs native bindings)
|
||||||
COPY --from=deps /app/node_modules/ ./node_modules/
|
COPY --from=deps /app/node_modules/ ./node_modules/
|
||||||
|
|
||||||
|
# Copy bun so prisma migrate deploy can run at container startup
|
||||||
|
COPY --from=build /usr/local/bin/bun /usr/local/bin/bun
|
||||||
|
RUN ln -s /usr/local/bin/bun /usr/local/bin/bunx
|
||||||
|
|
||||||
|
# Ensure pdfmake Roboto fonts are present at runtime for PDF generation.
|
||||||
|
COPY --from=build /app/api/node_modules/pdfmake/build/fonts/ ./node_modules/pdfmake/build/fonts/
|
||||||
|
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
|
|
||||||
# ---- Stage 4: API server runtime image ----
|
# ---- Stage 4: API server runtime image ----
|
||||||
@@ -101,7 +108,7 @@ COPY --from=build /app/api/logo.png ./logo.png
|
|||||||
COPY --from=build /app/api/src/modules/sales-utils/salesTaxRates.json ./salesTaxRates.json
|
COPY --from=build /app/api/src/modules/sales-utils/salesTaxRates.json ./salesTaxRates.json
|
||||||
|
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
CMD ["./server"]
|
CMD ["sh", "-c", "bunx prisma migrate deploy && ./server"]
|
||||||
|
|
||||||
# ---- Stage 5: Worker runtime image ----
|
# ---- Stage 5: Worker runtime image ----
|
||||||
FROM runtime-base AS worker
|
FROM runtime-base AS worker
|
||||||
@@ -125,15 +132,13 @@ COPY dalpuri/package.json ./dalpuri/package.json
|
|||||||
COPY ui/package.json ./ui/package.json
|
COPY ui/package.json ./ui/package.json
|
||||||
COPY patches ./patches
|
COPY patches ./patches
|
||||||
|
|
||||||
RUN bun install --frozen-lockfile
|
RUN bun install
|
||||||
|
|
||||||
COPY api/prisma/ ./api/prisma/
|
COPY api/prisma/ ./api/prisma/
|
||||||
COPY api/prisma.config.ts ./api/prisma.config.ts
|
COPY api/prisma.config.ts ./api/prisma.config.ts
|
||||||
|
|
||||||
RUN chmod +x /app/api/prisma/migrate-entrypoint.sh
|
|
||||||
|
|
||||||
WORKDIR /app/api
|
WORKDIR /app/api
|
||||||
CMD ["sh", "prisma/migrate-entrypoint.sh"]
|
CMD ["bunx", "prisma", "migrate", "deploy"]
|
||||||
|
|
||||||
# ---- Stage 7: Dalpuri CW-to-API sync runner ----
|
# ---- Stage 7: Dalpuri CW-to-API sync runner ----
|
||||||
FROM oven/bun:1.3.11 AS dalpuri-sync
|
FROM oven/bun:1.3.11 AS dalpuri-sync
|
||||||
@@ -146,7 +151,7 @@ COPY dalpuri/package.json ./dalpuri/package.json
|
|||||||
COPY ui/package.json ./ui/package.json
|
COPY ui/package.json ./ui/package.json
|
||||||
COPY patches ./patches
|
COPY patches ./patches
|
||||||
|
|
||||||
RUN bun install --frozen-lockfile
|
RUN bun install
|
||||||
|
|
||||||
COPY dalpuri/src/ ./dalpuri/src/
|
COPY dalpuri/src/ ./dalpuri/src/
|
||||||
COPY dalpuri/prisma/ ./dalpuri/prisma/
|
COPY dalpuri/prisma/ ./dalpuri/prisma/
|
||||||
@@ -164,3 +169,24 @@ RUN DATABASE_URL="postgresql://dummy:dummy@localhost:5432/dummy" bunx prisma gen
|
|||||||
|
|
||||||
WORKDIR /app/dalpuri
|
WORKDIR /app/dalpuri
|
||||||
CMD ["bun", "run", "src/sync.ts"]
|
CMD ["bun", "run", "src/sync.ts"]
|
||||||
|
|
||||||
|
FROM oven/bun:1.3.11 AS setup-admin
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY package.json bun.lock ./
|
||||||
|
COPY api/package.json ./api/package.json
|
||||||
|
COPY dalpuri/package.json ./dalpuri/package.json
|
||||||
|
COPY ui/package.json ./ui/package.json
|
||||||
|
COPY patches ./patches
|
||||||
|
|
||||||
|
RUN bun install
|
||||||
|
|
||||||
|
COPY api/prisma/ ./api/prisma/
|
||||||
|
COPY api/prisma.config.ts ./api/prisma.config.ts
|
||||||
|
COPY api/setup-admin.ts ./api/setup-admin.ts
|
||||||
|
|
||||||
|
WORKDIR /app/api
|
||||||
|
RUN DATABASE_URL="postgresql://dummy:dummy@localhost:5432/dummy" bunx prisma generate
|
||||||
|
|
||||||
|
CMD ["bun", "run", "setup-admin.ts"]
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -381,7 +381,7 @@ export type OpportunityGroupByOutputType = {
|
|||||||
name: string
|
name: string
|
||||||
notes: string | null
|
notes: string | null
|
||||||
oppNarrative: string | null
|
oppNarrative: string | null
|
||||||
typeId: number
|
typeId: number | null
|
||||||
stageId: number | null
|
stageId: number | null
|
||||||
statusId: number | null
|
statusId: number | null
|
||||||
taxCodeId: number | null
|
taxCodeId: number | null
|
||||||
@@ -438,7 +438,7 @@ export type OpportunityWhereInput = {
|
|||||||
name?: Prisma.StringFilter<"Opportunity"> | string
|
name?: Prisma.StringFilter<"Opportunity"> | string
|
||||||
notes?: Prisma.StringNullableFilter<"Opportunity"> | string | null
|
notes?: Prisma.StringNullableFilter<"Opportunity"> | string | null
|
||||||
oppNarrative?: Prisma.StringNullableFilter<"Opportunity"> | string | null
|
oppNarrative?: Prisma.StringNullableFilter<"Opportunity"> | string | null
|
||||||
typeId?: Prisma.IntFilter<"Opportunity"> | number
|
typeId?: Prisma.IntNullableFilter<"Opportunity"> | number | null
|
||||||
stageId?: Prisma.IntNullableFilter<"Opportunity"> | number | null
|
stageId?: Prisma.IntNullableFilter<"Opportunity"> | number | null
|
||||||
statusId?: Prisma.IntNullableFilter<"Opportunity"> | number | null
|
statusId?: Prisma.IntNullableFilter<"Opportunity"> | number | null
|
||||||
taxCodeId?: Prisma.IntNullableFilter<"Opportunity"> | number | null
|
taxCodeId?: Prisma.IntNullableFilter<"Opportunity"> | number | null
|
||||||
@@ -465,7 +465,7 @@ export type OpportunityWhereInput = {
|
|||||||
createdAt?: Prisma.DateTimeFilter<"Opportunity"> | Date | string
|
createdAt?: Prisma.DateTimeFilter<"Opportunity"> | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFilter<"Opportunity"> | Date | string
|
updatedAt?: Prisma.DateTimeFilter<"Opportunity"> | Date | string
|
||||||
generatedQuotes?: Prisma.GeneratedQuotesListRelationFilter
|
generatedQuotes?: Prisma.GeneratedQuotesListRelationFilter
|
||||||
type?: Prisma.XOR<Prisma.OpportunityTypeScalarRelationFilter, Prisma.OpportunityTypeWhereInput>
|
type?: Prisma.XOR<Prisma.OpportunityTypeNullableScalarRelationFilter, Prisma.OpportunityTypeWhereInput> | null
|
||||||
stage?: Prisma.XOR<Prisma.OpportunityStageNullableScalarRelationFilter, Prisma.OpportunityStageWhereInput> | null
|
stage?: Prisma.XOR<Prisma.OpportunityStageNullableScalarRelationFilter, Prisma.OpportunityStageWhereInput> | null
|
||||||
status?: Prisma.XOR<Prisma.OpportunityStatusNullableScalarRelationFilter, Prisma.OpportunityStatusWhereInput> | null
|
status?: Prisma.XOR<Prisma.OpportunityStatusNullableScalarRelationFilter, Prisma.OpportunityStatusWhereInput> | null
|
||||||
taxCode?: Prisma.XOR<Prisma.TaxCodeNullableScalarRelationFilter, Prisma.TaxCodeWhereInput> | null
|
taxCode?: Prisma.XOR<Prisma.TaxCodeNullableScalarRelationFilter, Prisma.TaxCodeWhereInput> | null
|
||||||
@@ -485,7 +485,7 @@ export type OpportunityOrderByWithRelationInput = {
|
|||||||
name?: Prisma.SortOrder
|
name?: Prisma.SortOrder
|
||||||
notes?: Prisma.SortOrderInput | Prisma.SortOrder
|
notes?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||||
oppNarrative?: Prisma.SortOrderInput | Prisma.SortOrder
|
oppNarrative?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||||
typeId?: Prisma.SortOrder
|
typeId?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||||
stageId?: Prisma.SortOrderInput | Prisma.SortOrder
|
stageId?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||||
statusId?: Prisma.SortOrderInput | Prisma.SortOrder
|
statusId?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||||
taxCodeId?: Prisma.SortOrderInput | Prisma.SortOrder
|
taxCodeId?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||||
@@ -535,7 +535,7 @@ export type OpportunityWhereUniqueInput = Prisma.AtLeast<{
|
|||||||
name?: Prisma.StringFilter<"Opportunity"> | string
|
name?: Prisma.StringFilter<"Opportunity"> | string
|
||||||
notes?: Prisma.StringNullableFilter<"Opportunity"> | string | null
|
notes?: Prisma.StringNullableFilter<"Opportunity"> | string | null
|
||||||
oppNarrative?: Prisma.StringNullableFilter<"Opportunity"> | string | null
|
oppNarrative?: Prisma.StringNullableFilter<"Opportunity"> | string | null
|
||||||
typeId?: Prisma.IntFilter<"Opportunity"> | number
|
typeId?: Prisma.IntNullableFilter<"Opportunity"> | number | null
|
||||||
stageId?: Prisma.IntNullableFilter<"Opportunity"> | number | null
|
stageId?: Prisma.IntNullableFilter<"Opportunity"> | number | null
|
||||||
statusId?: Prisma.IntNullableFilter<"Opportunity"> | number | null
|
statusId?: Prisma.IntNullableFilter<"Opportunity"> | number | null
|
||||||
taxCodeId?: Prisma.IntNullableFilter<"Opportunity"> | number | null
|
taxCodeId?: Prisma.IntNullableFilter<"Opportunity"> | number | null
|
||||||
@@ -562,7 +562,7 @@ export type OpportunityWhereUniqueInput = Prisma.AtLeast<{
|
|||||||
createdAt?: Prisma.DateTimeFilter<"Opportunity"> | Date | string
|
createdAt?: Prisma.DateTimeFilter<"Opportunity"> | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFilter<"Opportunity"> | Date | string
|
updatedAt?: Prisma.DateTimeFilter<"Opportunity"> | Date | string
|
||||||
generatedQuotes?: Prisma.GeneratedQuotesListRelationFilter
|
generatedQuotes?: Prisma.GeneratedQuotesListRelationFilter
|
||||||
type?: Prisma.XOR<Prisma.OpportunityTypeScalarRelationFilter, Prisma.OpportunityTypeWhereInput>
|
type?: Prisma.XOR<Prisma.OpportunityTypeNullableScalarRelationFilter, Prisma.OpportunityTypeWhereInput> | null
|
||||||
stage?: Prisma.XOR<Prisma.OpportunityStageNullableScalarRelationFilter, Prisma.OpportunityStageWhereInput> | null
|
stage?: Prisma.XOR<Prisma.OpportunityStageNullableScalarRelationFilter, Prisma.OpportunityStageWhereInput> | null
|
||||||
status?: Prisma.XOR<Prisma.OpportunityStatusNullableScalarRelationFilter, Prisma.OpportunityStatusWhereInput> | null
|
status?: Prisma.XOR<Prisma.OpportunityStatusNullableScalarRelationFilter, Prisma.OpportunityStatusWhereInput> | null
|
||||||
taxCode?: Prisma.XOR<Prisma.TaxCodeNullableScalarRelationFilter, Prisma.TaxCodeWhereInput> | null
|
taxCode?: Prisma.XOR<Prisma.TaxCodeNullableScalarRelationFilter, Prisma.TaxCodeWhereInput> | null
|
||||||
@@ -582,7 +582,7 @@ export type OpportunityOrderByWithAggregationInput = {
|
|||||||
name?: Prisma.SortOrder
|
name?: Prisma.SortOrder
|
||||||
notes?: Prisma.SortOrderInput | Prisma.SortOrder
|
notes?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||||
oppNarrative?: Prisma.SortOrderInput | Prisma.SortOrder
|
oppNarrative?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||||
typeId?: Prisma.SortOrder
|
typeId?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||||
stageId?: Prisma.SortOrderInput | Prisma.SortOrder
|
stageId?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||||
statusId?: Prisma.SortOrderInput | Prisma.SortOrder
|
statusId?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||||
taxCodeId?: Prisma.SortOrderInput | Prisma.SortOrder
|
taxCodeId?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||||
@@ -624,7 +624,7 @@ export type OpportunityScalarWhereWithAggregatesInput = {
|
|||||||
name?: Prisma.StringWithAggregatesFilter<"Opportunity"> | string
|
name?: Prisma.StringWithAggregatesFilter<"Opportunity"> | string
|
||||||
notes?: Prisma.StringNullableWithAggregatesFilter<"Opportunity"> | string | null
|
notes?: Prisma.StringNullableWithAggregatesFilter<"Opportunity"> | string | null
|
||||||
oppNarrative?: Prisma.StringNullableWithAggregatesFilter<"Opportunity"> | string | null
|
oppNarrative?: Prisma.StringNullableWithAggregatesFilter<"Opportunity"> | string | null
|
||||||
typeId?: Prisma.IntWithAggregatesFilter<"Opportunity"> | number
|
typeId?: Prisma.IntNullableWithAggregatesFilter<"Opportunity"> | number | null
|
||||||
stageId?: Prisma.IntNullableWithAggregatesFilter<"Opportunity"> | number | null
|
stageId?: Prisma.IntNullableWithAggregatesFilter<"Opportunity"> | number | null
|
||||||
statusId?: Prisma.IntNullableWithAggregatesFilter<"Opportunity"> | number | null
|
statusId?: Prisma.IntNullableWithAggregatesFilter<"Opportunity"> | number | null
|
||||||
taxCodeId?: Prisma.IntNullableWithAggregatesFilter<"Opportunity"> | number | null
|
taxCodeId?: Prisma.IntNullableWithAggregatesFilter<"Opportunity"> | number | null
|
||||||
@@ -674,7 +674,7 @@ export type OpportunityCreateInput = {
|
|||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
generatedQuotes?: Prisma.GeneratedQuotesCreateNestedManyWithoutOpportunityInput
|
generatedQuotes?: Prisma.GeneratedQuotesCreateNestedManyWithoutOpportunityInput
|
||||||
type: Prisma.OpportunityTypeCreateNestedOneWithoutOpportunitiesInput
|
type?: Prisma.OpportunityTypeCreateNestedOneWithoutOpportunitiesInput
|
||||||
stage?: Prisma.OpportunityStageCreateNestedOneWithoutOpportunitiesInput
|
stage?: Prisma.OpportunityStageCreateNestedOneWithoutOpportunitiesInput
|
||||||
status?: Prisma.OpportunityStatusCreateNestedOneWithoutOpportunitiesInput
|
status?: Prisma.OpportunityStatusCreateNestedOneWithoutOpportunitiesInput
|
||||||
taxCode?: Prisma.TaxCodeCreateNestedOneWithoutOpportunitiesInput
|
taxCode?: Prisma.TaxCodeCreateNestedOneWithoutOpportunitiesInput
|
||||||
@@ -694,7 +694,7 @@ export type OpportunityUncheckedCreateInput = {
|
|||||||
name: string
|
name: string
|
||||||
notes?: string | null
|
notes?: string | null
|
||||||
oppNarrative?: string | null
|
oppNarrative?: string | null
|
||||||
typeId: number
|
typeId?: number | null
|
||||||
stageId?: number | null
|
stageId?: number | null
|
||||||
statusId?: number | null
|
statusId?: number | null
|
||||||
taxCodeId?: number | null
|
taxCodeId?: number | null
|
||||||
@@ -746,7 +746,7 @@ export type OpportunityUpdateInput = {
|
|||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
generatedQuotes?: Prisma.GeneratedQuotesUpdateManyWithoutOpportunityNestedInput
|
generatedQuotes?: Prisma.GeneratedQuotesUpdateManyWithoutOpportunityNestedInput
|
||||||
type?: Prisma.OpportunityTypeUpdateOneRequiredWithoutOpportunitiesNestedInput
|
type?: Prisma.OpportunityTypeUpdateOneWithoutOpportunitiesNestedInput
|
||||||
stage?: Prisma.OpportunityStageUpdateOneWithoutOpportunitiesNestedInput
|
stage?: Prisma.OpportunityStageUpdateOneWithoutOpportunitiesNestedInput
|
||||||
status?: Prisma.OpportunityStatusUpdateOneWithoutOpportunitiesNestedInput
|
status?: Prisma.OpportunityStatusUpdateOneWithoutOpportunitiesNestedInput
|
||||||
taxCode?: Prisma.TaxCodeUpdateOneWithoutOpportunitiesNestedInput
|
taxCode?: Prisma.TaxCodeUpdateOneWithoutOpportunitiesNestedInput
|
||||||
@@ -766,7 +766,7 @@ export type OpportunityUncheckedUpdateInput = {
|
|||||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
notes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
notes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
oppNarrative?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
oppNarrative?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
typeId?: Prisma.IntFieldUpdateOperationsInput | number
|
typeId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
stageId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
stageId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
statusId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
statusId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
taxCodeId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
taxCodeId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
@@ -802,7 +802,7 @@ export type OpportunityCreateManyInput = {
|
|||||||
name: string
|
name: string
|
||||||
notes?: string | null
|
notes?: string | null
|
||||||
oppNarrative?: string | null
|
oppNarrative?: string | null
|
||||||
typeId: number
|
typeId?: number | null
|
||||||
stageId?: number | null
|
stageId?: number | null
|
||||||
statusId?: number | null
|
statusId?: number | null
|
||||||
taxCodeId?: number | null
|
taxCodeId?: number | null
|
||||||
@@ -859,7 +859,7 @@ export type OpportunityUncheckedUpdateManyInput = {
|
|||||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
notes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
notes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
oppNarrative?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
oppNarrative?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
typeId?: Prisma.IntFieldUpdateOperationsInput | number
|
typeId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
stageId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
stageId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
statusId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
statusId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
taxCodeId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
taxCodeId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
@@ -1572,7 +1572,7 @@ export type OpportunityCreateWithoutPrimarySalesRepInput = {
|
|||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
generatedQuotes?: Prisma.GeneratedQuotesCreateNestedManyWithoutOpportunityInput
|
generatedQuotes?: Prisma.GeneratedQuotesCreateNestedManyWithoutOpportunityInput
|
||||||
type: Prisma.OpportunityTypeCreateNestedOneWithoutOpportunitiesInput
|
type?: Prisma.OpportunityTypeCreateNestedOneWithoutOpportunitiesInput
|
||||||
stage?: Prisma.OpportunityStageCreateNestedOneWithoutOpportunitiesInput
|
stage?: Prisma.OpportunityStageCreateNestedOneWithoutOpportunitiesInput
|
||||||
status?: Prisma.OpportunityStatusCreateNestedOneWithoutOpportunitiesInput
|
status?: Prisma.OpportunityStatusCreateNestedOneWithoutOpportunitiesInput
|
||||||
taxCode?: Prisma.TaxCodeCreateNestedOneWithoutOpportunitiesInput
|
taxCode?: Prisma.TaxCodeCreateNestedOneWithoutOpportunitiesInput
|
||||||
@@ -1591,7 +1591,7 @@ export type OpportunityUncheckedCreateWithoutPrimarySalesRepInput = {
|
|||||||
name: string
|
name: string
|
||||||
notes?: string | null
|
notes?: string | null
|
||||||
oppNarrative?: string | null
|
oppNarrative?: string | null
|
||||||
typeId: number
|
typeId?: number | null
|
||||||
stageId?: number | null
|
stageId?: number | null
|
||||||
statusId?: number | null
|
statusId?: number | null
|
||||||
taxCodeId?: number | null
|
taxCodeId?: number | null
|
||||||
@@ -1652,7 +1652,7 @@ export type OpportunityCreateWithoutSecondarySalesRepInput = {
|
|||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
generatedQuotes?: Prisma.GeneratedQuotesCreateNestedManyWithoutOpportunityInput
|
generatedQuotes?: Prisma.GeneratedQuotesCreateNestedManyWithoutOpportunityInput
|
||||||
type: Prisma.OpportunityTypeCreateNestedOneWithoutOpportunitiesInput
|
type?: Prisma.OpportunityTypeCreateNestedOneWithoutOpportunitiesInput
|
||||||
stage?: Prisma.OpportunityStageCreateNestedOneWithoutOpportunitiesInput
|
stage?: Prisma.OpportunityStageCreateNestedOneWithoutOpportunitiesInput
|
||||||
status?: Prisma.OpportunityStatusCreateNestedOneWithoutOpportunitiesInput
|
status?: Prisma.OpportunityStatusCreateNestedOneWithoutOpportunitiesInput
|
||||||
taxCode?: Prisma.TaxCodeCreateNestedOneWithoutOpportunitiesInput
|
taxCode?: Prisma.TaxCodeCreateNestedOneWithoutOpportunitiesInput
|
||||||
@@ -1671,7 +1671,7 @@ export type OpportunityUncheckedCreateWithoutSecondarySalesRepInput = {
|
|||||||
name: string
|
name: string
|
||||||
notes?: string | null
|
notes?: string | null
|
||||||
oppNarrative?: string | null
|
oppNarrative?: string | null
|
||||||
typeId: number
|
typeId?: number | null
|
||||||
stageId?: number | null
|
stageId?: number | null
|
||||||
statusId?: number | null
|
statusId?: number | null
|
||||||
taxCodeId?: number | null
|
taxCodeId?: number | null
|
||||||
@@ -1735,7 +1735,7 @@ export type OpportunityScalarWhereInput = {
|
|||||||
name?: Prisma.StringFilter<"Opportunity"> | string
|
name?: Prisma.StringFilter<"Opportunity"> | string
|
||||||
notes?: Prisma.StringNullableFilter<"Opportunity"> | string | null
|
notes?: Prisma.StringNullableFilter<"Opportunity"> | string | null
|
||||||
oppNarrative?: Prisma.StringNullableFilter<"Opportunity"> | string | null
|
oppNarrative?: Prisma.StringNullableFilter<"Opportunity"> | string | null
|
||||||
typeId?: Prisma.IntFilter<"Opportunity"> | number
|
typeId?: Prisma.IntNullableFilter<"Opportunity"> | number | null
|
||||||
stageId?: Prisma.IntNullableFilter<"Opportunity"> | number | null
|
stageId?: Prisma.IntNullableFilter<"Opportunity"> | number | null
|
||||||
statusId?: Prisma.IntNullableFilter<"Opportunity"> | number | null
|
statusId?: Prisma.IntNullableFilter<"Opportunity"> | number | null
|
||||||
taxCodeId?: Prisma.IntNullableFilter<"Opportunity"> | number | null
|
taxCodeId?: Prisma.IntNullableFilter<"Opportunity"> | number | null
|
||||||
@@ -1801,7 +1801,7 @@ export type OpportunityCreateWithoutLocationInput = {
|
|||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
generatedQuotes?: Prisma.GeneratedQuotesCreateNestedManyWithoutOpportunityInput
|
generatedQuotes?: Prisma.GeneratedQuotesCreateNestedManyWithoutOpportunityInput
|
||||||
type: Prisma.OpportunityTypeCreateNestedOneWithoutOpportunitiesInput
|
type?: Prisma.OpportunityTypeCreateNestedOneWithoutOpportunitiesInput
|
||||||
stage?: Prisma.OpportunityStageCreateNestedOneWithoutOpportunitiesInput
|
stage?: Prisma.OpportunityStageCreateNestedOneWithoutOpportunitiesInput
|
||||||
status?: Prisma.OpportunityStatusCreateNestedOneWithoutOpportunitiesInput
|
status?: Prisma.OpportunityStatusCreateNestedOneWithoutOpportunitiesInput
|
||||||
taxCode?: Prisma.TaxCodeCreateNestedOneWithoutOpportunitiesInput
|
taxCode?: Prisma.TaxCodeCreateNestedOneWithoutOpportunitiesInput
|
||||||
@@ -1820,7 +1820,7 @@ export type OpportunityUncheckedCreateWithoutLocationInput = {
|
|||||||
name: string
|
name: string
|
||||||
notes?: string | null
|
notes?: string | null
|
||||||
oppNarrative?: string | null
|
oppNarrative?: string | null
|
||||||
typeId: number
|
typeId?: number | null
|
||||||
stageId?: number | null
|
stageId?: number | null
|
||||||
statusId?: number | null
|
statusId?: number | null
|
||||||
taxCodeId?: number | null
|
taxCodeId?: number | null
|
||||||
@@ -1897,7 +1897,7 @@ export type OpportunityCreateWithoutDepartmentInput = {
|
|||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
generatedQuotes?: Prisma.GeneratedQuotesCreateNestedManyWithoutOpportunityInput
|
generatedQuotes?: Prisma.GeneratedQuotesCreateNestedManyWithoutOpportunityInput
|
||||||
type: Prisma.OpportunityTypeCreateNestedOneWithoutOpportunitiesInput
|
type?: Prisma.OpportunityTypeCreateNestedOneWithoutOpportunitiesInput
|
||||||
stage?: Prisma.OpportunityStageCreateNestedOneWithoutOpportunitiesInput
|
stage?: Prisma.OpportunityStageCreateNestedOneWithoutOpportunitiesInput
|
||||||
status?: Prisma.OpportunityStatusCreateNestedOneWithoutOpportunitiesInput
|
status?: Prisma.OpportunityStatusCreateNestedOneWithoutOpportunitiesInput
|
||||||
taxCode?: Prisma.TaxCodeCreateNestedOneWithoutOpportunitiesInput
|
taxCode?: Prisma.TaxCodeCreateNestedOneWithoutOpportunitiesInput
|
||||||
@@ -1916,7 +1916,7 @@ export type OpportunityUncheckedCreateWithoutDepartmentInput = {
|
|||||||
name: string
|
name: string
|
||||||
notes?: string | null
|
notes?: string | null
|
||||||
oppNarrative?: string | null
|
oppNarrative?: string | null
|
||||||
typeId: number
|
typeId?: number | null
|
||||||
stageId?: number | null
|
stageId?: number | null
|
||||||
statusId?: number | null
|
statusId?: number | null
|
||||||
taxCodeId?: number | null
|
taxCodeId?: number | null
|
||||||
@@ -1993,7 +1993,7 @@ export type OpportunityCreateWithoutCompanyInput = {
|
|||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
generatedQuotes?: Prisma.GeneratedQuotesCreateNestedManyWithoutOpportunityInput
|
generatedQuotes?: Prisma.GeneratedQuotesCreateNestedManyWithoutOpportunityInput
|
||||||
type: Prisma.OpportunityTypeCreateNestedOneWithoutOpportunitiesInput
|
type?: Prisma.OpportunityTypeCreateNestedOneWithoutOpportunitiesInput
|
||||||
stage?: Prisma.OpportunityStageCreateNestedOneWithoutOpportunitiesInput
|
stage?: Prisma.OpportunityStageCreateNestedOneWithoutOpportunitiesInput
|
||||||
status?: Prisma.OpportunityStatusCreateNestedOneWithoutOpportunitiesInput
|
status?: Prisma.OpportunityStatusCreateNestedOneWithoutOpportunitiesInput
|
||||||
taxCode?: Prisma.TaxCodeCreateNestedOneWithoutOpportunitiesInput
|
taxCode?: Prisma.TaxCodeCreateNestedOneWithoutOpportunitiesInput
|
||||||
@@ -2012,7 +2012,7 @@ export type OpportunityUncheckedCreateWithoutCompanyInput = {
|
|||||||
name: string
|
name: string
|
||||||
notes?: string | null
|
notes?: string | null
|
||||||
oppNarrative?: string | null
|
oppNarrative?: string | null
|
||||||
typeId: number
|
typeId?: number | null
|
||||||
stageId?: number | null
|
stageId?: number | null
|
||||||
statusId?: number | null
|
statusId?: number | null
|
||||||
taxCodeId?: number | null
|
taxCodeId?: number | null
|
||||||
@@ -2089,7 +2089,7 @@ export type OpportunityCreateWithoutSiteInput = {
|
|||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
generatedQuotes?: Prisma.GeneratedQuotesCreateNestedManyWithoutOpportunityInput
|
generatedQuotes?: Prisma.GeneratedQuotesCreateNestedManyWithoutOpportunityInput
|
||||||
type: Prisma.OpportunityTypeCreateNestedOneWithoutOpportunitiesInput
|
type?: Prisma.OpportunityTypeCreateNestedOneWithoutOpportunitiesInput
|
||||||
stage?: Prisma.OpportunityStageCreateNestedOneWithoutOpportunitiesInput
|
stage?: Prisma.OpportunityStageCreateNestedOneWithoutOpportunitiesInput
|
||||||
status?: Prisma.OpportunityStatusCreateNestedOneWithoutOpportunitiesInput
|
status?: Prisma.OpportunityStatusCreateNestedOneWithoutOpportunitiesInput
|
||||||
taxCode?: Prisma.TaxCodeCreateNestedOneWithoutOpportunitiesInput
|
taxCode?: Prisma.TaxCodeCreateNestedOneWithoutOpportunitiesInput
|
||||||
@@ -2108,7 +2108,7 @@ export type OpportunityUncheckedCreateWithoutSiteInput = {
|
|||||||
name: string
|
name: string
|
||||||
notes?: string | null
|
notes?: string | null
|
||||||
oppNarrative?: string | null
|
oppNarrative?: string | null
|
||||||
typeId: number
|
typeId?: number | null
|
||||||
stageId?: number | null
|
stageId?: number | null
|
||||||
statusId?: number | null
|
statusId?: number | null
|
||||||
taxCodeId?: number | null
|
taxCodeId?: number | null
|
||||||
@@ -2185,7 +2185,7 @@ export type OpportunityCreateWithoutContactInput = {
|
|||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
generatedQuotes?: Prisma.GeneratedQuotesCreateNestedManyWithoutOpportunityInput
|
generatedQuotes?: Prisma.GeneratedQuotesCreateNestedManyWithoutOpportunityInput
|
||||||
type: Prisma.OpportunityTypeCreateNestedOneWithoutOpportunitiesInput
|
type?: Prisma.OpportunityTypeCreateNestedOneWithoutOpportunitiesInput
|
||||||
stage?: Prisma.OpportunityStageCreateNestedOneWithoutOpportunitiesInput
|
stage?: Prisma.OpportunityStageCreateNestedOneWithoutOpportunitiesInput
|
||||||
status?: Prisma.OpportunityStatusCreateNestedOneWithoutOpportunitiesInput
|
status?: Prisma.OpportunityStatusCreateNestedOneWithoutOpportunitiesInput
|
||||||
taxCode?: Prisma.TaxCodeCreateNestedOneWithoutOpportunitiesInput
|
taxCode?: Prisma.TaxCodeCreateNestedOneWithoutOpportunitiesInput
|
||||||
@@ -2204,7 +2204,7 @@ export type OpportunityUncheckedCreateWithoutContactInput = {
|
|||||||
name: string
|
name: string
|
||||||
notes?: string | null
|
notes?: string | null
|
||||||
oppNarrative?: string | null
|
oppNarrative?: string | null
|
||||||
typeId: number
|
typeId?: number | null
|
||||||
stageId?: number | null
|
stageId?: number | null
|
||||||
statusId?: number | null
|
statusId?: number | null
|
||||||
taxCodeId?: number | null
|
taxCodeId?: number | null
|
||||||
@@ -2281,7 +2281,7 @@ export type OpportunityCreateWithoutProductsInput = {
|
|||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
generatedQuotes?: Prisma.GeneratedQuotesCreateNestedManyWithoutOpportunityInput
|
generatedQuotes?: Prisma.GeneratedQuotesCreateNestedManyWithoutOpportunityInput
|
||||||
type: Prisma.OpportunityTypeCreateNestedOneWithoutOpportunitiesInput
|
type?: Prisma.OpportunityTypeCreateNestedOneWithoutOpportunitiesInput
|
||||||
stage?: Prisma.OpportunityStageCreateNestedOneWithoutOpportunitiesInput
|
stage?: Prisma.OpportunityStageCreateNestedOneWithoutOpportunitiesInput
|
||||||
status?: Prisma.OpportunityStatusCreateNestedOneWithoutOpportunitiesInput
|
status?: Prisma.OpportunityStatusCreateNestedOneWithoutOpportunitiesInput
|
||||||
taxCode?: Prisma.TaxCodeCreateNestedOneWithoutOpportunitiesInput
|
taxCode?: Prisma.TaxCodeCreateNestedOneWithoutOpportunitiesInput
|
||||||
@@ -2300,7 +2300,7 @@ export type OpportunityUncheckedCreateWithoutProductsInput = {
|
|||||||
name: string
|
name: string
|
||||||
notes?: string | null
|
notes?: string | null
|
||||||
oppNarrative?: string | null
|
oppNarrative?: string | null
|
||||||
typeId: number
|
typeId?: number | null
|
||||||
stageId?: number | null
|
stageId?: number | null
|
||||||
statusId?: number | null
|
statusId?: number | null
|
||||||
taxCodeId?: number | null
|
taxCodeId?: number | null
|
||||||
@@ -2367,7 +2367,7 @@ export type OpportunityUpdateWithoutProductsInput = {
|
|||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
generatedQuotes?: Prisma.GeneratedQuotesUpdateManyWithoutOpportunityNestedInput
|
generatedQuotes?: Prisma.GeneratedQuotesUpdateManyWithoutOpportunityNestedInput
|
||||||
type?: Prisma.OpportunityTypeUpdateOneRequiredWithoutOpportunitiesNestedInput
|
type?: Prisma.OpportunityTypeUpdateOneWithoutOpportunitiesNestedInput
|
||||||
stage?: Prisma.OpportunityStageUpdateOneWithoutOpportunitiesNestedInput
|
stage?: Prisma.OpportunityStageUpdateOneWithoutOpportunitiesNestedInput
|
||||||
status?: Prisma.OpportunityStatusUpdateOneWithoutOpportunitiesNestedInput
|
status?: Prisma.OpportunityStatusUpdateOneWithoutOpportunitiesNestedInput
|
||||||
taxCode?: Prisma.TaxCodeUpdateOneWithoutOpportunitiesNestedInput
|
taxCode?: Prisma.TaxCodeUpdateOneWithoutOpportunitiesNestedInput
|
||||||
@@ -2386,7 +2386,7 @@ export type OpportunityUncheckedUpdateWithoutProductsInput = {
|
|||||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
notes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
notes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
oppNarrative?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
oppNarrative?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
typeId?: Prisma.IntFieldUpdateOperationsInput | number
|
typeId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
stageId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
stageId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
statusId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
statusId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
taxCodeId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
taxCodeId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
@@ -2437,7 +2437,7 @@ export type OpportunityCreateWithoutStageInput = {
|
|||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
generatedQuotes?: Prisma.GeneratedQuotesCreateNestedManyWithoutOpportunityInput
|
generatedQuotes?: Prisma.GeneratedQuotesCreateNestedManyWithoutOpportunityInput
|
||||||
type: Prisma.OpportunityTypeCreateNestedOneWithoutOpportunitiesInput
|
type?: Prisma.OpportunityTypeCreateNestedOneWithoutOpportunitiesInput
|
||||||
status?: Prisma.OpportunityStatusCreateNestedOneWithoutOpportunitiesInput
|
status?: Prisma.OpportunityStatusCreateNestedOneWithoutOpportunitiesInput
|
||||||
taxCode?: Prisma.TaxCodeCreateNestedOneWithoutOpportunitiesInput
|
taxCode?: Prisma.TaxCodeCreateNestedOneWithoutOpportunitiesInput
|
||||||
primarySalesRep?: Prisma.UserCreateNestedOneWithoutOpportunitiesInput
|
primarySalesRep?: Prisma.UserCreateNestedOneWithoutOpportunitiesInput
|
||||||
@@ -2456,7 +2456,7 @@ export type OpportunityUncheckedCreateWithoutStageInput = {
|
|||||||
name: string
|
name: string
|
||||||
notes?: string | null
|
notes?: string | null
|
||||||
oppNarrative?: string | null
|
oppNarrative?: string | null
|
||||||
typeId: number
|
typeId?: number | null
|
||||||
statusId?: number | null
|
statusId?: number | null
|
||||||
taxCodeId?: number | null
|
taxCodeId?: number | null
|
||||||
interest?: $Enums.OpportunityInterest | null
|
interest?: $Enums.OpportunityInterest | null
|
||||||
@@ -2629,7 +2629,7 @@ export type OpportunityCreateWithoutStatusInput = {
|
|||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
generatedQuotes?: Prisma.GeneratedQuotesCreateNestedManyWithoutOpportunityInput
|
generatedQuotes?: Prisma.GeneratedQuotesCreateNestedManyWithoutOpportunityInput
|
||||||
type: Prisma.OpportunityTypeCreateNestedOneWithoutOpportunitiesInput
|
type?: Prisma.OpportunityTypeCreateNestedOneWithoutOpportunitiesInput
|
||||||
stage?: Prisma.OpportunityStageCreateNestedOneWithoutOpportunitiesInput
|
stage?: Prisma.OpportunityStageCreateNestedOneWithoutOpportunitiesInput
|
||||||
taxCode?: Prisma.TaxCodeCreateNestedOneWithoutOpportunitiesInput
|
taxCode?: Prisma.TaxCodeCreateNestedOneWithoutOpportunitiesInput
|
||||||
primarySalesRep?: Prisma.UserCreateNestedOneWithoutOpportunitiesInput
|
primarySalesRep?: Prisma.UserCreateNestedOneWithoutOpportunitiesInput
|
||||||
@@ -2648,7 +2648,7 @@ export type OpportunityUncheckedCreateWithoutStatusInput = {
|
|||||||
name: string
|
name: string
|
||||||
notes?: string | null
|
notes?: string | null
|
||||||
oppNarrative?: string | null
|
oppNarrative?: string | null
|
||||||
typeId: number
|
typeId?: number | null
|
||||||
stageId?: number | null
|
stageId?: number | null
|
||||||
taxCodeId?: number | null
|
taxCodeId?: number | null
|
||||||
interest?: $Enums.OpportunityInterest | null
|
interest?: $Enums.OpportunityInterest | null
|
||||||
@@ -2724,7 +2724,7 @@ export type OpportunityCreateWithoutGeneratedQuotesInput = {
|
|||||||
eneteredBy: string
|
eneteredBy: string
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
type: Prisma.OpportunityTypeCreateNestedOneWithoutOpportunitiesInput
|
type?: Prisma.OpportunityTypeCreateNestedOneWithoutOpportunitiesInput
|
||||||
stage?: Prisma.OpportunityStageCreateNestedOneWithoutOpportunitiesInput
|
stage?: Prisma.OpportunityStageCreateNestedOneWithoutOpportunitiesInput
|
||||||
status?: Prisma.OpportunityStatusCreateNestedOneWithoutOpportunitiesInput
|
status?: Prisma.OpportunityStatusCreateNestedOneWithoutOpportunitiesInput
|
||||||
taxCode?: Prisma.TaxCodeCreateNestedOneWithoutOpportunitiesInput
|
taxCode?: Prisma.TaxCodeCreateNestedOneWithoutOpportunitiesInput
|
||||||
@@ -2744,7 +2744,7 @@ export type OpportunityUncheckedCreateWithoutGeneratedQuotesInput = {
|
|||||||
name: string
|
name: string
|
||||||
notes?: string | null
|
notes?: string | null
|
||||||
oppNarrative?: string | null
|
oppNarrative?: string | null
|
||||||
typeId: number
|
typeId?: number | null
|
||||||
stageId?: number | null
|
stageId?: number | null
|
||||||
statusId?: number | null
|
statusId?: number | null
|
||||||
taxCodeId?: number | null
|
taxCodeId?: number | null
|
||||||
@@ -2810,7 +2810,7 @@ export type OpportunityUpdateWithoutGeneratedQuotesInput = {
|
|||||||
eneteredBy?: Prisma.StringFieldUpdateOperationsInput | string
|
eneteredBy?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
type?: Prisma.OpportunityTypeUpdateOneRequiredWithoutOpportunitiesNestedInput
|
type?: Prisma.OpportunityTypeUpdateOneWithoutOpportunitiesNestedInput
|
||||||
stage?: Prisma.OpportunityStageUpdateOneWithoutOpportunitiesNestedInput
|
stage?: Prisma.OpportunityStageUpdateOneWithoutOpportunitiesNestedInput
|
||||||
status?: Prisma.OpportunityStatusUpdateOneWithoutOpportunitiesNestedInput
|
status?: Prisma.OpportunityStatusUpdateOneWithoutOpportunitiesNestedInput
|
||||||
taxCode?: Prisma.TaxCodeUpdateOneWithoutOpportunitiesNestedInput
|
taxCode?: Prisma.TaxCodeUpdateOneWithoutOpportunitiesNestedInput
|
||||||
@@ -2830,7 +2830,7 @@ export type OpportunityUncheckedUpdateWithoutGeneratedQuotesInput = {
|
|||||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
notes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
notes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
oppNarrative?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
oppNarrative?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
typeId?: Prisma.IntFieldUpdateOperationsInput | number
|
typeId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
stageId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
stageId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
statusId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
statusId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
taxCodeId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
taxCodeId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
@@ -2881,7 +2881,7 @@ export type OpportunityCreateWithoutTaxCodeInput = {
|
|||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
generatedQuotes?: Prisma.GeneratedQuotesCreateNestedManyWithoutOpportunityInput
|
generatedQuotes?: Prisma.GeneratedQuotesCreateNestedManyWithoutOpportunityInput
|
||||||
type: Prisma.OpportunityTypeCreateNestedOneWithoutOpportunitiesInput
|
type?: Prisma.OpportunityTypeCreateNestedOneWithoutOpportunitiesInput
|
||||||
stage?: Prisma.OpportunityStageCreateNestedOneWithoutOpportunitiesInput
|
stage?: Prisma.OpportunityStageCreateNestedOneWithoutOpportunitiesInput
|
||||||
status?: Prisma.OpportunityStatusCreateNestedOneWithoutOpportunitiesInput
|
status?: Prisma.OpportunityStatusCreateNestedOneWithoutOpportunitiesInput
|
||||||
primarySalesRep?: Prisma.UserCreateNestedOneWithoutOpportunitiesInput
|
primarySalesRep?: Prisma.UserCreateNestedOneWithoutOpportunitiesInput
|
||||||
@@ -2900,7 +2900,7 @@ export type OpportunityUncheckedCreateWithoutTaxCodeInput = {
|
|||||||
name: string
|
name: string
|
||||||
notes?: string | null
|
notes?: string | null
|
||||||
oppNarrative?: string | null
|
oppNarrative?: string | null
|
||||||
typeId: number
|
typeId?: number | null
|
||||||
stageId?: number | null
|
stageId?: number | null
|
||||||
statusId?: number | null
|
statusId?: number | null
|
||||||
interest?: $Enums.OpportunityInterest | null
|
interest?: $Enums.OpportunityInterest | null
|
||||||
@@ -2961,7 +2961,7 @@ export type OpportunityCreateManyPrimarySalesRepInput = {
|
|||||||
name: string
|
name: string
|
||||||
notes?: string | null
|
notes?: string | null
|
||||||
oppNarrative?: string | null
|
oppNarrative?: string | null
|
||||||
typeId: number
|
typeId?: number | null
|
||||||
stageId?: number | null
|
stageId?: number | null
|
||||||
statusId?: number | null
|
statusId?: number | null
|
||||||
taxCodeId?: number | null
|
taxCodeId?: number | null
|
||||||
@@ -2994,7 +2994,7 @@ export type OpportunityCreateManySecondarySalesRepInput = {
|
|||||||
name: string
|
name: string
|
||||||
notes?: string | null
|
notes?: string | null
|
||||||
oppNarrative?: string | null
|
oppNarrative?: string | null
|
||||||
typeId: number
|
typeId?: number | null
|
||||||
stageId?: number | null
|
stageId?: number | null
|
||||||
statusId?: number | null
|
statusId?: number | null
|
||||||
taxCodeId?: number | null
|
taxCodeId?: number | null
|
||||||
@@ -3043,7 +3043,7 @@ export type OpportunityUpdateWithoutPrimarySalesRepInput = {
|
|||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
generatedQuotes?: Prisma.GeneratedQuotesUpdateManyWithoutOpportunityNestedInput
|
generatedQuotes?: Prisma.GeneratedQuotesUpdateManyWithoutOpportunityNestedInput
|
||||||
type?: Prisma.OpportunityTypeUpdateOneRequiredWithoutOpportunitiesNestedInput
|
type?: Prisma.OpportunityTypeUpdateOneWithoutOpportunitiesNestedInput
|
||||||
stage?: Prisma.OpportunityStageUpdateOneWithoutOpportunitiesNestedInput
|
stage?: Prisma.OpportunityStageUpdateOneWithoutOpportunitiesNestedInput
|
||||||
status?: Prisma.OpportunityStatusUpdateOneWithoutOpportunitiesNestedInput
|
status?: Prisma.OpportunityStatusUpdateOneWithoutOpportunitiesNestedInput
|
||||||
taxCode?: Prisma.TaxCodeUpdateOneWithoutOpportunitiesNestedInput
|
taxCode?: Prisma.TaxCodeUpdateOneWithoutOpportunitiesNestedInput
|
||||||
@@ -3062,7 +3062,7 @@ export type OpportunityUncheckedUpdateWithoutPrimarySalesRepInput = {
|
|||||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
notes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
notes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
oppNarrative?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
oppNarrative?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
typeId?: Prisma.IntFieldUpdateOperationsInput | number
|
typeId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
stageId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
stageId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
statusId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
statusId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
taxCodeId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
taxCodeId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
@@ -3097,7 +3097,7 @@ export type OpportunityUncheckedUpdateManyWithoutPrimarySalesRepInput = {
|
|||||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
notes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
notes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
oppNarrative?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
oppNarrative?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
typeId?: Prisma.IntFieldUpdateOperationsInput | number
|
typeId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
stageId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
stageId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
statusId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
statusId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
taxCodeId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
taxCodeId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
@@ -3146,7 +3146,7 @@ export type OpportunityUpdateWithoutSecondarySalesRepInput = {
|
|||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
generatedQuotes?: Prisma.GeneratedQuotesUpdateManyWithoutOpportunityNestedInput
|
generatedQuotes?: Prisma.GeneratedQuotesUpdateManyWithoutOpportunityNestedInput
|
||||||
type?: Prisma.OpportunityTypeUpdateOneRequiredWithoutOpportunitiesNestedInput
|
type?: Prisma.OpportunityTypeUpdateOneWithoutOpportunitiesNestedInput
|
||||||
stage?: Prisma.OpportunityStageUpdateOneWithoutOpportunitiesNestedInput
|
stage?: Prisma.OpportunityStageUpdateOneWithoutOpportunitiesNestedInput
|
||||||
status?: Prisma.OpportunityStatusUpdateOneWithoutOpportunitiesNestedInput
|
status?: Prisma.OpportunityStatusUpdateOneWithoutOpportunitiesNestedInput
|
||||||
taxCode?: Prisma.TaxCodeUpdateOneWithoutOpportunitiesNestedInput
|
taxCode?: Prisma.TaxCodeUpdateOneWithoutOpportunitiesNestedInput
|
||||||
@@ -3165,7 +3165,7 @@ export type OpportunityUncheckedUpdateWithoutSecondarySalesRepInput = {
|
|||||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
notes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
notes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
oppNarrative?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
oppNarrative?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
typeId?: Prisma.IntFieldUpdateOperationsInput | number
|
typeId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
stageId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
stageId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
statusId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
statusId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
taxCodeId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
taxCodeId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
@@ -3200,7 +3200,7 @@ export type OpportunityUncheckedUpdateManyWithoutSecondarySalesRepInput = {
|
|||||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
notes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
notes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
oppNarrative?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
oppNarrative?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
typeId?: Prisma.IntFieldUpdateOperationsInput | number
|
typeId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
stageId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
stageId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
statusId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
statusId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
taxCodeId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
taxCodeId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
@@ -3233,7 +3233,7 @@ export type OpportunityCreateManyLocationInput = {
|
|||||||
name: string
|
name: string
|
||||||
notes?: string | null
|
notes?: string | null
|
||||||
oppNarrative?: string | null
|
oppNarrative?: string | null
|
||||||
typeId: number
|
typeId?: number | null
|
||||||
stageId?: number | null
|
stageId?: number | null
|
||||||
statusId?: number | null
|
statusId?: number | null
|
||||||
taxCodeId?: number | null
|
taxCodeId?: number | null
|
||||||
@@ -3282,7 +3282,7 @@ export type OpportunityUpdateWithoutLocationInput = {
|
|||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
generatedQuotes?: Prisma.GeneratedQuotesUpdateManyWithoutOpportunityNestedInput
|
generatedQuotes?: Prisma.GeneratedQuotesUpdateManyWithoutOpportunityNestedInput
|
||||||
type?: Prisma.OpportunityTypeUpdateOneRequiredWithoutOpportunitiesNestedInput
|
type?: Prisma.OpportunityTypeUpdateOneWithoutOpportunitiesNestedInput
|
||||||
stage?: Prisma.OpportunityStageUpdateOneWithoutOpportunitiesNestedInput
|
stage?: Prisma.OpportunityStageUpdateOneWithoutOpportunitiesNestedInput
|
||||||
status?: Prisma.OpportunityStatusUpdateOneWithoutOpportunitiesNestedInput
|
status?: Prisma.OpportunityStatusUpdateOneWithoutOpportunitiesNestedInput
|
||||||
taxCode?: Prisma.TaxCodeUpdateOneWithoutOpportunitiesNestedInput
|
taxCode?: Prisma.TaxCodeUpdateOneWithoutOpportunitiesNestedInput
|
||||||
@@ -3301,7 +3301,7 @@ export type OpportunityUncheckedUpdateWithoutLocationInput = {
|
|||||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
notes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
notes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
oppNarrative?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
oppNarrative?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
typeId?: Prisma.IntFieldUpdateOperationsInput | number
|
typeId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
stageId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
stageId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
statusId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
statusId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
taxCodeId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
taxCodeId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
@@ -3336,7 +3336,7 @@ export type OpportunityUncheckedUpdateManyWithoutLocationInput = {
|
|||||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
notes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
notes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
oppNarrative?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
oppNarrative?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
typeId?: Prisma.IntFieldUpdateOperationsInput | number
|
typeId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
stageId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
stageId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
statusId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
statusId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
taxCodeId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
taxCodeId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
@@ -3369,7 +3369,7 @@ export type OpportunityCreateManyDepartmentInput = {
|
|||||||
name: string
|
name: string
|
||||||
notes?: string | null
|
notes?: string | null
|
||||||
oppNarrative?: string | null
|
oppNarrative?: string | null
|
||||||
typeId: number
|
typeId?: number | null
|
||||||
stageId?: number | null
|
stageId?: number | null
|
||||||
statusId?: number | null
|
statusId?: number | null
|
||||||
taxCodeId?: number | null
|
taxCodeId?: number | null
|
||||||
@@ -3418,7 +3418,7 @@ export type OpportunityUpdateWithoutDepartmentInput = {
|
|||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
generatedQuotes?: Prisma.GeneratedQuotesUpdateManyWithoutOpportunityNestedInput
|
generatedQuotes?: Prisma.GeneratedQuotesUpdateManyWithoutOpportunityNestedInput
|
||||||
type?: Prisma.OpportunityTypeUpdateOneRequiredWithoutOpportunitiesNestedInput
|
type?: Prisma.OpportunityTypeUpdateOneWithoutOpportunitiesNestedInput
|
||||||
stage?: Prisma.OpportunityStageUpdateOneWithoutOpportunitiesNestedInput
|
stage?: Prisma.OpportunityStageUpdateOneWithoutOpportunitiesNestedInput
|
||||||
status?: Prisma.OpportunityStatusUpdateOneWithoutOpportunitiesNestedInput
|
status?: Prisma.OpportunityStatusUpdateOneWithoutOpportunitiesNestedInput
|
||||||
taxCode?: Prisma.TaxCodeUpdateOneWithoutOpportunitiesNestedInput
|
taxCode?: Prisma.TaxCodeUpdateOneWithoutOpportunitiesNestedInput
|
||||||
@@ -3437,7 +3437,7 @@ export type OpportunityUncheckedUpdateWithoutDepartmentInput = {
|
|||||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
notes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
notes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
oppNarrative?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
oppNarrative?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
typeId?: Prisma.IntFieldUpdateOperationsInput | number
|
typeId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
stageId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
stageId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
statusId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
statusId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
taxCodeId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
taxCodeId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
@@ -3472,7 +3472,7 @@ export type OpportunityUncheckedUpdateManyWithoutDepartmentInput = {
|
|||||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
notes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
notes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
oppNarrative?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
oppNarrative?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
typeId?: Prisma.IntFieldUpdateOperationsInput | number
|
typeId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
stageId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
stageId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
statusId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
statusId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
taxCodeId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
taxCodeId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
@@ -3505,7 +3505,7 @@ export type OpportunityCreateManyCompanyInput = {
|
|||||||
name: string
|
name: string
|
||||||
notes?: string | null
|
notes?: string | null
|
||||||
oppNarrative?: string | null
|
oppNarrative?: string | null
|
||||||
typeId: number
|
typeId?: number | null
|
||||||
stageId?: number | null
|
stageId?: number | null
|
||||||
statusId?: number | null
|
statusId?: number | null
|
||||||
taxCodeId?: number | null
|
taxCodeId?: number | null
|
||||||
@@ -3554,7 +3554,7 @@ export type OpportunityUpdateWithoutCompanyInput = {
|
|||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
generatedQuotes?: Prisma.GeneratedQuotesUpdateManyWithoutOpportunityNestedInput
|
generatedQuotes?: Prisma.GeneratedQuotesUpdateManyWithoutOpportunityNestedInput
|
||||||
type?: Prisma.OpportunityTypeUpdateOneRequiredWithoutOpportunitiesNestedInput
|
type?: Prisma.OpportunityTypeUpdateOneWithoutOpportunitiesNestedInput
|
||||||
stage?: Prisma.OpportunityStageUpdateOneWithoutOpportunitiesNestedInput
|
stage?: Prisma.OpportunityStageUpdateOneWithoutOpportunitiesNestedInput
|
||||||
status?: Prisma.OpportunityStatusUpdateOneWithoutOpportunitiesNestedInput
|
status?: Prisma.OpportunityStatusUpdateOneWithoutOpportunitiesNestedInput
|
||||||
taxCode?: Prisma.TaxCodeUpdateOneWithoutOpportunitiesNestedInput
|
taxCode?: Prisma.TaxCodeUpdateOneWithoutOpportunitiesNestedInput
|
||||||
@@ -3573,7 +3573,7 @@ export type OpportunityUncheckedUpdateWithoutCompanyInput = {
|
|||||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
notes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
notes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
oppNarrative?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
oppNarrative?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
typeId?: Prisma.IntFieldUpdateOperationsInput | number
|
typeId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
stageId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
stageId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
statusId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
statusId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
taxCodeId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
taxCodeId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
@@ -3608,7 +3608,7 @@ export type OpportunityUncheckedUpdateManyWithoutCompanyInput = {
|
|||||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
notes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
notes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
oppNarrative?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
oppNarrative?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
typeId?: Prisma.IntFieldUpdateOperationsInput | number
|
typeId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
stageId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
stageId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
statusId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
statusId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
taxCodeId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
taxCodeId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
@@ -3641,7 +3641,7 @@ export type OpportunityCreateManySiteInput = {
|
|||||||
name: string
|
name: string
|
||||||
notes?: string | null
|
notes?: string | null
|
||||||
oppNarrative?: string | null
|
oppNarrative?: string | null
|
||||||
typeId: number
|
typeId?: number | null
|
||||||
stageId?: number | null
|
stageId?: number | null
|
||||||
statusId?: number | null
|
statusId?: number | null
|
||||||
taxCodeId?: number | null
|
taxCodeId?: number | null
|
||||||
@@ -3690,7 +3690,7 @@ export type OpportunityUpdateWithoutSiteInput = {
|
|||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
generatedQuotes?: Prisma.GeneratedQuotesUpdateManyWithoutOpportunityNestedInput
|
generatedQuotes?: Prisma.GeneratedQuotesUpdateManyWithoutOpportunityNestedInput
|
||||||
type?: Prisma.OpportunityTypeUpdateOneRequiredWithoutOpportunitiesNestedInput
|
type?: Prisma.OpportunityTypeUpdateOneWithoutOpportunitiesNestedInput
|
||||||
stage?: Prisma.OpportunityStageUpdateOneWithoutOpportunitiesNestedInput
|
stage?: Prisma.OpportunityStageUpdateOneWithoutOpportunitiesNestedInput
|
||||||
status?: Prisma.OpportunityStatusUpdateOneWithoutOpportunitiesNestedInput
|
status?: Prisma.OpportunityStatusUpdateOneWithoutOpportunitiesNestedInput
|
||||||
taxCode?: Prisma.TaxCodeUpdateOneWithoutOpportunitiesNestedInput
|
taxCode?: Prisma.TaxCodeUpdateOneWithoutOpportunitiesNestedInput
|
||||||
@@ -3709,7 +3709,7 @@ export type OpportunityUncheckedUpdateWithoutSiteInput = {
|
|||||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
notes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
notes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
oppNarrative?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
oppNarrative?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
typeId?: Prisma.IntFieldUpdateOperationsInput | number
|
typeId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
stageId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
stageId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
statusId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
statusId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
taxCodeId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
taxCodeId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
@@ -3744,7 +3744,7 @@ export type OpportunityUncheckedUpdateManyWithoutSiteInput = {
|
|||||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
notes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
notes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
oppNarrative?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
oppNarrative?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
typeId?: Prisma.IntFieldUpdateOperationsInput | number
|
typeId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
stageId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
stageId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
statusId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
statusId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
taxCodeId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
taxCodeId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
@@ -3777,7 +3777,7 @@ export type OpportunityCreateManyContactInput = {
|
|||||||
name: string
|
name: string
|
||||||
notes?: string | null
|
notes?: string | null
|
||||||
oppNarrative?: string | null
|
oppNarrative?: string | null
|
||||||
typeId: number
|
typeId?: number | null
|
||||||
stageId?: number | null
|
stageId?: number | null
|
||||||
statusId?: number | null
|
statusId?: number | null
|
||||||
taxCodeId?: number | null
|
taxCodeId?: number | null
|
||||||
@@ -3826,7 +3826,7 @@ export type OpportunityUpdateWithoutContactInput = {
|
|||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
generatedQuotes?: Prisma.GeneratedQuotesUpdateManyWithoutOpportunityNestedInput
|
generatedQuotes?: Prisma.GeneratedQuotesUpdateManyWithoutOpportunityNestedInput
|
||||||
type?: Prisma.OpportunityTypeUpdateOneRequiredWithoutOpportunitiesNestedInput
|
type?: Prisma.OpportunityTypeUpdateOneWithoutOpportunitiesNestedInput
|
||||||
stage?: Prisma.OpportunityStageUpdateOneWithoutOpportunitiesNestedInput
|
stage?: Prisma.OpportunityStageUpdateOneWithoutOpportunitiesNestedInput
|
||||||
status?: Prisma.OpportunityStatusUpdateOneWithoutOpportunitiesNestedInput
|
status?: Prisma.OpportunityStatusUpdateOneWithoutOpportunitiesNestedInput
|
||||||
taxCode?: Prisma.TaxCodeUpdateOneWithoutOpportunitiesNestedInput
|
taxCode?: Prisma.TaxCodeUpdateOneWithoutOpportunitiesNestedInput
|
||||||
@@ -3845,7 +3845,7 @@ export type OpportunityUncheckedUpdateWithoutContactInput = {
|
|||||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
notes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
notes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
oppNarrative?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
oppNarrative?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
typeId?: Prisma.IntFieldUpdateOperationsInput | number
|
typeId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
stageId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
stageId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
statusId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
statusId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
taxCodeId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
taxCodeId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
@@ -3880,7 +3880,7 @@ export type OpportunityUncheckedUpdateManyWithoutContactInput = {
|
|||||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
notes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
notes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
oppNarrative?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
oppNarrative?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
typeId?: Prisma.IntFieldUpdateOperationsInput | number
|
typeId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
stageId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
stageId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
statusId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
statusId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
taxCodeId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
taxCodeId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
@@ -3913,7 +3913,7 @@ export type OpportunityCreateManyStageInput = {
|
|||||||
name: string
|
name: string
|
||||||
notes?: string | null
|
notes?: string | null
|
||||||
oppNarrative?: string | null
|
oppNarrative?: string | null
|
||||||
typeId: number
|
typeId?: number | null
|
||||||
statusId?: number | null
|
statusId?: number | null
|
||||||
taxCodeId?: number | null
|
taxCodeId?: number | null
|
||||||
interest?: $Enums.OpportunityInterest | null
|
interest?: $Enums.OpportunityInterest | null
|
||||||
@@ -3962,7 +3962,7 @@ export type OpportunityUpdateWithoutStageInput = {
|
|||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
generatedQuotes?: Prisma.GeneratedQuotesUpdateManyWithoutOpportunityNestedInput
|
generatedQuotes?: Prisma.GeneratedQuotesUpdateManyWithoutOpportunityNestedInput
|
||||||
type?: Prisma.OpportunityTypeUpdateOneRequiredWithoutOpportunitiesNestedInput
|
type?: Prisma.OpportunityTypeUpdateOneWithoutOpportunitiesNestedInput
|
||||||
status?: Prisma.OpportunityStatusUpdateOneWithoutOpportunitiesNestedInput
|
status?: Prisma.OpportunityStatusUpdateOneWithoutOpportunitiesNestedInput
|
||||||
taxCode?: Prisma.TaxCodeUpdateOneWithoutOpportunitiesNestedInput
|
taxCode?: Prisma.TaxCodeUpdateOneWithoutOpportunitiesNestedInput
|
||||||
primarySalesRep?: Prisma.UserUpdateOneWithoutOpportunitiesNestedInput
|
primarySalesRep?: Prisma.UserUpdateOneWithoutOpportunitiesNestedInput
|
||||||
@@ -3981,7 +3981,7 @@ export type OpportunityUncheckedUpdateWithoutStageInput = {
|
|||||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
notes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
notes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
oppNarrative?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
oppNarrative?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
typeId?: Prisma.IntFieldUpdateOperationsInput | number
|
typeId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
statusId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
statusId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
taxCodeId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
taxCodeId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
interest?: Prisma.NullableEnumOpportunityInterestFieldUpdateOperationsInput | $Enums.OpportunityInterest | null
|
interest?: Prisma.NullableEnumOpportunityInterestFieldUpdateOperationsInput | $Enums.OpportunityInterest | null
|
||||||
@@ -4016,7 +4016,7 @@ export type OpportunityUncheckedUpdateManyWithoutStageInput = {
|
|||||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
notes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
notes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
oppNarrative?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
oppNarrative?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
typeId?: Prisma.IntFieldUpdateOperationsInput | number
|
typeId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
statusId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
statusId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
taxCodeId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
taxCodeId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
interest?: Prisma.NullableEnumOpportunityInterestFieldUpdateOperationsInput | $Enums.OpportunityInterest | null
|
interest?: Prisma.NullableEnumOpportunityInterestFieldUpdateOperationsInput | $Enums.OpportunityInterest | null
|
||||||
@@ -4185,7 +4185,7 @@ export type OpportunityCreateManyStatusInput = {
|
|||||||
name: string
|
name: string
|
||||||
notes?: string | null
|
notes?: string | null
|
||||||
oppNarrative?: string | null
|
oppNarrative?: string | null
|
||||||
typeId: number
|
typeId?: number | null
|
||||||
stageId?: number | null
|
stageId?: number | null
|
||||||
taxCodeId?: number | null
|
taxCodeId?: number | null
|
||||||
interest?: $Enums.OpportunityInterest | null
|
interest?: $Enums.OpportunityInterest | null
|
||||||
@@ -4234,7 +4234,7 @@ export type OpportunityUpdateWithoutStatusInput = {
|
|||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
generatedQuotes?: Prisma.GeneratedQuotesUpdateManyWithoutOpportunityNestedInput
|
generatedQuotes?: Prisma.GeneratedQuotesUpdateManyWithoutOpportunityNestedInput
|
||||||
type?: Prisma.OpportunityTypeUpdateOneRequiredWithoutOpportunitiesNestedInput
|
type?: Prisma.OpportunityTypeUpdateOneWithoutOpportunitiesNestedInput
|
||||||
stage?: Prisma.OpportunityStageUpdateOneWithoutOpportunitiesNestedInput
|
stage?: Prisma.OpportunityStageUpdateOneWithoutOpportunitiesNestedInput
|
||||||
taxCode?: Prisma.TaxCodeUpdateOneWithoutOpportunitiesNestedInput
|
taxCode?: Prisma.TaxCodeUpdateOneWithoutOpportunitiesNestedInput
|
||||||
primarySalesRep?: Prisma.UserUpdateOneWithoutOpportunitiesNestedInput
|
primarySalesRep?: Prisma.UserUpdateOneWithoutOpportunitiesNestedInput
|
||||||
@@ -4253,7 +4253,7 @@ export type OpportunityUncheckedUpdateWithoutStatusInput = {
|
|||||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
notes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
notes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
oppNarrative?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
oppNarrative?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
typeId?: Prisma.IntFieldUpdateOperationsInput | number
|
typeId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
stageId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
stageId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
taxCodeId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
taxCodeId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
interest?: Prisma.NullableEnumOpportunityInterestFieldUpdateOperationsInput | $Enums.OpportunityInterest | null
|
interest?: Prisma.NullableEnumOpportunityInterestFieldUpdateOperationsInput | $Enums.OpportunityInterest | null
|
||||||
@@ -4288,7 +4288,7 @@ export type OpportunityUncheckedUpdateManyWithoutStatusInput = {
|
|||||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
notes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
notes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
oppNarrative?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
oppNarrative?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
typeId?: Prisma.IntFieldUpdateOperationsInput | number
|
typeId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
stageId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
stageId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
taxCodeId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
taxCodeId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
interest?: Prisma.NullableEnumOpportunityInterestFieldUpdateOperationsInput | $Enums.OpportunityInterest | null
|
interest?: Prisma.NullableEnumOpportunityInterestFieldUpdateOperationsInput | $Enums.OpportunityInterest | null
|
||||||
@@ -4321,7 +4321,7 @@ export type OpportunityCreateManyTaxCodeInput = {
|
|||||||
name: string
|
name: string
|
||||||
notes?: string | null
|
notes?: string | null
|
||||||
oppNarrative?: string | null
|
oppNarrative?: string | null
|
||||||
typeId: number
|
typeId?: number | null
|
||||||
stageId?: number | null
|
stageId?: number | null
|
||||||
statusId?: number | null
|
statusId?: number | null
|
||||||
interest?: $Enums.OpportunityInterest | null
|
interest?: $Enums.OpportunityInterest | null
|
||||||
@@ -4370,7 +4370,7 @@ export type OpportunityUpdateWithoutTaxCodeInput = {
|
|||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
generatedQuotes?: Prisma.GeneratedQuotesUpdateManyWithoutOpportunityNestedInput
|
generatedQuotes?: Prisma.GeneratedQuotesUpdateManyWithoutOpportunityNestedInput
|
||||||
type?: Prisma.OpportunityTypeUpdateOneRequiredWithoutOpportunitiesNestedInput
|
type?: Prisma.OpportunityTypeUpdateOneWithoutOpportunitiesNestedInput
|
||||||
stage?: Prisma.OpportunityStageUpdateOneWithoutOpportunitiesNestedInput
|
stage?: Prisma.OpportunityStageUpdateOneWithoutOpportunitiesNestedInput
|
||||||
status?: Prisma.OpportunityStatusUpdateOneWithoutOpportunitiesNestedInput
|
status?: Prisma.OpportunityStatusUpdateOneWithoutOpportunitiesNestedInput
|
||||||
primarySalesRep?: Prisma.UserUpdateOneWithoutOpportunitiesNestedInput
|
primarySalesRep?: Prisma.UserUpdateOneWithoutOpportunitiesNestedInput
|
||||||
@@ -4389,7 +4389,7 @@ export type OpportunityUncheckedUpdateWithoutTaxCodeInput = {
|
|||||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
notes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
notes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
oppNarrative?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
oppNarrative?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
typeId?: Prisma.IntFieldUpdateOperationsInput | number
|
typeId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
stageId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
stageId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
statusId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
statusId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
interest?: Prisma.NullableEnumOpportunityInterestFieldUpdateOperationsInput | $Enums.OpportunityInterest | null
|
interest?: Prisma.NullableEnumOpportunityInterestFieldUpdateOperationsInput | $Enums.OpportunityInterest | null
|
||||||
@@ -4424,7 +4424,7 @@ export type OpportunityUncheckedUpdateManyWithoutTaxCodeInput = {
|
|||||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
notes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
notes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
oppNarrative?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
oppNarrative?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
typeId?: Prisma.IntFieldUpdateOperationsInput | number
|
typeId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
stageId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
stageId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
statusId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
statusId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
interest?: Prisma.NullableEnumOpportunityInterestFieldUpdateOperationsInput | $Enums.OpportunityInterest | null
|
interest?: Prisma.NullableEnumOpportunityInterestFieldUpdateOperationsInput | $Enums.OpportunityInterest | null
|
||||||
@@ -4524,7 +4524,7 @@ export type OpportunitySelect<ExtArgs extends runtime.Types.Extensions.InternalA
|
|||||||
createdAt?: boolean
|
createdAt?: boolean
|
||||||
updatedAt?: boolean
|
updatedAt?: boolean
|
||||||
generatedQuotes?: boolean | Prisma.Opportunity$generatedQuotesArgs<ExtArgs>
|
generatedQuotes?: boolean | Prisma.Opportunity$generatedQuotesArgs<ExtArgs>
|
||||||
type?: boolean | Prisma.OpportunityTypeDefaultArgs<ExtArgs>
|
type?: boolean | Prisma.Opportunity$typeArgs<ExtArgs>
|
||||||
stage?: boolean | Prisma.Opportunity$stageArgs<ExtArgs>
|
stage?: boolean | Prisma.Opportunity$stageArgs<ExtArgs>
|
||||||
status?: boolean | Prisma.Opportunity$statusArgs<ExtArgs>
|
status?: boolean | Prisma.Opportunity$statusArgs<ExtArgs>
|
||||||
taxCode?: boolean | Prisma.Opportunity$taxCodeArgs<ExtArgs>
|
taxCode?: boolean | Prisma.Opportunity$taxCodeArgs<ExtArgs>
|
||||||
@@ -4571,7 +4571,7 @@ export type OpportunitySelectCreateManyAndReturn<ExtArgs extends runtime.Types.E
|
|||||||
eneteredBy?: boolean
|
eneteredBy?: boolean
|
||||||
createdAt?: boolean
|
createdAt?: boolean
|
||||||
updatedAt?: boolean
|
updatedAt?: boolean
|
||||||
type?: boolean | Prisma.OpportunityTypeDefaultArgs<ExtArgs>
|
type?: boolean | Prisma.Opportunity$typeArgs<ExtArgs>
|
||||||
stage?: boolean | Prisma.Opportunity$stageArgs<ExtArgs>
|
stage?: boolean | Prisma.Opportunity$stageArgs<ExtArgs>
|
||||||
status?: boolean | Prisma.Opportunity$statusArgs<ExtArgs>
|
status?: boolean | Prisma.Opportunity$statusArgs<ExtArgs>
|
||||||
taxCode?: boolean | Prisma.Opportunity$taxCodeArgs<ExtArgs>
|
taxCode?: boolean | Prisma.Opportunity$taxCodeArgs<ExtArgs>
|
||||||
@@ -4616,7 +4616,7 @@ export type OpportunitySelectUpdateManyAndReturn<ExtArgs extends runtime.Types.E
|
|||||||
eneteredBy?: boolean
|
eneteredBy?: boolean
|
||||||
createdAt?: boolean
|
createdAt?: boolean
|
||||||
updatedAt?: boolean
|
updatedAt?: boolean
|
||||||
type?: boolean | Prisma.OpportunityTypeDefaultArgs<ExtArgs>
|
type?: boolean | Prisma.Opportunity$typeArgs<ExtArgs>
|
||||||
stage?: boolean | Prisma.Opportunity$stageArgs<ExtArgs>
|
stage?: boolean | Prisma.Opportunity$stageArgs<ExtArgs>
|
||||||
status?: boolean | Prisma.Opportunity$statusArgs<ExtArgs>
|
status?: boolean | Prisma.Opportunity$statusArgs<ExtArgs>
|
||||||
taxCode?: boolean | Prisma.Opportunity$taxCodeArgs<ExtArgs>
|
taxCode?: boolean | Prisma.Opportunity$taxCodeArgs<ExtArgs>
|
||||||
@@ -4666,7 +4666,7 @@ export type OpportunitySelectScalar = {
|
|||||||
export type OpportunityOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "uid" | "name" | "notes" | "oppNarrative" | "typeId" | "stageId" | "statusId" | "taxCodeId" | "interest" | "probability" | "source" | "primarySalesRepId" | "secondarySalesRepId" | "companyId" | "contactId" | "siteId" | "customerPO" | "locationId" | "departmentId" | "expectedCloseDate" | "pipelineChangeDate" | "dateBecameLead" | "closedDate" | "closedFlag" | "closedById" | "productSequence" | "updatedBy" | "eneteredBy" | "createdAt" | "updatedAt", ExtArgs["result"]["opportunity"]>
|
export type OpportunityOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "uid" | "name" | "notes" | "oppNarrative" | "typeId" | "stageId" | "statusId" | "taxCodeId" | "interest" | "probability" | "source" | "primarySalesRepId" | "secondarySalesRepId" | "companyId" | "contactId" | "siteId" | "customerPO" | "locationId" | "departmentId" | "expectedCloseDate" | "pipelineChangeDate" | "dateBecameLead" | "closedDate" | "closedFlag" | "closedById" | "productSequence" | "updatedBy" | "eneteredBy" | "createdAt" | "updatedAt", ExtArgs["result"]["opportunity"]>
|
||||||
export type OpportunityInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
export type OpportunityInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||||
generatedQuotes?: boolean | Prisma.Opportunity$generatedQuotesArgs<ExtArgs>
|
generatedQuotes?: boolean | Prisma.Opportunity$generatedQuotesArgs<ExtArgs>
|
||||||
type?: boolean | Prisma.OpportunityTypeDefaultArgs<ExtArgs>
|
type?: boolean | Prisma.Opportunity$typeArgs<ExtArgs>
|
||||||
stage?: boolean | Prisma.Opportunity$stageArgs<ExtArgs>
|
stage?: boolean | Prisma.Opportunity$stageArgs<ExtArgs>
|
||||||
status?: boolean | Prisma.Opportunity$statusArgs<ExtArgs>
|
status?: boolean | Prisma.Opportunity$statusArgs<ExtArgs>
|
||||||
taxCode?: boolean | Prisma.Opportunity$taxCodeArgs<ExtArgs>
|
taxCode?: boolean | Prisma.Opportunity$taxCodeArgs<ExtArgs>
|
||||||
@@ -4681,7 +4681,7 @@ export type OpportunityInclude<ExtArgs extends runtime.Types.Extensions.Internal
|
|||||||
_count?: boolean | Prisma.OpportunityCountOutputTypeDefaultArgs<ExtArgs>
|
_count?: boolean | Prisma.OpportunityCountOutputTypeDefaultArgs<ExtArgs>
|
||||||
}
|
}
|
||||||
export type OpportunityIncludeCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
export type OpportunityIncludeCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||||
type?: boolean | Prisma.OpportunityTypeDefaultArgs<ExtArgs>
|
type?: boolean | Prisma.Opportunity$typeArgs<ExtArgs>
|
||||||
stage?: boolean | Prisma.Opportunity$stageArgs<ExtArgs>
|
stage?: boolean | Prisma.Opportunity$stageArgs<ExtArgs>
|
||||||
status?: boolean | Prisma.Opportunity$statusArgs<ExtArgs>
|
status?: boolean | Prisma.Opportunity$statusArgs<ExtArgs>
|
||||||
taxCode?: boolean | Prisma.Opportunity$taxCodeArgs<ExtArgs>
|
taxCode?: boolean | Prisma.Opportunity$taxCodeArgs<ExtArgs>
|
||||||
@@ -4694,7 +4694,7 @@ export type OpportunityIncludeCreateManyAndReturn<ExtArgs extends runtime.Types.
|
|||||||
department?: boolean | Prisma.Opportunity$departmentArgs<ExtArgs>
|
department?: boolean | Prisma.Opportunity$departmentArgs<ExtArgs>
|
||||||
}
|
}
|
||||||
export type OpportunityIncludeUpdateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
export type OpportunityIncludeUpdateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||||
type?: boolean | Prisma.OpportunityTypeDefaultArgs<ExtArgs>
|
type?: boolean | Prisma.Opportunity$typeArgs<ExtArgs>
|
||||||
stage?: boolean | Prisma.Opportunity$stageArgs<ExtArgs>
|
stage?: boolean | Prisma.Opportunity$stageArgs<ExtArgs>
|
||||||
status?: boolean | Prisma.Opportunity$statusArgs<ExtArgs>
|
status?: boolean | Prisma.Opportunity$statusArgs<ExtArgs>
|
||||||
taxCode?: boolean | Prisma.Opportunity$taxCodeArgs<ExtArgs>
|
taxCode?: boolean | Prisma.Opportunity$taxCodeArgs<ExtArgs>
|
||||||
@@ -4711,7 +4711,7 @@ export type $OpportunityPayload<ExtArgs extends runtime.Types.Extensions.Interna
|
|||||||
name: "Opportunity"
|
name: "Opportunity"
|
||||||
objects: {
|
objects: {
|
||||||
generatedQuotes: Prisma.$GeneratedQuotesPayload<ExtArgs>[]
|
generatedQuotes: Prisma.$GeneratedQuotesPayload<ExtArgs>[]
|
||||||
type: Prisma.$OpportunityTypePayload<ExtArgs>
|
type: Prisma.$OpportunityTypePayload<ExtArgs> | null
|
||||||
stage: Prisma.$OpportunityStagePayload<ExtArgs> | null
|
stage: Prisma.$OpportunityStagePayload<ExtArgs> | null
|
||||||
status: Prisma.$OpportunityStatusPayload<ExtArgs> | null
|
status: Prisma.$OpportunityStatusPayload<ExtArgs> | null
|
||||||
taxCode: Prisma.$TaxCodePayload<ExtArgs> | null
|
taxCode: Prisma.$TaxCodePayload<ExtArgs> | null
|
||||||
@@ -4730,7 +4730,7 @@ export type $OpportunityPayload<ExtArgs extends runtime.Types.Extensions.Interna
|
|||||||
name: string
|
name: string
|
||||||
notes: string | null
|
notes: string | null
|
||||||
oppNarrative: string | null
|
oppNarrative: string | null
|
||||||
typeId: number
|
typeId: number | null
|
||||||
stageId: number | null
|
stageId: number | null
|
||||||
statusId: number | null
|
statusId: number | null
|
||||||
taxCodeId: number | null
|
taxCodeId: number | null
|
||||||
@@ -5151,7 +5151,7 @@ readonly fields: OpportunityFieldRefs;
|
|||||||
export interface Prisma__OpportunityClient<T, Null = never, ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs, GlobalOmitOptions = {}> extends Prisma.PrismaPromise<T> {
|
export interface Prisma__OpportunityClient<T, Null = never, ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs, GlobalOmitOptions = {}> extends Prisma.PrismaPromise<T> {
|
||||||
readonly [Symbol.toStringTag]: "PrismaPromise"
|
readonly [Symbol.toStringTag]: "PrismaPromise"
|
||||||
generatedQuotes<T extends Prisma.Opportunity$generatedQuotesArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Opportunity$generatedQuotesArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$GeneratedQuotesPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
|
generatedQuotes<T extends Prisma.Opportunity$generatedQuotesArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Opportunity$generatedQuotesArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$GeneratedQuotesPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
|
||||||
type<T extends Prisma.OpportunityTypeDefaultArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.OpportunityTypeDefaultArgs<ExtArgs>>): Prisma.Prisma__OpportunityTypeClient<runtime.Types.Result.GetResult<Prisma.$OpportunityTypePayload<ExtArgs>, T, "findUniqueOrThrow", GlobalOmitOptions> | Null, Null, ExtArgs, GlobalOmitOptions>
|
type<T extends Prisma.Opportunity$typeArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Opportunity$typeArgs<ExtArgs>>): Prisma.Prisma__OpportunityTypeClient<runtime.Types.Result.GetResult<Prisma.$OpportunityTypePayload<ExtArgs>, T, "findUniqueOrThrow", GlobalOmitOptions> | null, null, ExtArgs, GlobalOmitOptions>
|
||||||
stage<T extends Prisma.Opportunity$stageArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Opportunity$stageArgs<ExtArgs>>): Prisma.Prisma__OpportunityStageClient<runtime.Types.Result.GetResult<Prisma.$OpportunityStagePayload<ExtArgs>, T, "findUniqueOrThrow", GlobalOmitOptions> | null, null, ExtArgs, GlobalOmitOptions>
|
stage<T extends Prisma.Opportunity$stageArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Opportunity$stageArgs<ExtArgs>>): Prisma.Prisma__OpportunityStageClient<runtime.Types.Result.GetResult<Prisma.$OpportunityStagePayload<ExtArgs>, T, "findUniqueOrThrow", GlobalOmitOptions> | null, null, ExtArgs, GlobalOmitOptions>
|
||||||
status<T extends Prisma.Opportunity$statusArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Opportunity$statusArgs<ExtArgs>>): Prisma.Prisma__OpportunityStatusClient<runtime.Types.Result.GetResult<Prisma.$OpportunityStatusPayload<ExtArgs>, T, "findUniqueOrThrow", GlobalOmitOptions> | null, null, ExtArgs, GlobalOmitOptions>
|
status<T extends Prisma.Opportunity$statusArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Opportunity$statusArgs<ExtArgs>>): Prisma.Prisma__OpportunityStatusClient<runtime.Types.Result.GetResult<Prisma.$OpportunityStatusPayload<ExtArgs>, T, "findUniqueOrThrow", GlobalOmitOptions> | null, null, ExtArgs, GlobalOmitOptions>
|
||||||
taxCode<T extends Prisma.Opportunity$taxCodeArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Opportunity$taxCodeArgs<ExtArgs>>): Prisma.Prisma__TaxCodeClient<runtime.Types.Result.GetResult<Prisma.$TaxCodePayload<ExtArgs>, T, "findUniqueOrThrow", GlobalOmitOptions> | null, null, ExtArgs, GlobalOmitOptions>
|
taxCode<T extends Prisma.Opportunity$taxCodeArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Opportunity$taxCodeArgs<ExtArgs>>): Prisma.Prisma__TaxCodeClient<runtime.Types.Result.GetResult<Prisma.$TaxCodePayload<ExtArgs>, T, "findUniqueOrThrow", GlobalOmitOptions> | null, null, ExtArgs, GlobalOmitOptions>
|
||||||
@@ -5647,6 +5647,25 @@ export type Opportunity$generatedQuotesArgs<ExtArgs extends runtime.Types.Extens
|
|||||||
distinct?: Prisma.GeneratedQuotesScalarFieldEnum | Prisma.GeneratedQuotesScalarFieldEnum[]
|
distinct?: Prisma.GeneratedQuotesScalarFieldEnum | Prisma.GeneratedQuotesScalarFieldEnum[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opportunity.type
|
||||||
|
*/
|
||||||
|
export type Opportunity$typeArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||||
|
/**
|
||||||
|
* Select specific fields to fetch from the OpportunityType
|
||||||
|
*/
|
||||||
|
select?: Prisma.OpportunityTypeSelect<ExtArgs> | null
|
||||||
|
/**
|
||||||
|
* Omit specific fields from the OpportunityType
|
||||||
|
*/
|
||||||
|
omit?: Prisma.OpportunityTypeOmit<ExtArgs> | null
|
||||||
|
/**
|
||||||
|
* Choose, which related nodes to fetch as well
|
||||||
|
*/
|
||||||
|
include?: Prisma.OpportunityTypeInclude<ExtArgs> | null
|
||||||
|
where?: Prisma.OpportunityTypeWhereInput
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Opportunity.stage
|
* Opportunity.stage
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -20,6 +20,11 @@ spec:
|
|||||||
env:
|
env:
|
||||||
- name: MANAGER_SOCKET_URL
|
- name: MANAGER_SOCKET_URL
|
||||||
value: "http://optima-api.optima.svc.cluster.local:8671"
|
value: "http://optima-api.optima.svc.cluster.local:8671"
|
||||||
|
- name: API_DATABASE_URL
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: api-env-secret
|
||||||
|
key: DATABASE_URL
|
||||||
envFrom:
|
envFrom:
|
||||||
- secretRef:
|
- secretRef:
|
||||||
name: api-env-secret
|
name: api-env-secret
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
import { defineConfig, env } from 'prisma/config'
|
export default {
|
||||||
|
|
||||||
export default defineConfig({
|
|
||||||
schema: 'prisma/schema.prisma',
|
schema: 'prisma/schema.prisma',
|
||||||
migrations: {
|
migrations: {
|
||||||
path: 'prisma/migrations',
|
path: 'prisma/migrations',
|
||||||
},
|
},
|
||||||
datasource: {
|
datasource: {
|
||||||
url: env('DATABASE_URL'),
|
url: process.env.DATABASE_URL,
|
||||||
},
|
},
|
||||||
})
|
}
|
||||||
@@ -1032,8 +1032,8 @@ model Opportunity {
|
|||||||
|
|
||||||
generatedQuotes GeneratedQuotes[]
|
generatedQuotes GeneratedQuotes[]
|
||||||
|
|
||||||
typeId Int
|
typeId Int?
|
||||||
type OpportunityType @relation(fields: [typeId], references: [id])
|
type OpportunityType? @relation(fields: [typeId], references: [id])
|
||||||
|
|
||||||
stageId Int?
|
stageId Int?
|
||||||
stage OpportunityStage? @relation(fields: [stageId], references: [id])
|
stage OpportunityStage? @relation(fields: [stageId], references: [id])
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { companies } from "../../../managers/companies";
|
|||||||
import { apiResponse } from "../../../modules/api-utils/apiResponse";
|
import { apiResponse } from "../../../modules/api-utils/apiResponse";
|
||||||
import { ContentfulStatusCode } from "hono/utils/http-status";
|
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||||
import { authMiddleware } from "../../middleware/authorization";
|
import { authMiddleware } from "../../middleware/authorization";
|
||||||
|
import GenericError from "../../../Errors/GenericError";
|
||||||
import { processObjectValuePerms } from "../../../modules/permission-utils/processObjectPermissions";
|
import { processObjectValuePerms } from "../../../modules/permission-utils/processObjectPermissions";
|
||||||
|
|
||||||
/* /v1/company/companies/[id] */
|
/* /v1/company/companies/[id] */
|
||||||
@@ -12,18 +13,37 @@ export default createRoute(
|
|||||||
["/companies/:identifier"],
|
["/companies/:identifier"],
|
||||||
|
|
||||||
async (c) => {
|
async (c) => {
|
||||||
const company = await companies.fetch(c.req.param("identifier"));
|
const company = await companies.fetch(c.req.param("identifier") as string);
|
||||||
const user = c.get("user");
|
const includeAddress = c.req.query("includeAddress") === "true";
|
||||||
const includeAddress =
|
|
||||||
c.req.query("includeAddress") === "true" &&
|
|
||||||
!!user &&
|
|
||||||
(await user.hasPermission("company.fetch.address"));
|
|
||||||
const includePrimaryContact =
|
const includePrimaryContact =
|
||||||
c.req.query("includePrimaryContact") === "true";
|
c.req.query("includePrimaryContact") === "true";
|
||||||
const includeAllContacts =
|
const includeAllContacts = c.req.query("includeAllContacts") === "true";
|
||||||
c.req.query("includeAllContacts") === "true" &&
|
|
||||||
!!user &&
|
console.log(company.toJson({ includeAddress, includePrimaryContact, includeAllContacts }));
|
||||||
(await user.hasPermission("company.fetch.contacts"));
|
|
||||||
|
// 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 companyData = company.toJson({
|
const companyData = company.toJson({
|
||||||
includeAddress,
|
includeAddress,
|
||||||
@@ -36,13 +56,6 @@ export default createRoute(
|
|||||||
c.get("user"),
|
c.get("user"),
|
||||||
);
|
);
|
||||||
|
|
||||||
// cw_Data fields were already gated by the explicit permission checks above
|
|
||||||
// (company.fetch.contacts / company.fetch.address). Re-attach them so they
|
|
||||||
// are not silently dropped by field-level gating on obj.company.cw_Data.
|
|
||||||
if (companyData.cw_Data && Object.keys(companyData.cw_Data).length > 0) {
|
|
||||||
(gatedData as any).cw_Data = companyData.cw_Data;
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = apiResponse.successful(
|
const response = apiResponse.successful(
|
||||||
"Company Fetched Successfully!",
|
"Company Fetched Successfully!",
|
||||||
gatedData,
|
gatedData,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { createRoute } from "../../modules/api-utils/createRoute";
|
|||||||
import { apiResponse } from "../../modules/api-utils/apiResponse";
|
import { apiResponse } from "../../modules/api-utils/apiResponse";
|
||||||
import { ContentfulStatusCode } from "hono/utils/http-status";
|
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||||
import { authMiddleware } from "../middleware/authorization";
|
import { authMiddleware } from "../middleware/authorization";
|
||||||
import { getBoss } from "../../workert";
|
import { getBoss } from "../../boss-instance";
|
||||||
import { WorkerQueue } from "../../modules/workers/queues";
|
import { WorkerQueue } from "../../modules/workers/queues";
|
||||||
|
|
||||||
/* POST /v1/cw/sync/full */
|
/* POST /v1/cw/sync/full */
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
/**
|
||||||
|
* Shared PgBoss singleton — kept in its own module to break circular imports
|
||||||
|
* between workert.ts and the worker modules that call getBoss().
|
||||||
|
*/
|
||||||
|
import { PgBoss } from "pg-boss";
|
||||||
|
|
||||||
|
function makePgBossUrl(rawUrl: string): string {
|
||||||
|
try {
|
||||||
|
const u = new URL(rawUrl);
|
||||||
|
// 30-second statement timeout to prevent individual SQL queries from
|
||||||
|
// hanging indefinitely if the DB server stops responding mid-query.
|
||||||
|
u.searchParams.set("options", "-c statement_timeout=30000");
|
||||||
|
return u.toString();
|
||||||
|
} catch {
|
||||||
|
return rawUrl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const boss = new PgBoss({
|
||||||
|
connectionString: makePgBossUrl(process.env.DATABASE_URL!),
|
||||||
|
connectionTimeoutMillis: 15_000,
|
||||||
|
});
|
||||||
|
|
||||||
|
boss.on("error", (err) => {
|
||||||
|
console.error("[worker] PgBoss error", err);
|
||||||
|
});
|
||||||
|
|
||||||
|
export function getBoss(): PgBoss {
|
||||||
|
return boss;
|
||||||
|
}
|
||||||
@@ -274,7 +274,7 @@ export class CompanyController {
|
|||||||
(ci) => ci.type?.name === "Email"
|
(ci) => ci.type?.name === "Email"
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
id: contact.id,
|
cwId: contact.id,
|
||||||
firstName: contact.firstName,
|
firstName: contact.firstName,
|
||||||
lastName: contact.lastName,
|
lastName: contact.lastName,
|
||||||
inactive: contact.inactiveFlag ?? false,
|
inactive: contact.inactiveFlag ?? false,
|
||||||
@@ -303,16 +303,41 @@ export class CompanyController {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (opts?.includePrimaryContact && this.cw_Data?.defaultContact) {
|
if (opts?.includePrimaryContact) {
|
||||||
|
if (this.cw_Data?.defaultContact) {
|
||||||
cw_Data.primaryContact = this._serializeContact(
|
cw_Data.primaryContact = this._serializeContact(
|
||||||
this.cw_Data.defaultContact
|
this.cw_Data.defaultContact
|
||||||
);
|
);
|
||||||
|
} else if (this._defaultContact) {
|
||||||
|
const c = this._defaultContact;
|
||||||
|
cw_Data.primaryContact = {
|
||||||
|
cwId: c.id,
|
||||||
|
firstName: c.firstName,
|
||||||
|
lastName: c.lastName,
|
||||||
|
inactive: !c.active,
|
||||||
|
title: c.title ?? null,
|
||||||
|
phone: c.phone ?? null,
|
||||||
|
email: c.email ?? null,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (opts?.includeAllContacts && this.cw_Data?.allContacts) {
|
if (opts?.includeAllContacts) {
|
||||||
|
if (this.cw_Data?.allContacts) {
|
||||||
cw_Data.allContacts = this.cw_Data.allContacts.map((c) =>
|
cw_Data.allContacts = this.cw_Data.allContacts.map((c) =>
|
||||||
this._serializeContact(c)
|
this._serializeContact(c)
|
||||||
);
|
);
|
||||||
|
} else if (this._contacts.length > 0) {
|
||||||
|
cw_Data.allContacts = this._contacts.map((c) => ({
|
||||||
|
cwId: c.id,
|
||||||
|
firstName: c.firstName,
|
||||||
|
lastName: c.lastName,
|
||||||
|
inactive: !c.active,
|
||||||
|
title: c.title ?? null,
|
||||||
|
phone: c.phone ?? null,
|
||||||
|
email: c.email ?? null,
|
||||||
|
}));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -76,6 +76,20 @@ function mapRatingNameToInterest(
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatOpportunityContactName(
|
||||||
|
firstName?: string | null,
|
||||||
|
lastName?: string | null
|
||||||
|
): string {
|
||||||
|
const first = (firstName ?? "").trim();
|
||||||
|
const last = (lastName ?? "").trim();
|
||||||
|
|
||||||
|
if (first && last.toLowerCase() === "contact") {
|
||||||
|
return first;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${first} ${last}`.trim();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Opportunity Controller
|
* Opportunity Controller
|
||||||
*
|
*
|
||||||
@@ -290,7 +304,7 @@ export class OpportunityController {
|
|||||||
| null
|
| null
|
||||||
| undefined;
|
| undefined;
|
||||||
this.contactName = (data as any).contactName ?? (contactRel
|
this.contactName = (data as any).contactName ?? (contactRel
|
||||||
? `${contactRel.firstName} ${contactRel.lastName}`.trim()
|
? formatOpportunityContactName(contactRel.firstName, contactRel.lastName)
|
||||||
: null);
|
: null);
|
||||||
|
|
||||||
// Site
|
// Site
|
||||||
@@ -674,7 +688,7 @@ export class OpportunityController {
|
|||||||
id: contact.id,
|
id: contact.id,
|
||||||
contact: {
|
contact: {
|
||||||
id: contact.id,
|
id: contact.id,
|
||||||
name: `${contact.firstName} ${contact.lastName}`.trim(),
|
name: formatOpportunityContactName(contact.firstName, contact.lastName),
|
||||||
},
|
},
|
||||||
company: contact.company
|
company: contact.company
|
||||||
? {
|
? {
|
||||||
|
|||||||
+2
-1
@@ -6,7 +6,8 @@ import { events } from "./modules/globalEvents";
|
|||||||
import { setupEventDebugger } from "./modules/logging/eventDebugger";
|
import { setupEventDebugger } from "./modules/logging/eventDebugger";
|
||||||
import { signPermissions } from "./modules/permission-utils/signPermissions";
|
import { signPermissions } from "./modules/permission-utils/signPermissions";
|
||||||
import { RoleController } from "./controllers/RoleController";
|
import { RoleController } from "./controllers/RoleController";
|
||||||
import { initializeWorkerSystem, getBoss } from "./workert";
|
import { initializeWorkerSystem } from "./workert";
|
||||||
|
import { getBoss } from "./boss-instance";
|
||||||
import { WorkerQueue } from "./modules/workers/queues";
|
import { WorkerQueue } from "./modules/workers/queues";
|
||||||
import { enqueueIncrementalSync } from "./modules/workers/incremental-sync";
|
import { enqueueIncrementalSync } from "./modules/workers/incremental-sync";
|
||||||
import { startCommsServer } from "./modules/workers/coms";
|
import { startCommsServer } from "./modules/workers/coms";
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ export const opportunities = {
|
|||||||
|
|
||||||
// Resolve optional local FKs — nullify any that don't exist locally yet
|
// Resolve optional local FKs — nullify any that don't exist locally yet
|
||||||
// (the sync may be behind; these are all nullable in the schema)
|
// (the sync may be behind; these are all nullable in the schema)
|
||||||
const [companyExists, contactExists, siteExists] = await Promise.all([
|
const [companyExists, contactExists, siteExists, typeExists] = await Promise.all([
|
||||||
cwData.company?.id
|
cwData.company?.id
|
||||||
? prisma.company.findFirst({ where: { id: cwData.company.id }, select: { id: true } })
|
? prisma.company.findFirst({ where: { id: cwData.company.id }, select: { id: true } })
|
||||||
: null,
|
: null,
|
||||||
@@ -56,16 +56,21 @@ export const opportunities = {
|
|||||||
mapped.siteId != null
|
mapped.siteId != null
|
||||||
? prisma.companyAddress.findFirst({ where: { id: mapped.siteId }, select: { id: true } })
|
? prisma.companyAddress.findFirst({ where: { id: mapped.siteId }, select: { id: true } })
|
||||||
: null,
|
: null,
|
||||||
|
mapped.typeId != null
|
||||||
|
? prisma.opportunityType.findFirst({ where: { id: mapped.typeId }, select: { id: true } })
|
||||||
|
: null,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const companyId = companyExists?.id ?? null;
|
const companyId = companyExists?.id ?? null;
|
||||||
const contactId = contactExists?.id ?? null;
|
const contactId = contactExists?.id ?? null;
|
||||||
const siteId = siteExists?.id ?? null;
|
const siteId = siteExists?.id ?? null;
|
||||||
|
const typeId = typeExists?.id ?? null;
|
||||||
|
|
||||||
const record = await prisma.opportunity.create({
|
const record = await prisma.opportunity.create({
|
||||||
data: {
|
data: {
|
||||||
id: cwData.id,
|
id: cwData.id,
|
||||||
...mapped,
|
...mapped,
|
||||||
|
typeId,
|
||||||
companyId,
|
companyId,
|
||||||
contactId,
|
contactId,
|
||||||
siteId,
|
siteId,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import PdfPrinter from "pdfmake/src/Printer";
|
import PdfPrinter from "pdfmake/src/Printer";
|
||||||
import { readFileSync } from "node:fs";
|
import { existsSync, readFileSync } from "node:fs";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
|
|
||||||
export interface QuoteLineItem {
|
export interface QuoteLineItem {
|
||||||
@@ -110,7 +110,26 @@ const COMPANY = {
|
|||||||
|
|
||||||
const DEFAULT_LOGO_PATH = join(process.cwd(), "logo.png");
|
const DEFAULT_LOGO_PATH = join(process.cwd(), "logo.png");
|
||||||
|
|
||||||
const fontDir = join(process.cwd(), "node_modules/pdfmake/build/fonts/Roboto");
|
function resolveRobotoFontDir(): string {
|
||||||
|
const candidates = [
|
||||||
|
join(process.cwd(), "node_modules/pdfmake/build/fonts/Roboto"),
|
||||||
|
join(import.meta.dir, "../../../node_modules/pdfmake/build/fonts/Roboto"),
|
||||||
|
join("/app/node_modules/pdfmake/build/fonts/Roboto"),
|
||||||
|
join("/app/api/node_modules/pdfmake/build/fonts/Roboto"),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const dir of candidates) {
|
||||||
|
if (existsSync(join(dir, "Roboto-Medium.ttf"))) {
|
||||||
|
return dir;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(
|
||||||
|
`[pdf] Could not locate pdfmake Roboto fonts. Checked: ${candidates.join(", ")}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const fontDir = resolveRobotoFontDir();
|
||||||
const fonts = {
|
const fonts = {
|
||||||
Roboto: {
|
Roboto: {
|
||||||
normal: join(fontDir, "Roboto-Regular.ttf"),
|
normal: join(fontDir, "Roboto-Regular.ttf"),
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Server } from "socket.io";
|
import { Server } from "socket.io";
|
||||||
import { events, EventTypes } from "../globalEvents";
|
import { events, EventTypes } from "../globalEvents";
|
||||||
import { WorkerQueue } from "./queues";
|
import { WorkerQueue } from "./queues";
|
||||||
|
import { reserveWorkerId } from "../../workert";
|
||||||
|
|
||||||
function emitGlobalEvent<K extends keyof EventTypes>(
|
function emitGlobalEvent<K extends keyof EventTypes>(
|
||||||
name: K,
|
name: K,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Socket } from "socket.io-client";
|
import { Socket } from "socket.io-client";
|
||||||
import { executeFullDalpuriSync, executeForcedIncrementalDalpuriSync } from "dalpuri";
|
import { executeFullDalpuriSync, executeForcedIncrementalDalpuriSync } from "dalpuri";
|
||||||
|
import { prisma } from "../../constants";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Execute a full sync from Dalpuri (ConnectWise) to the API database.
|
* Execute a full sync from Dalpuri (ConnectWise) to the API database.
|
||||||
@@ -14,5 +15,53 @@ export async function executeFullSync(_workerSocket: Socket): Promise<void> {
|
|||||||
* Called every 5 seconds via PgBoss from the API process interval.
|
* Called every 5 seconds via PgBoss from the API process interval.
|
||||||
*/
|
*/
|
||||||
export async function executeIncrementalSync(): Promise<void> {
|
export async function executeIncrementalSync(): Promise<void> {
|
||||||
return executeForcedIncrementalDalpuriSync();
|
let jobRunId: string | undefined;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const run = await prisma.syncJobRun.create({
|
||||||
|
data: {
|
||||||
|
jobType: "INCREMENTAL_SYNC",
|
||||||
|
status: "RUNNING",
|
||||||
|
triggeredBy: "worker",
|
||||||
|
startedAt: new Date(),
|
||||||
|
},
|
||||||
|
select: { id: true },
|
||||||
|
});
|
||||||
|
jobRunId = run.id;
|
||||||
|
} catch (err) {
|
||||||
|
// Sync should still run even if tracking insert fails.
|
||||||
|
console.error("[sync] Failed to create incremental SyncJobRun", err);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await executeForcedIncrementalDalpuriSync({ jobRunId });
|
||||||
|
|
||||||
|
if (jobRunId) {
|
||||||
|
await prisma.syncJobRun.update({
|
||||||
|
where: { id: jobRunId },
|
||||||
|
data: {
|
||||||
|
status: "COMPLETED",
|
||||||
|
completedAt: new Date(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
if (jobRunId) {
|
||||||
|
const errorSummary = err instanceof Error ? err.message : String(err);
|
||||||
|
await prisma.syncJobRun
|
||||||
|
.update({
|
||||||
|
where: { id: jobRunId },
|
||||||
|
data: {
|
||||||
|
status: "FAILED",
|
||||||
|
completedAt: new Date(),
|
||||||
|
errorSummary: errorSummary.slice(0, 2000),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
// Best-effort update only.
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { getBoss } from "../../workert";
|
import { getBoss } from "../../boss-instance";
|
||||||
import { WorkerQueue } from "./queues";
|
import { WorkerQueue } from "./queues";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -6,5 +6,17 @@ import { WorkerQueue } from "./queues";
|
|||||||
* Called on an interval from the main API process so it survives worker restarts.
|
* Called on an interval from the main API process so it survives worker restarts.
|
||||||
*/
|
*/
|
||||||
export async function enqueueIncrementalSync(): Promise<void> {
|
export async function enqueueIncrementalSync(): Promise<void> {
|
||||||
await getBoss().send(WorkerQueue.DALPURI_INCREMENTAL_SYNC, {});
|
const jobId = await getBoss().send(
|
||||||
|
WorkerQueue.DALPURI_INCREMENTAL_SYNC,
|
||||||
|
{
|
||||||
|
enqueuedAt: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
singletonKey: "dalpuri-incremental-sync",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!jobId) {
|
||||||
|
console.debug("[interval] DALPURI_INCREMENTAL_SYNC already pending or active");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,8 @@ export async function createWorkerJob<T>(
|
|||||||
queueType: WorkerQueue,
|
queueType: WorkerQueue,
|
||||||
workFn: (workerSocket: Socket) => Promise<T>,
|
workFn: (workerSocket: Socket) => Promise<T>,
|
||||||
): Promise<T> {
|
): Promise<T> {
|
||||||
|
const managerUrl = process.env.MANAGER_SOCKET_URL ?? "http://localhost:8671";
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
// Request a worker ID and namespace from the manager
|
// Request a worker ID and namespace from the manager
|
||||||
socket.emit(
|
socket.emit(
|
||||||
@@ -53,7 +55,7 @@ export async function createWorkerJob<T>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Connect to the worker-specific namespace
|
// Connect to the worker-specific namespace
|
||||||
const workerSocket = io(`http://localhost:8671/worker-${workerId}`, {
|
const workerSocket = io(`${managerUrl}/worker-${workerId}`, {
|
||||||
reconnection: false,
|
reconnection: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+60
-22
@@ -1,13 +1,7 @@
|
|||||||
import { PgBoss } from "pg-boss";
|
|
||||||
import { io, Socket } from "socket.io-client";
|
import { io, Socket } from "socket.io-client";
|
||||||
import { WorkerQueue } from "./modules/workers/queues";
|
import { WorkerQueue } from "./modules/workers/queues";
|
||||||
import { setupEventDebugger } from "./modules/logging/eventDebugger";
|
import { setupEventDebugger } from "./modules/logging/eventDebugger";
|
||||||
|
import { boss, getBoss } from "./boss-instance";
|
||||||
const boss = new PgBoss(process.env.DATABASE_URL!);
|
|
||||||
|
|
||||||
boss.on("error", (err) => {
|
|
||||||
console.error("[worker] PgBoss error", err);
|
|
||||||
});
|
|
||||||
|
|
||||||
let bossStartPromise: Promise<void> | null = null;
|
let bossStartPromise: Promise<void> | null = null;
|
||||||
let reservationQueueReady = false;
|
let reservationQueueReady = false;
|
||||||
@@ -111,19 +105,25 @@ export async function reserveWorkerId(queueType: WorkerQueue): Promise<string> {
|
|||||||
|
|
||||||
async function ensureDalpuriSyncQueue(): Promise<void> {
|
async function ensureDalpuriSyncQueue(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
|
console.log("[worker] Creating DALPURI_FULL_SYNC queue...");
|
||||||
await boss.createQueue(WorkerQueue.DALPURI_FULL_SYNC);
|
await boss.createQueue(WorkerQueue.DALPURI_FULL_SYNC);
|
||||||
} catch {
|
console.log("[worker] DALPURI_FULL_SYNC queue ready");
|
||||||
// Queue may already exist; ignore to keep this idempotent.
|
} catch (err) {
|
||||||
|
console.log("[worker] DALPURI_FULL_SYNC queue already exists (or error):", (err as Error).message);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
|
console.log("[worker] Creating DALPURI_INCREMENTAL_SYNC queue...");
|
||||||
await boss.createQueue(WorkerQueue.DALPURI_INCREMENTAL_SYNC);
|
await boss.createQueue(WorkerQueue.DALPURI_INCREMENTAL_SYNC);
|
||||||
} catch {
|
console.log("[worker] DALPURI_INCREMENTAL_SYNC queue ready");
|
||||||
// Queue may already exist; ignore to keep this idempotent.
|
} catch (err) {
|
||||||
|
console.log("[worker] DALPURI_INCREMENTAL_SYNC queue already exists (or error):", (err as Error).message);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
|
console.log("[worker] Creating REFRESH_SALES_METRICS queue...");
|
||||||
await boss.createQueue(WorkerQueue.REFRESH_SALES_METRICS);
|
await boss.createQueue(WorkerQueue.REFRESH_SALES_METRICS);
|
||||||
} catch {
|
console.log("[worker] REFRESH_SALES_METRICS queue ready");
|
||||||
// Queue may already exist; ignore to keep this idempotent.
|
} catch (err) {
|
||||||
|
console.log("[worker] REFRESH_SALES_METRICS queue already exists (or error):", (err as Error).message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,14 +138,6 @@ export async function initializeWorkerSystem(): Promise<void> {
|
|||||||
console.log("[worker] Worker system initialized - ready for job enqueueing");
|
console.log("[worker] Worker system initialized - ready for job enqueueing");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the PgBoss instance for direct job enqueueing.
|
|
||||||
* Must call initializeWorkerSystem() first.
|
|
||||||
*/
|
|
||||||
export function getBoss(): PgBoss {
|
|
||||||
return boss;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (import.meta.main) {
|
if (import.meta.main) {
|
||||||
// if (Bun.env.NODE_ENV === "development") {
|
// if (Bun.env.NODE_ENV === "development") {
|
||||||
// setupEventDebugger({ processLabel: "WORKER" });
|
// setupEventDebugger({ processLabel: "WORKER" });
|
||||||
@@ -155,14 +147,33 @@ if (import.meta.main) {
|
|||||||
console.log(
|
console.log(
|
||||||
`[worker] Connecting to PgBoss on DATABASE_URL and SocketIO on ${process.env.MANAGER_SOCKET_URL ?? "http://localhost:8671"}`
|
`[worker] Connecting to PgBoss on DATABASE_URL and SocketIO on ${process.env.MANAGER_SOCKET_URL ?? "http://localhost:8671"}`
|
||||||
);
|
);
|
||||||
|
console.log(`[worker] DATABASE_URL set: ${!!process.env.DATABASE_URL}`);
|
||||||
|
|
||||||
// Ensure PgBoss is connected and queues exist
|
// Ensure PgBoss is connected and queues exist
|
||||||
await ensureBossStarted();
|
console.log("[worker] Starting PgBoss...");
|
||||||
|
try {
|
||||||
|
await Promise.race([
|
||||||
|
ensureBossStarted(),
|
||||||
|
new Promise<never>((_, reject) =>
|
||||||
|
setTimeout(() => reject(new Error("boss.start() timed out after 30s")), 30_000)
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("[worker] FATAL: PgBoss failed to start:", err);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
console.log("[worker] PgBoss started successfully");
|
||||||
|
console.log("[worker] Ensuring sync queues...");
|
||||||
await ensureDalpuriSyncQueue();
|
await ensureDalpuriSyncQueue();
|
||||||
|
console.log("[worker] Sync queues ready");
|
||||||
|
|
||||||
// Register job handler for DALPURI_FULL_SYNC
|
// Register job handler for DALPURI_FULL_SYNC
|
||||||
|
console.log("[worker] Importing sync-manager...");
|
||||||
const { enqueueDalpuriFullSync } = await import("./modules/workers/sync-manager");
|
const { enqueueDalpuriFullSync } = await import("./modules/workers/sync-manager");
|
||||||
|
console.log("[worker] Importing dalpuri-sync...");
|
||||||
const { executeIncrementalSync } = await import("./modules/workers/dalpuri-sync");
|
const { executeIncrementalSync } = await import("./modules/workers/dalpuri-sync");
|
||||||
|
console.log("[worker] Importing incremental-sync...");
|
||||||
|
const { enqueueIncrementalSync } = await import("./modules/workers/incremental-sync");
|
||||||
await boss.work(WorkerQueue.DALPURI_FULL_SYNC, async () => {
|
await boss.work(WorkerQueue.DALPURI_FULL_SYNC, async () => {
|
||||||
const socket = await ensureManagerSocketReady();
|
const socket = await ensureManagerSocketReady();
|
||||||
await enqueueDalpuriFullSync(socket);
|
await enqueueDalpuriFullSync(socket);
|
||||||
@@ -170,10 +181,37 @@ if (import.meta.main) {
|
|||||||
console.log("[worker] Registered DALPURI_FULL_SYNC job handler");
|
console.log("[worker] Registered DALPURI_FULL_SYNC job handler");
|
||||||
|
|
||||||
await boss.work(WorkerQueue.DALPURI_INCREMENTAL_SYNC, async () => {
|
await boss.work(WorkerQueue.DALPURI_INCREMENTAL_SYNC, async () => {
|
||||||
|
const startedAt = Date.now();
|
||||||
|
console.log("[worker] DALPURI_INCREMENTAL_SYNC started");
|
||||||
|
try {
|
||||||
await executeIncrementalSync();
|
await executeIncrementalSync();
|
||||||
|
console.log(
|
||||||
|
`[worker] DALPURI_INCREMENTAL_SYNC completed in ${Date.now() - startedAt}ms`
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(
|
||||||
|
`[worker] DALPURI_INCREMENTAL_SYNC failed in ${Date.now() - startedAt}ms`,
|
||||||
|
err
|
||||||
|
);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
console.log("[worker] Registered DALPURI_INCREMENTAL_SYNC job handler");
|
console.log("[worker] Registered DALPURI_INCREMENTAL_SYNC job handler");
|
||||||
|
|
||||||
|
const enqueueIncrementalWithLogging = () => {
|
||||||
|
enqueueIncrementalSync().catch((err) => {
|
||||||
|
console.error(
|
||||||
|
`[worker] interval enqueueIncrementalSync failed: ${err?.message ?? err}`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Keep a worker-local 5s scheduler so incremental sync continues even when
|
||||||
|
// API interval scheduling is unavailable.
|
||||||
|
enqueueIncrementalWithLogging();
|
||||||
|
setInterval(enqueueIncrementalWithLogging, 5_000);
|
||||||
|
console.log("[worker] Started 5-second incremental enqueue interval");
|
||||||
|
|
||||||
// Register job handler for REFRESH_SALES_METRICS
|
// Register job handler for REFRESH_SALES_METRICS
|
||||||
const { executeSalesMetricsRefresh } = await import("./modules/workers/sales-metrics");
|
const { executeSalesMetricsRefresh } = await import("./modules/workers/sales-metrics");
|
||||||
await boss.work(WorkerQueue.REFRESH_SALES_METRICS, async (jobs) => {
|
await boss.work(WorkerQueue.REFRESH_SALES_METRICS, async (jobs) => {
|
||||||
|
|||||||
@@ -1,71 +0,0 @@
|
|||||||
import axios from "axios";
|
|
||||||
|
|
||||||
const connectWiseApi = axios.create({
|
|
||||||
baseURL: `https://ttscw.totaltech.net/v4_6_release/apis/3.0/`,
|
|
||||||
headers: {
|
|
||||||
Authorization: `Basic ${process.env.CW_BASIC_TOKEN}`,
|
|
||||||
clientId: `${process.env.CW_CLIENT_ID}`,
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
// Fetch inactive catalog items
|
|
||||||
const pageSize = 1000;
|
|
||||||
let page = 1;
|
|
||||||
const inactiveItems: any[] = [];
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
const response = await connectWiseApi.get(
|
|
||||||
`/procurement/catalog?page=${page}&pageSize=${pageSize}&conditions=inactiveFlag=true&fields=id,identifier,description,_info`,
|
|
||||||
);
|
|
||||||
if (response.data.length === 0) break;
|
|
||||||
inactiveItems.push(...response.data);
|
|
||||||
page++;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`Found ${inactiveItems.length} inactive catalog items`);
|
|
||||||
console.log(`Checking inventory for each (batches of 50)...\n`);
|
|
||||||
|
|
||||||
const withStock: any[] = [];
|
|
||||||
const batchSize = 50;
|
|
||||||
|
|
||||||
for (let i = 0; i < inactiveItems.length; i += batchSize) {
|
|
||||||
const batch = inactiveItems.slice(i, i + batchSize);
|
|
||||||
await Promise.all(
|
|
||||||
batch.map(async (item) => {
|
|
||||||
try {
|
|
||||||
const res = await connectWiseApi.get(
|
|
||||||
`/procurement/catalog/${item.id}/inventory?fields=onHand`,
|
|
||||||
);
|
|
||||||
const totalOnHand = (res.data as { onHand: number }[]).reduce(
|
|
||||||
(sum, e) => sum + (e.onHand || 0),
|
|
||||||
0,
|
|
||||||
);
|
|
||||||
if (totalOnHand > 0) {
|
|
||||||
withStock.push({
|
|
||||||
id: item.id,
|
|
||||||
identifier: item.identifier,
|
|
||||||
description: item.description,
|
|
||||||
totalOnHand,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch {}
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
const done = Math.min(i + batchSize, inactiveItems.length);
|
|
||||||
if (done % 500 === 0 || done === inactiveItems.length) {
|
|
||||||
console.log(` ${done}/${inactiveItems.length} checked`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
`\nInactive items with inventory: ${withStock.length}/${inactiveItems.length}\n`,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (withStock.length > 0) {
|
|
||||||
console.log(JSON.stringify(withStock, null, 2));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
main().catch(console.error);
|
|
||||||
@@ -1,600 +0,0 @@
|
|||||||
/**
|
|
||||||
* Test Script: CW Forecast Item Edit & Partial Cancellation
|
|
||||||
*
|
|
||||||
* This script performs read-write operations against the ConnectWise API:
|
|
||||||
*
|
|
||||||
* 1. Search all open opportunities for a forecast item with description
|
|
||||||
* matching "labor Special Order" (case-insensitive).
|
|
||||||
* 2. Report the current state of that item (price, cost, qty, etc.).
|
|
||||||
* 3. PATCH the item: revenue → 72,000 | cost → 8,500 | quantity → 67
|
|
||||||
* 4. Verify the update by re-fetching the forecast.
|
|
||||||
* 5. Cancel 13 units via the linked procurement product
|
|
||||||
* (partial cancellation: quantityCancelled = 13).
|
|
||||||
* 6. Verify the cancellation by re-fetching procurement data.
|
|
||||||
* 7. Report on every step.
|
|
||||||
*
|
|
||||||
* Usage: bun run test-cw-edit-item.ts
|
|
||||||
*/
|
|
||||||
|
|
||||||
import axios from "axios";
|
|
||||||
|
|
||||||
const cw = axios.create({
|
|
||||||
baseURL: "https://ttscw.totaltech.net/v4_6_release/apis/3.0/",
|
|
||||||
headers: {
|
|
||||||
Authorization: `Basic ${process.env.CW_BASIC_TOKEN}`,
|
|
||||||
clientId: `${process.env.CW_CLIENT_ID}`,
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
timeout: 30_000,
|
|
||||||
});
|
|
||||||
|
|
||||||
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
const log = (label: string, ...args: unknown[]) =>
|
|
||||||
console.log(`\n[${label}]`, ...args);
|
|
||||||
|
|
||||||
const divider = () => console.log("─".repeat(72));
|
|
||||||
|
|
||||||
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
|
|
||||||
|
|
||||||
const fmt = (n: number) =>
|
|
||||||
n.toLocaleString("en-US", {
|
|
||||||
minimumFractionDigits: 2,
|
|
||||||
maximumFractionDigits: 2,
|
|
||||||
});
|
|
||||||
|
|
||||||
// ── Types (minimal, for this script) ──────────────────────────────────────────
|
|
||||||
|
|
||||||
interface ForecastItem {
|
|
||||||
id: number;
|
|
||||||
forecastDescription: string;
|
|
||||||
productDescription: string;
|
|
||||||
quantity: number;
|
|
||||||
revenue: number;
|
|
||||||
cost: number;
|
|
||||||
margin: number;
|
|
||||||
forecastType: string;
|
|
||||||
sequenceNumber: number;
|
|
||||||
catalogItem?: { id: number; identifier: string };
|
|
||||||
status?: { id: number; name: string };
|
|
||||||
opportunity?: { id: number; name: string };
|
|
||||||
[key: string]: unknown;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Forecast {
|
|
||||||
id: number;
|
|
||||||
forecastItems: ForecastItem[];
|
|
||||||
[key: string]: unknown;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ProcurementProduct {
|
|
||||||
id: number;
|
|
||||||
forecastDetailId: number;
|
|
||||||
description: string;
|
|
||||||
quantity: number;
|
|
||||||
price: number;
|
|
||||||
cost: number;
|
|
||||||
cancelledFlag: boolean;
|
|
||||||
quantityCancelled: number;
|
|
||||||
cancelledReason: string | null;
|
|
||||||
cancelledBy: string | null;
|
|
||||||
cancelledDate: string | null;
|
|
||||||
opportunity?: { id: number };
|
|
||||||
[key: string]: unknown;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Main ──────────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
divider();
|
|
||||||
log("START", "CW Forecast Item Edit & Cancellation Test");
|
|
||||||
log("START", `Timestamp: ${new Date().toISOString()}`);
|
|
||||||
divider();
|
|
||||||
|
|
||||||
// ── Step 1: Find the "labor Special Order" forecast item ────────────────
|
|
||||||
|
|
||||||
const OPP_ID = 5150;
|
|
||||||
log(
|
|
||||||
"SEARCH",
|
|
||||||
`Looking for forecast item matching "labor Special Order" on opportunity ${OPP_ID}...`,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Fetch the forecast for opportunity 5150 directly
|
|
||||||
let targetOppId: number = OPP_ID;
|
|
||||||
let targetItem: ForecastItem | null = null;
|
|
||||||
let targetForecast: Forecast | null = null;
|
|
||||||
|
|
||||||
const forecastRes = await cw.get(`/sales/opportunities/${OPP_ID}/forecast`);
|
|
||||||
targetForecast = forecastRes.data as Forecast;
|
|
||||||
const match = (targetForecast.forecastItems ?? []).find(
|
|
||||||
(fi: ForecastItem) =>
|
|
||||||
fi.forecastDescription?.toLowerCase().includes("special order") ||
|
|
||||||
fi.productDescription?.toLowerCase().includes("special order"),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (match) {
|
|
||||||
targetItem = match;
|
|
||||||
log("SEARCH", `✓ FOUND forecast item on opportunity ${OPP_ID}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!targetItem || !targetForecast) {
|
|
||||||
log(
|
|
||||||
"SEARCH",
|
|
||||||
`✗ No "labor Special Order" item found on opportunity ${OPP_ID}.`,
|
|
||||||
);
|
|
||||||
log("SEARCH", "All forecast items on this opportunity:");
|
|
||||||
for (const fi of targetForecast.forecastItems ?? []) {
|
|
||||||
console.log(
|
|
||||||
` id=${fi.id} "${fi.forecastDescription}" / "${fi.productDescription}"`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
log("SEARCH", "Aborting.");
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Step 2: Report current state ────────────────────────────────────────
|
|
||||||
|
|
||||||
divider();
|
|
||||||
log("CURRENT STATE", "Forecast item details BEFORE edit:");
|
|
||||||
console.log(` Opportunity ID: ${targetOppId}`);
|
|
||||||
console.log(` Forecast Item ID: ${targetItem.id}`);
|
|
||||||
console.log(` Forecast Description: ${targetItem.forecastDescription}`);
|
|
||||||
console.log(` Product Description: ${targetItem.productDescription}`);
|
|
||||||
console.log(
|
|
||||||
` Catalog Item: ${targetItem.catalogItem?.identifier ?? "(none)"} (cwId=${targetItem.catalogItem?.id ?? "N/A"})`,
|
|
||||||
);
|
|
||||||
console.log(` Forecast Type: ${targetItem.forecastType}`);
|
|
||||||
console.log(
|
|
||||||
` Status: ${targetItem.status?.name ?? "?"} (id=${targetItem.status?.id ?? "?"})`,
|
|
||||||
);
|
|
||||||
console.log(` Sequence Number: ${targetItem.sequenceNumber}`);
|
|
||||||
console.log(` ──────────────────────────────────`);
|
|
||||||
console.log(` Quantity: ${targetItem.quantity}`);
|
|
||||||
console.log(` Revenue (Price): $${fmt(targetItem.revenue)}`);
|
|
||||||
console.log(` Cost: $${fmt(targetItem.cost)}`);
|
|
||||||
console.log(` Margin: $${fmt(targetItem.margin)}`);
|
|
||||||
|
|
||||||
// Also report all items on this opportunity for context
|
|
||||||
const allItems = targetForecast.forecastItems ?? [];
|
|
||||||
log(
|
|
||||||
"CONTEXT",
|
|
||||||
`Total forecast items on this opportunity: ${allItems.length}`,
|
|
||||||
);
|
|
||||||
for (const fi of allItems) {
|
|
||||||
const marker = fi.id === targetItem.id ? " ◀ TARGET" : "";
|
|
||||||
console.log(
|
|
||||||
` [${fi.sequenceNumber}] id=${fi.id} "${fi.forecastDescription}" ` +
|
|
||||||
`qty=${fi.quantity} rev=$${fmt(fi.revenue)} cost=$${fmt(fi.cost)}${marker}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Step 3: PATCH the forecast item ─────────────────────────────────────
|
|
||||||
|
|
||||||
divider();
|
|
||||||
const UNIT_PRICE = 72_000;
|
|
||||||
const UNIT_COST = 8_500;
|
|
||||||
const QTY = 67;
|
|
||||||
const TOTAL_REVENUE = UNIT_PRICE * QTY; // $4,824,000
|
|
||||||
const TOTAL_COST = UNIT_COST * QTY; // $569,500
|
|
||||||
|
|
||||||
log("EDIT", "Patching forecast item...");
|
|
||||||
log(
|
|
||||||
"EDIT",
|
|
||||||
` Unit price: $${fmt(UNIT_PRICE)} × ${QTY} = $${fmt(TOTAL_REVENUE)} (revenue)`,
|
|
||||||
);
|
|
||||||
log(
|
|
||||||
"EDIT",
|
|
||||||
` Unit cost: $${fmt(UNIT_COST)} × ${QTY} = $${fmt(TOTAL_COST)} (cost)`,
|
|
||||||
);
|
|
||||||
log("EDIT", ` Quantity: ${QTY}`);
|
|
||||||
|
|
||||||
// Find the index of our target item in the forecast array
|
|
||||||
const forecastItems = targetForecast.forecastItems ?? [];
|
|
||||||
const targetIdx = forecastItems.findIndex((fi) => fi.id === targetItem!.id);
|
|
||||||
|
|
||||||
if (targetIdx === -1) {
|
|
||||||
log(
|
|
||||||
"EDIT",
|
|
||||||
"✗ Could not find target item index in forecast array. Aborting.",
|
|
||||||
);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
log("EDIT", `Target item is at index ${targetIdx} in forecastItems array.`);
|
|
||||||
|
|
||||||
const patchOps = [
|
|
||||||
{
|
|
||||||
op: "replace",
|
|
||||||
path: `/forecastItems/${targetIdx}/revenue`,
|
|
||||||
value: TOTAL_REVENUE,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
op: "replace",
|
|
||||||
path: `/forecastItems/${targetIdx}/cost`,
|
|
||||||
value: TOTAL_COST,
|
|
||||||
},
|
|
||||||
{ op: "replace", path: `/forecastItems/${targetIdx}/quantity`, value: QTY },
|
|
||||||
];
|
|
||||||
|
|
||||||
log("EDIT", "Patch operations:");
|
|
||||||
for (const op of patchOps) {
|
|
||||||
console.log(` ${op.op} ${op.path} → ${op.value}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const patchRes = await cw.patch(
|
|
||||||
`/sales/opportunities/${targetOppId}/forecast`,
|
|
||||||
patchOps,
|
|
||||||
);
|
|
||||||
const updatedForecast: Forecast = patchRes.data;
|
|
||||||
const updatedItem = (updatedForecast.forecastItems ?? [])[targetIdx];
|
|
||||||
|
|
||||||
if (!updatedItem) {
|
|
||||||
log("EDIT", "✗ Item not found at expected index after PATCH.");
|
|
||||||
} else {
|
|
||||||
log("EDIT", "✓ PATCH successful. Updated item:");
|
|
||||||
console.log(` Forecast Item ID: ${updatedItem.id}`);
|
|
||||||
console.log(` Forecast Description: ${updatedItem.forecastDescription}`);
|
|
||||||
console.log(` Quantity: ${updatedItem.quantity}`);
|
|
||||||
console.log(` Revenue (Price): $${fmt(updatedItem.revenue)}`);
|
|
||||||
console.log(` Cost: $${fmt(updatedItem.cost)}`);
|
|
||||||
console.log(` Margin: $${fmt(updatedItem.margin)}`);
|
|
||||||
|
|
||||||
// Verify values match what we set
|
|
||||||
const checks = [
|
|
||||||
{
|
|
||||||
field: "revenue",
|
|
||||||
expected: TOTAL_REVENUE,
|
|
||||||
actual: updatedItem.revenue,
|
|
||||||
},
|
|
||||||
{ field: "cost", expected: TOTAL_COST, actual: updatedItem.cost },
|
|
||||||
{ field: "quantity", expected: QTY, actual: updatedItem.quantity },
|
|
||||||
];
|
|
||||||
|
|
||||||
log("VERIFY EDIT", "Checking values match requested:");
|
|
||||||
for (const check of checks) {
|
|
||||||
const ok = check.actual === check.expected;
|
|
||||||
console.log(
|
|
||||||
` ${ok ? "✓" : "✗"} ${check.field}: expected=${check.expected}, actual=${check.actual}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update our reference for the cancellation step
|
|
||||||
targetItem = updatedItem;
|
|
||||||
}
|
|
||||||
} catch (err: any) {
|
|
||||||
log("EDIT", `✗ PATCH failed: ${err.response?.status ?? err.message}`);
|
|
||||||
if (err.response?.data) {
|
|
||||||
console.log(" Response:", JSON.stringify(err.response.data, null, 2));
|
|
||||||
}
|
|
||||||
|
|
||||||
// If quantity PATCH failed (read-only), try without quantity
|
|
||||||
if (err.response?.status === 400 || err.response?.status === 422) {
|
|
||||||
log(
|
|
||||||
"EDIT",
|
|
||||||
"Retrying without quantity (may be read-only on forecast items)...",
|
|
||||||
);
|
|
||||||
const retryOps = patchOps.filter((op) => !op.path.endsWith("/quantity"));
|
|
||||||
try {
|
|
||||||
const retryRes = await cw.patch(
|
|
||||||
`/sales/opportunities/${targetOppId}/forecast`,
|
|
||||||
retryOps,
|
|
||||||
);
|
|
||||||
const retryForecast: Forecast = retryRes.data;
|
|
||||||
const retryItem = (retryForecast.forecastItems ?? [])[targetIdx];
|
|
||||||
|
|
||||||
if (retryItem) {
|
|
||||||
log(
|
|
||||||
"EDIT",
|
|
||||||
"✓ Retry PATCH successful (without quantity). Updated item:",
|
|
||||||
);
|
|
||||||
console.log(
|
|
||||||
` Quantity: ${retryItem.quantity} (unchanged — read-only)`,
|
|
||||||
);
|
|
||||||
console.log(` Revenue (Price): $${fmt(retryItem.revenue)}`);
|
|
||||||
console.log(` Cost: $${fmt(retryItem.cost)}`);
|
|
||||||
console.log(` Margin: $${fmt(retryItem.margin)}`);
|
|
||||||
targetItem = retryItem;
|
|
||||||
}
|
|
||||||
} catch (retryErr: any) {
|
|
||||||
log(
|
|
||||||
"EDIT",
|
|
||||||
`✗ Retry also failed: ${retryErr.response?.status ?? retryErr.message}`,
|
|
||||||
);
|
|
||||||
if (retryErr.response?.data) {
|
|
||||||
console.log(
|
|
||||||
" Response:",
|
|
||||||
JSON.stringify(retryErr.response.data, null, 2),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Step 4: Re-fetch and confirm final forecast state ───────────────────
|
|
||||||
|
|
||||||
divider();
|
|
||||||
log("RE-FETCH", "Fetching forecast to confirm final state...");
|
|
||||||
await sleep(500);
|
|
||||||
|
|
||||||
const confirmRes = await cw.get(
|
|
||||||
`/sales/opportunities/${targetOppId}/forecast`,
|
|
||||||
);
|
|
||||||
const confirmedForecast: Forecast = confirmRes.data;
|
|
||||||
const confirmedItem = (confirmedForecast.forecastItems ?? []).find(
|
|
||||||
(fi) => fi.id === targetItem!.id,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (confirmedItem) {
|
|
||||||
log("CONFIRMED STATE", "Forecast item after edit:");
|
|
||||||
console.log(` Forecast Item ID: ${confirmedItem.id}`);
|
|
||||||
console.log(` Forecast Description: ${confirmedItem.forecastDescription}`);
|
|
||||||
console.log(` Quantity: ${confirmedItem.quantity}`);
|
|
||||||
console.log(` Revenue (Price): $${fmt(confirmedItem.revenue)}`);
|
|
||||||
console.log(` Cost: $${fmt(confirmedItem.cost)}`);
|
|
||||||
console.log(` Margin: $${fmt(confirmedItem.margin)}`);
|
|
||||||
} else {
|
|
||||||
log(
|
|
||||||
"CONFIRMED STATE",
|
|
||||||
"⚠ Could not find item by original ID — it may have been regenerated.",
|
|
||||||
);
|
|
||||||
log("CONFIRMED STATE", "All current forecast items:");
|
|
||||||
for (const fi of confirmedForecast.forecastItems ?? []) {
|
|
||||||
console.log(
|
|
||||||
` id=${fi.id} "${fi.forecastDescription}" qty=${fi.quantity} rev=$${fmt(fi.revenue)} cost=$${fmt(fi.cost)}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Step 5: Cancel 13 items via procurement product ─────────────────────
|
|
||||||
|
|
||||||
divider();
|
|
||||||
log("CANCEL", "Cancelling 13 units on this item via procurement product...");
|
|
||||||
|
|
||||||
// First, find existing procurement products linked to this opportunity
|
|
||||||
const procRes = await cw.get(
|
|
||||||
`/procurement/products?conditions=${encodeURIComponent(`opportunity/id=${targetOppId}`)}&pageSize=1000`,
|
|
||||||
);
|
|
||||||
const procProducts: ProcurementProduct[] = procRes.data;
|
|
||||||
|
|
||||||
log(
|
|
||||||
"CANCEL",
|
|
||||||
`Found ${procProducts.length} procurement product(s) on this opportunity.`,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (procProducts.length > 0) {
|
|
||||||
for (const pp of procProducts) {
|
|
||||||
console.log(
|
|
||||||
` Proc id=${pp.id} forecastDetailId=${pp.forecastDetailId} ` +
|
|
||||||
`"${pp.description}" qty=${pp.quantity} price=$${fmt(pp.price ?? 0)} ` +
|
|
||||||
`cancelled=${pp.cancelledFlag} qtyCancelled=${pp.quantityCancelled}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find the procurement product linked to our forecast item
|
|
||||||
const linkedProc = procProducts.find(
|
|
||||||
(pp) => pp.forecastDetailId === targetItem!.id,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (linkedProc) {
|
|
||||||
log("CANCEL", `Found linked procurement product: id=${linkedProc.id}`);
|
|
||||||
log(
|
|
||||||
"CANCEL",
|
|
||||||
`Current state: cancelled=${linkedProc.cancelledFlag}, quantityCancelled=${linkedProc.quantityCancelled}`,
|
|
||||||
);
|
|
||||||
log("CANCEL", "Patching: quantityCancelled → 13, cancelledFlag → true");
|
|
||||||
|
|
||||||
try {
|
|
||||||
const cancelRes = await cw.patch(
|
|
||||||
`/procurement/products/${linkedProc.id}`,
|
|
||||||
[
|
|
||||||
{ op: "replace", path: "cancelledFlag", value: true },
|
|
||||||
{ op: "replace", path: "quantityCancelled", value: 13 },
|
|
||||||
{
|
|
||||||
op: "replace",
|
|
||||||
path: "cancelledReason",
|
|
||||||
value: "Test cancellation — 13 units",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
);
|
|
||||||
|
|
||||||
log("CANCEL", "✓ Cancellation PATCH successful.");
|
|
||||||
console.log(` cancelledFlag: ${cancelRes.data.cancelledFlag}`);
|
|
||||||
console.log(` quantityCancelled: ${cancelRes.data.quantityCancelled}`);
|
|
||||||
console.log(` cancelledReason: ${cancelRes.data.cancelledReason}`);
|
|
||||||
console.log(
|
|
||||||
` cancelledBy: ${cancelRes.data.cancelledBy ?? "N/A"}`,
|
|
||||||
);
|
|
||||||
console.log(
|
|
||||||
` cancelledDate: ${cancelRes.data.cancelledDate ?? "N/A"}`,
|
|
||||||
);
|
|
||||||
} catch (err: any) {
|
|
||||||
log(
|
|
||||||
"CANCEL",
|
|
||||||
`✗ Cancellation PATCH failed: ${err.response?.status ?? err.message}`,
|
|
||||||
);
|
|
||||||
if (err.response?.data) {
|
|
||||||
console.log(" Response:", JSON.stringify(err.response.data, null, 2));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
log(
|
|
||||||
"CANCEL",
|
|
||||||
`No procurement product linked to forecast item id=${targetItem!.id}.`,
|
|
||||||
);
|
|
||||||
log(
|
|
||||||
"CANCEL",
|
|
||||||
"Creating a procurement product first, then cancelling 13...",
|
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Create a procurement product linked to this forecast item
|
|
||||||
const createProcRes = await cw.post("/procurement/products", {
|
|
||||||
catalogItem: targetItem!.catalogItem?.id
|
|
||||||
? { id: targetItem!.catalogItem.id }
|
|
||||||
: undefined,
|
|
||||||
description:
|
|
||||||
targetItem!.forecastDescription || targetItem!.productDescription,
|
|
||||||
quantity: targetItem!.quantity || 67,
|
|
||||||
price: targetItem!.revenue || 72_000,
|
|
||||||
cost: targetItem!.cost || 8_500,
|
|
||||||
billableOption: "Billable",
|
|
||||||
opportunity: { id: targetOppId },
|
|
||||||
forecastDetailId: targetItem!.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
const newProc = createProcRes.data;
|
|
||||||
log("CANCEL", `✓ Created procurement product id=${newProc.id}`);
|
|
||||||
console.log(` forecastDetailId: ${newProc.forecastDetailId}`);
|
|
||||||
console.log(` description: ${newProc.description}`);
|
|
||||||
console.log(` quantity: ${newProc.quantity}`);
|
|
||||||
console.log(` price: $${fmt(newProc.price ?? 0)}`);
|
|
||||||
console.log(` cost: $${fmt(newProc.cost ?? 0)}`);
|
|
||||||
|
|
||||||
// Now cancel 13 units
|
|
||||||
log("CANCEL", "Patching procurement product: quantityCancelled → 13...");
|
|
||||||
const cancelRes = await cw.patch(`/procurement/products/${newProc.id}`, [
|
|
||||||
{ op: "replace", path: "cancelledFlag", value: true },
|
|
||||||
{ op: "replace", path: "quantityCancelled", value: 13 },
|
|
||||||
{
|
|
||||||
op: "replace",
|
|
||||||
path: "cancelledReason",
|
|
||||||
value: "Test cancellation — 13 units",
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
log("CANCEL", "✓ Cancellation PATCH successful.");
|
|
||||||
console.log(` cancelledFlag: ${cancelRes.data.cancelledFlag}`);
|
|
||||||
console.log(` quantityCancelled: ${cancelRes.data.quantityCancelled}`);
|
|
||||||
console.log(` cancelledReason: ${cancelRes.data.cancelledReason}`);
|
|
||||||
console.log(
|
|
||||||
` cancelledBy: ${cancelRes.data.cancelledBy ?? "N/A"}`,
|
|
||||||
);
|
|
||||||
console.log(
|
|
||||||
` cancelledDate: ${cancelRes.data.cancelledDate ?? "N/A"}`,
|
|
||||||
);
|
|
||||||
} catch (err: any) {
|
|
||||||
log("CANCEL", `✗ Failed: ${err.response?.status ?? err.message}`);
|
|
||||||
if (err.response?.data) {
|
|
||||||
console.log(" Response:", JSON.stringify(err.response.data, null, 2));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Step 6: Final verification ──────────────────────────────────────────
|
|
||||||
|
|
||||||
divider();
|
|
||||||
log("FINAL VERIFY", "Re-fetching all data for final report...");
|
|
||||||
await sleep(500);
|
|
||||||
|
|
||||||
// Re-fetch forecast
|
|
||||||
const finalForecastRes = await cw.get(
|
|
||||||
`/sales/opportunities/${targetOppId}/forecast`,
|
|
||||||
);
|
|
||||||
const finalForecast: Forecast = finalForecastRes.data;
|
|
||||||
const finalItem =
|
|
||||||
(finalForecast.forecastItems ?? []).find(
|
|
||||||
(fi) => fi.id === targetItem!.id,
|
|
||||||
) ??
|
|
||||||
(finalForecast.forecastItems ?? []).find(
|
|
||||||
(fi) =>
|
|
||||||
fi.forecastDescription?.toLowerCase().includes("special order") ||
|
|
||||||
fi.productDescription?.toLowerCase().includes("special order"),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Re-fetch procurement
|
|
||||||
const finalProcRes = await cw.get(
|
|
||||||
`/procurement/products?conditions=${encodeURIComponent(`opportunity/id=${targetOppId}`)}&pageSize=1000`,
|
|
||||||
);
|
|
||||||
const finalProcs: ProcurementProduct[] = finalProcRes.data;
|
|
||||||
|
|
||||||
log("FINAL STATE — FORECAST ITEM", "");
|
|
||||||
if (finalItem) {
|
|
||||||
console.log(` Forecast Item ID: ${finalItem.id}`);
|
|
||||||
console.log(` Forecast Description: ${finalItem.forecastDescription}`);
|
|
||||||
console.log(` Quantity: ${finalItem.quantity}`);
|
|
||||||
console.log(` Revenue (Price): $${fmt(finalItem.revenue)}`);
|
|
||||||
console.log(` Cost: $${fmt(finalItem.cost)}`);
|
|
||||||
console.log(` Margin: $${fmt(finalItem.margin)}`);
|
|
||||||
} else {
|
|
||||||
console.log(" ⚠ Target item not found in final forecast.");
|
|
||||||
}
|
|
||||||
|
|
||||||
log("FINAL STATE — PROCUREMENT", `${finalProcs.length} product(s):`);
|
|
||||||
for (const pp of finalProcs) {
|
|
||||||
console.log(
|
|
||||||
` id=${pp.id} forecastDetailId=${pp.forecastDetailId} ` +
|
|
||||||
`"${pp.description}" qty=${pp.quantity} cancelled=${pp.cancelledFlag} ` +
|
|
||||||
`qtyCancelled=${pp.quantityCancelled} reason="${pp.cancelledReason ?? ""}"`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Summary ─────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
divider();
|
|
||||||
log("SUMMARY", "");
|
|
||||||
|
|
||||||
// After cancelling 13 of 67, CW recalculates totals for remaining 54 units
|
|
||||||
const expectedFinalRevenue = Math.round(UNIT_PRICE * (QTY - 13) * 100) / 100;
|
|
||||||
const expectedFinalCost = Math.round(UNIT_COST * (QTY - 13) * 100) / 100;
|
|
||||||
|
|
||||||
const editOk = finalItem
|
|
||||||
? Math.abs(finalItem.revenue - expectedFinalRevenue) < 1 &&
|
|
||||||
Math.abs(finalItem.cost - expectedFinalCost) < 1
|
|
||||||
: false;
|
|
||||||
const qtyOk = finalItem ? finalItem.quantity === QTY : false;
|
|
||||||
|
|
||||||
if (finalItem) {
|
|
||||||
console.log(
|
|
||||||
` Expected final revenue ($${fmt(UNIT_PRICE)} × ${QTY - 13}): $${fmt(expectedFinalRevenue)}`,
|
|
||||||
);
|
|
||||||
console.log(
|
|
||||||
` Actual final revenue: $${fmt(finalItem.revenue)}`,
|
|
||||||
);
|
|
||||||
console.log(
|
|
||||||
` Expected final cost ($${fmt(UNIT_COST)} × ${QTY - 13}): $${fmt(expectedFinalCost)}`,
|
|
||||||
);
|
|
||||||
console.log(
|
|
||||||
` Actual final cost: $${fmt(finalItem.cost)}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const cancelOk = finalProcs.some(
|
|
||||||
(pp) =>
|
|
||||||
pp.forecastDetailId === targetItem!.id &&
|
|
||||||
pp.cancelledFlag === true &&
|
|
||||||
pp.quantityCancelled === 13,
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
` Unit price $${fmt(UNIT_PRICE)}/ea: `,
|
|
||||||
editOk ? "✓ PASS" : "✗ FAIL",
|
|
||||||
);
|
|
||||||
console.log(
|
|
||||||
` Unit cost $${fmt(UNIT_COST)}/ea: `,
|
|
||||||
editOk ? "✓ PASS" : "✗ FAIL",
|
|
||||||
);
|
|
||||||
console.log(
|
|
||||||
` Quantity set to ${QTY}: `,
|
|
||||||
qtyOk ? "✓ PASS" : "✗ FAIL (may be read-only)",
|
|
||||||
);
|
|
||||||
console.log(
|
|
||||||
" 13 units cancelled: ",
|
|
||||||
cancelOk ? "✓ PASS" : "✗ FAIL",
|
|
||||||
);
|
|
||||||
|
|
||||||
const allPass = editOk && qtyOk && cancelOk;
|
|
||||||
divider();
|
|
||||||
log(
|
|
||||||
"RESULT",
|
|
||||||
allPass
|
|
||||||
? "✓ ALL CHECKS PASSED"
|
|
||||||
: "⚠ SOME CHECKS DID NOT PASS — review output above",
|
|
||||||
);
|
|
||||||
divider();
|
|
||||||
}
|
|
||||||
|
|
||||||
main().catch((err) => {
|
|
||||||
console.error("\n[FATAL]", err.response?.data ?? err.message);
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
@@ -1,442 +0,0 @@
|
|||||||
/**
|
|
||||||
* Test Script: Forecast Item Resequencing & Procurement Linkage
|
|
||||||
*
|
|
||||||
* Validates the CW forecast API behaviour discovered via probing:
|
|
||||||
* - `sequenceNumber` is read-only — display order = array position
|
|
||||||
* - PUT always regenerates all forecast item IDs
|
|
||||||
* - Revenue & cost are preserved through PUT
|
|
||||||
* - PATCH on /forecast with `/forecastItems/{idx}/field` paths works
|
|
||||||
* for some fields (e.g. forecastDescription) and preserves IDs
|
|
||||||
*
|
|
||||||
* Test flow:
|
|
||||||
* 1. Create opportunity under XYZ Test Company
|
|
||||||
* 2. Add 4 products via POST
|
|
||||||
* 3. Create procurement products (linked by forecastDetailId)
|
|
||||||
* 4. Cancel one procurement product
|
|
||||||
* 5. Reorder forecast items via PUT (reverse order)
|
|
||||||
* 6. Remap procurement forecastDetailId to new IDs
|
|
||||||
* 7. Verify: order correct, prices preserved, cancellation data intact
|
|
||||||
* 8. Clean up
|
|
||||||
*
|
|
||||||
* Usage: bun run test-forecast-resequence.ts
|
|
||||||
*/
|
|
||||||
import axios from "axios";
|
|
||||||
|
|
||||||
const cw = axios.create({
|
|
||||||
baseURL: "https://ttscw.totaltech.net/v4_6_release/apis/3.0/",
|
|
||||||
headers: {
|
|
||||||
Authorization: `Basic ${process.env.CW_BASIC_TOKEN}`,
|
|
||||||
clientId: `${process.env.CW_CLIENT_ID}`,
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const log = (label: string, ...args: unknown[]) =>
|
|
||||||
console.log(`\n[${label}]`, ...args);
|
|
||||||
|
|
||||||
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
// ── 1. Find company ─────────────────────────────────────────────────────
|
|
||||||
log("SETUP", "Finding XYZ Test Company...");
|
|
||||||
const compRes = await cw.get(
|
|
||||||
`/company/companies?conditions=${encodeURIComponent("name like 'XYZ Test%'")}&fields=id,identifier,name`,
|
|
||||||
);
|
|
||||||
if (compRes.data.length === 0) {
|
|
||||||
console.error("ERROR: 'XYZ Test Company' not found.");
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
const company = compRes.data[0];
|
|
||||||
log("SETUP", `Company: ${company.name} (id=${company.id})`);
|
|
||||||
|
|
||||||
// ── 2. Create opportunity ───────────────────────────────────────────────
|
|
||||||
log("SETUP", "Creating test opportunity...");
|
|
||||||
const oppRes = await cw.post("/sales/opportunities", {
|
|
||||||
name: `[TEST] Resequence – ${new Date().toISOString().slice(0, 16)}`,
|
|
||||||
company: { id: company.id },
|
|
||||||
contact: { id: 1 },
|
|
||||||
primarySalesRep: { id: 153 },
|
|
||||||
expectedCloseDate: new Date(Date.now() + 30 * 86_400_000)
|
|
||||||
.toISOString()
|
|
||||||
.replace(/\.\d{3}Z$/, "Z"),
|
|
||||||
});
|
|
||||||
const oppId = oppRes.data.id;
|
|
||||||
log("SETUP", `Created opportunity id=${oppId}`);
|
|
||||||
|
|
||||||
const forecastUrl = `/sales/opportunities/${oppId}/forecast`;
|
|
||||||
|
|
||||||
// Track IDs for cleanup
|
|
||||||
const procIdsToClean: number[] = [];
|
|
||||||
|
|
||||||
try {
|
|
||||||
// ── 3. Add 4 products ───────────────────────────────────────────────────
|
|
||||||
log("PRODUCTS", "Adding 4 products...");
|
|
||||||
const postRes = await cw.post(forecastUrl, {
|
|
||||||
forecastItems: [
|
|
||||||
{
|
|
||||||
opportunity: { id: oppId },
|
|
||||||
status: { id: 1 },
|
|
||||||
forecastDescription: "Alpha",
|
|
||||||
revenue: 100,
|
|
||||||
cost: 50,
|
|
||||||
forecastType: "Product",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
opportunity: { id: oppId },
|
|
||||||
status: { id: 1 },
|
|
||||||
forecastDescription: "Bravo",
|
|
||||||
revenue: 250,
|
|
||||||
cost: 125,
|
|
||||||
forecastType: "Product",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
opportunity: { id: oppId },
|
|
||||||
status: { id: 1 },
|
|
||||||
forecastDescription: "Charlie",
|
|
||||||
revenue: 30,
|
|
||||||
cost: 10,
|
|
||||||
forecastType: "Product",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
opportunity: { id: oppId },
|
|
||||||
status: { id: 1 },
|
|
||||||
forecastDescription: "Delta",
|
|
||||||
revenue: 75,
|
|
||||||
cost: 40,
|
|
||||||
forecastType: "Product",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
const items: any[] = postRes.data.forecastItems ?? [];
|
|
||||||
log("PRODUCTS", `Created ${items.length} items:`);
|
|
||||||
for (const it of items) {
|
|
||||||
console.log(
|
|
||||||
` id=${it.id} desc="${it.forecastDescription}" rev=${it.revenue} cost=${it.cost}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Snapshot prices
|
|
||||||
const priceSnap = new Map<string, { rev: number; cost: number }>(
|
|
||||||
items.map((i) => [
|
|
||||||
i.forecastDescription,
|
|
||||||
{ rev: i.revenue, cost: i.cost },
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
|
|
||||||
// ── 4. Create procurement products ──────────────────────────────────────
|
|
||||||
log("PROCUREMENT", "Creating procurement products...");
|
|
||||||
const procProducts: any[] = [];
|
|
||||||
for (const item of items) {
|
|
||||||
try {
|
|
||||||
const pr = await cw.post("/procurement/products", {
|
|
||||||
catalogItem: { id: 87 },
|
|
||||||
description: item.forecastDescription,
|
|
||||||
quantity: 1,
|
|
||||||
price: item.revenue,
|
|
||||||
cost: item.cost,
|
|
||||||
billableOption: "Billable",
|
|
||||||
opportunity: { id: oppId },
|
|
||||||
forecastDetailId: item.id,
|
|
||||||
});
|
|
||||||
procProducts.push(pr.data);
|
|
||||||
procIdsToClean.push(pr.data.id);
|
|
||||||
console.log(
|
|
||||||
` ✓ Proc ${pr.data.id} → forecastDetailId=${pr.data.forecastDetailId} "${item.forecastDescription}"`,
|
|
||||||
);
|
|
||||||
} catch (e: any) {
|
|
||||||
console.log(
|
|
||||||
` ✗ Failed: ${e.response?.status} ${JSON.stringify(e.response?.data)}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (procProducts.length === 0) {
|
|
||||||
log(
|
|
||||||
"PROCUREMENT",
|
|
||||||
"Could not create procurement products (permission issue?).",
|
|
||||||
);
|
|
||||||
log(
|
|
||||||
"PROCUREMENT",
|
|
||||||
"Will run reorder test without cancellation verification.",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── 5. Cancel "Bravo" procurement product ───────────────────────────────
|
|
||||||
const bravoProc = procProducts.find((p: any) => p.description === "Bravo");
|
|
||||||
if (bravoProc) {
|
|
||||||
log("CANCEL", `Cancelling Bravo (proc id=${bravoProc.id})...`);
|
|
||||||
try {
|
|
||||||
await cw.patch(`/procurement/products/${bravoProc.id}`, [
|
|
||||||
{ op: "replace", path: "cancelledFlag", value: true },
|
|
||||||
{ op: "replace", path: "quantityCancelled", value: 1 },
|
|
||||||
{
|
|
||||||
op: "replace",
|
|
||||||
path: "cancelledReason",
|
|
||||||
value: "Test cancellation",
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
log("CANCEL", "✓ Cancelled.");
|
|
||||||
} catch (e: any) {
|
|
||||||
log(
|
|
||||||
"CANCEL",
|
|
||||||
`✗ ${e.response?.status} ${JSON.stringify(e.response?.data)}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── 5b. Check for auto-created forecast items ─────────────────────────
|
|
||||||
await sleep(300);
|
|
||||||
const midForecast = await cw.get(forecastUrl);
|
|
||||||
const midItems = midForecast.data.forecastItems ?? [];
|
|
||||||
log(
|
|
||||||
"OBSERVE",
|
|
||||||
`Forecast items after procurement creation: ${midItems.length} (was ${items.length})`,
|
|
||||||
);
|
|
||||||
if (midItems.length !== items.length) {
|
|
||||||
log(
|
|
||||||
"OBSERVE",
|
|
||||||
"⚠ Creating procurement products auto-created additional forecast items!",
|
|
||||||
);
|
|
||||||
for (const mi of midItems) {
|
|
||||||
const isOriginal = items.some((i: any) => i.id === mi.id);
|
|
||||||
console.log(
|
|
||||||
` id=${mi.id} desc="${mi.forecastDescription}" ${isOriginal ? "(original)" : "(AUTO-CREATED by procurement)"}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Snapshot procurement state before reorder
|
|
||||||
const beforeProc = await cw.get(
|
|
||||||
`/procurement/products?conditions=${encodeURIComponent(`opportunity/id=${oppId}`)}&fields=id,forecastDetailId,cancelledFlag,quantityCancelled,cancelledReason,description`,
|
|
||||||
);
|
|
||||||
// Build map by description for cross-PUT comparison (IDs will change)
|
|
||||||
const beforeByDesc = new Map<string, any>();
|
|
||||||
log(
|
|
||||||
"SNAPSHOT",
|
|
||||||
`${beforeProc.data.length} procurement products before reorder:`,
|
|
||||||
);
|
|
||||||
for (const p of beforeProc.data) {
|
|
||||||
beforeByDesc.set(p.description, p);
|
|
||||||
console.log(
|
|
||||||
` Proc ${p.id}: forecastDetailId=${p.forecastDetailId} cancelled=${p.cancelledFlag} qty=${p.quantityCancelled} reason="${p.cancelledReason ?? ""}" "${p.description}"`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Record old procurement IDs for later comparison
|
|
||||||
const oldProcIds = new Set(beforeProc.data.map((p: any) => p.id));
|
|
||||||
|
|
||||||
// ── 6. Reorder: reverse ONLY the original 4 forecast items ──────────────
|
|
||||||
log("REORDER", "Reversing forecast item order via PUT...");
|
|
||||||
|
|
||||||
// Only reorder the original items; keep any auto-created ones in place
|
|
||||||
const originalDescs = new Set(items.map((i: any) => i.forecastDescription));
|
|
||||||
const originals = midItems.filter(
|
|
||||||
(i: any) =>
|
|
||||||
originalDescs.has(i.forecastDescription) &&
|
|
||||||
items.some((o: any) => o.id === i.id),
|
|
||||||
);
|
|
||||||
const extras = midItems.filter(
|
|
||||||
(i: any) => !originals.some((o: any) => o.id === i.id),
|
|
||||||
);
|
|
||||||
|
|
||||||
const reversedOriginals = [...originals].reverse();
|
|
||||||
const reorderedAll = [...reversedOriginals, ...extras];
|
|
||||||
|
|
||||||
const clone = JSON.parse(JSON.stringify(midForecast.data));
|
|
||||||
clone.forecastItems = JSON.parse(JSON.stringify(reorderedAll));
|
|
||||||
|
|
||||||
const putRes = await cw.put(forecastUrl, clone);
|
|
||||||
const newItems: any[] = putRes.data.forecastItems ?? [];
|
|
||||||
|
|
||||||
log("REORDER", `After PUT (${newItems.length} items):`);
|
|
||||||
for (const it of newItems) {
|
|
||||||
console.log(
|
|
||||||
` id=${it.id} desc="${it.forecastDescription}" rev=${it.revenue} cost=${it.cost}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build old→new ID map by position (for original items only)
|
|
||||||
const idMap = new Map<number, number>();
|
|
||||||
for (let i = 0; i < reversedOriginals.length && i < newItems.length; i++) {
|
|
||||||
idMap.set(reversedOriginals[i].id, newItems[i].id);
|
|
||||||
}
|
|
||||||
log("ID MAP", "Forecast item Old → New:");
|
|
||||||
for (const [oldId, newId] of idMap) {
|
|
||||||
console.log(` ${oldId} → ${newId}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── 7. Check if procurement products survived PUT ───────────────────────
|
|
||||||
await sleep(300);
|
|
||||||
const afterProc = await cw.get(
|
|
||||||
`/procurement/products?conditions=${encodeURIComponent(`opportunity/id=${oppId}`)}&fields=id,forecastDetailId,cancelledFlag,quantityCancelled,cancelledReason,description`,
|
|
||||||
);
|
|
||||||
const newProcIds = new Set(afterProc.data.map((p: any) => p.id));
|
|
||||||
|
|
||||||
log(
|
|
||||||
"PROCUREMENT SURVIVAL",
|
|
||||||
"Checking if procurement product IDs survived PUT...",
|
|
||||||
);
|
|
||||||
const procSurvived = [...oldProcIds].every((id) => newProcIds.has(id));
|
|
||||||
if (procSurvived) {
|
|
||||||
console.log(" ✓ All original procurement product IDs survived PUT.");
|
|
||||||
} else {
|
|
||||||
console.log(" ✗ PUT REGENERATED procurement product IDs!");
|
|
||||||
console.log(` Before: [${[...oldProcIds].join(", ")}]`);
|
|
||||||
console.log(` After: [${[...newProcIds].join(", ")}]`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try remap if old IDs still exist
|
|
||||||
let remapOk = true;
|
|
||||||
if (procSurvived) {
|
|
||||||
log("REMAP", "Updating procurement products forecastDetailId...");
|
|
||||||
for (const pp of beforeProc.data) {
|
|
||||||
const oldFdId = pp.forecastDetailId as number;
|
|
||||||
const newFdId = idMap.get(oldFdId);
|
|
||||||
if (!newFdId || newFdId === oldFdId) continue;
|
|
||||||
try {
|
|
||||||
await cw.patch(`/procurement/products/${pp.id}`, [
|
|
||||||
{ op: "replace", path: "forecastDetailId", value: newFdId },
|
|
||||||
]);
|
|
||||||
console.log(
|
|
||||||
` ✓ Proc ${pp.id}: forecastDetailId ${oldFdId} → ${newFdId}`,
|
|
||||||
);
|
|
||||||
} catch (e: any) {
|
|
||||||
remapOk = false;
|
|
||||||
console.log(
|
|
||||||
` ✗ Proc ${pp.id} remap failed: ${e.response?.status} ${JSON.stringify(e.response?.data)}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
remapOk = false;
|
|
||||||
log(
|
|
||||||
"REMAP",
|
|
||||||
"⚠ SKIPPED — procurement products were regenerated by PUT; old IDs no longer exist.",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── 8. Verify ───────────────────────────────────────────────────────────
|
|
||||||
await sleep(300);
|
|
||||||
|
|
||||||
// 8a. Verify order (first 4 items)
|
|
||||||
log("VERIFY ORDER", "Expected reverse: Delta, Charlie, Bravo, Alpha");
|
|
||||||
const expectedOrder = ["Delta", "Charlie", "Bravo", "Alpha"];
|
|
||||||
let orderOk = true;
|
|
||||||
for (let i = 0; i < expectedOrder.length; i++) {
|
|
||||||
const actual = newItems[i]?.forecastDescription;
|
|
||||||
const ok = actual === expectedOrder[i];
|
|
||||||
if (!ok) orderOk = false;
|
|
||||||
console.log(
|
|
||||||
` Position ${i}: ${ok ? "✓" : "✗"} expected "${expectedOrder[i]}", got "${actual}"`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 8b. Verify prices (by description)
|
|
||||||
log("VERIFY PRICES", "");
|
|
||||||
let pricesOk = true;
|
|
||||||
for (const item of newItems) {
|
|
||||||
const orig = priceSnap.get(item.forecastDescription);
|
|
||||||
if (!orig) continue;
|
|
||||||
if (item.revenue !== orig.rev || item.cost !== orig.cost) {
|
|
||||||
pricesOk = false;
|
|
||||||
console.log(
|
|
||||||
` ✗ "${item.forecastDescription}": rev ${orig.rev}→${item.revenue}, cost ${orig.cost}→${item.cost}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (pricesOk) console.log(" ✓ All prices preserved.");
|
|
||||||
|
|
||||||
// 8c. Verify cancellation data — match by description since IDs may have changed
|
|
||||||
let cancelOk = true;
|
|
||||||
if (procProducts.length > 0) {
|
|
||||||
log(
|
|
||||||
"VERIFY CANCELLATION",
|
|
||||||
"Checking cancellation data on procurement products after PUT...",
|
|
||||||
);
|
|
||||||
const finalProc = await cw.get(
|
|
||||||
`/procurement/products?conditions=${encodeURIComponent(`opportunity/id=${oppId}`)}&fields=id,forecastDetailId,cancelledFlag,quantityCancelled,cancelledReason,description`,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Track by procIdsToClean for cleanup
|
|
||||||
for (const p of finalProc.data) {
|
|
||||||
if (!procIdsToClean.includes(p.id)) procIdsToClean.push(p.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const pp of finalProc.data) {
|
|
||||||
const orig = beforeByDesc.get(pp.description);
|
|
||||||
if (!orig) {
|
|
||||||
console.log(
|
|
||||||
` ? Proc ${pp.id} "${pp.description}" — no matching pre-PUT record`,
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const cancelledMatch =
|
|
||||||
pp.cancelledFlag === orig.cancelledFlag &&
|
|
||||||
pp.quantityCancelled === orig.quantityCancelled &&
|
|
||||||
(pp.cancelledReason ?? "") === (orig.cancelledReason ?? "");
|
|
||||||
|
|
||||||
if (!cancelledMatch) {
|
|
||||||
cancelOk = false;
|
|
||||||
console.log(
|
|
||||||
` ✗ Proc ${pp.id} "${pp.description}": CANCELLATION DATA CHANGED\n` +
|
|
||||||
` Before: cancelled=${orig.cancelledFlag} qty=${orig.quantityCancelled} reason="${orig.cancelledReason ?? ""}"\n` +
|
|
||||||
` After: cancelled=${pp.cancelledFlag} qty=${pp.quantityCancelled} reason="${pp.cancelledReason ?? ""}"`,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
console.log(
|
|
||||||
` ✓ Proc ${pp.id} "${pp.description}": cancelled=${pp.cancelledFlag} qty=${pp.quantityCancelled} reason="${pp.cancelledReason ?? ""}"`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Summary ─────────────────────────────────────────────────────────────
|
|
||||||
log("SUMMARY", "");
|
|
||||||
console.log(
|
|
||||||
" Order correct: ",
|
|
||||||
orderOk ? "✓ PASS" : "✗ FAIL",
|
|
||||||
);
|
|
||||||
console.log(
|
|
||||||
" Prices preserved: ",
|
|
||||||
pricesOk ? "✓ PASS" : "✗ FAIL",
|
|
||||||
);
|
|
||||||
console.log(
|
|
||||||
" Proc IDs survived PUT: ",
|
|
||||||
procSurvived ? "✓ PASS" : "✗ FAIL",
|
|
||||||
);
|
|
||||||
console.log(
|
|
||||||
" Procurement remap: ",
|
|
||||||
remapOk ? "✓ PASS" : "✗ FAIL (skipped or failed)",
|
|
||||||
);
|
|
||||||
console.log(
|
|
||||||
" Cancellation data preserved:",
|
|
||||||
cancelOk ? "✓ PASS" : "✗ FAIL",
|
|
||||||
);
|
|
||||||
|
|
||||||
const allPass = orderOk && pricesOk && procSurvived && remapOk && cancelOk;
|
|
||||||
log("RESULT", allPass ? "✓ ALL TESTS PASSED" : "✗ SOME TESTS FAILED");
|
|
||||||
} finally {
|
|
||||||
// ── Cleanup ─────────────────────────────────────────────────────────────
|
|
||||||
log("CLEANUP", "Deleting procurement products...");
|
|
||||||
for (const id of procIdsToClean) {
|
|
||||||
try {
|
|
||||||
await cw.delete(`/procurement/products/${id}`);
|
|
||||||
} catch {}
|
|
||||||
}
|
|
||||||
log("CLEANUP", `Deleted ${procIdsToClean.length} procurement products.`);
|
|
||||||
|
|
||||||
log("CLEANUP", `Deleting opportunity ${oppId}...`);
|
|
||||||
try {
|
|
||||||
await cw.delete(`/sales/opportunities/${oppId}`);
|
|
||||||
log("CLEANUP", "✓ Done.");
|
|
||||||
} catch (e: any) {
|
|
||||||
log("CLEANUP", `✗ ${e.response?.status ?? e.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
main().catch((err) => {
|
|
||||||
console.error("\n[FATAL]", err.response?.data ?? err.message);
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
@@ -1,133 +0,0 @@
|
|||||||
// Test script to probe UniFi API endpoints for response shapes
|
|
||||||
import axios, { AxiosInstance } from "axios";
|
|
||||||
|
|
||||||
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
|
|
||||||
|
|
||||||
const controllerBaseUrl = "https://unifi.totaltech.net";
|
|
||||||
const site = "km9b1v8i";
|
|
||||||
const username = "admin";
|
|
||||||
const password = "Tt$Un1fiIZth3B3$t26";
|
|
||||||
|
|
||||||
class TestClient {
|
|
||||||
private client: AxiosInstance;
|
|
||||||
|
|
||||||
constructor(baseURL: string) {
|
|
||||||
this.client = axios.create({
|
|
||||||
baseURL,
|
|
||||||
validateStatus: (s) => s >= 200 && s < 400,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private persistSession(res: { headers: Record<string, unknown> }): void {
|
|
||||||
const raw = res.headers["set-cookie"];
|
|
||||||
if (raw) {
|
|
||||||
const cookies = (Array.isArray(raw) ? raw : [raw]) as string[];
|
|
||||||
const cookieString = cookies.map((c) => c.split(";")[0]).join("; ");
|
|
||||||
this.client.defaults.headers.common["Cookie"] = cookieString;
|
|
||||||
}
|
|
||||||
const csrf = res.headers["x-csrf-token"];
|
|
||||||
if (typeof csrf === "string") {
|
|
||||||
this.client.defaults.headers.common["X-CSRF-Token"] = csrf;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async login(): Promise<void> {
|
|
||||||
try {
|
|
||||||
const res = await this.client.post("/api/auth/login", {
|
|
||||||
username,
|
|
||||||
password,
|
|
||||||
});
|
|
||||||
console.log("Login OK (UniFi OS)", res.status);
|
|
||||||
this.persistSession(res);
|
|
||||||
} catch (e) {
|
|
||||||
const res = await this.client.post("/api/login", { username, password });
|
|
||||||
console.log("Login OK (legacy)", res.status);
|
|
||||||
this.persistSession(res);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async tryGet(label: string, paths: string[]): Promise<any> {
|
|
||||||
for (const path of paths) {
|
|
||||||
try {
|
|
||||||
const res = await this.client.get(path);
|
|
||||||
const data = res.data?.data ?? res.data;
|
|
||||||
console.log(`\n=== ${label} (${path}) ===`);
|
|
||||||
console.log(JSON.stringify(data, null, 2));
|
|
||||||
return data;
|
|
||||||
} catch (e: any) {
|
|
||||||
console.log(` Failed ${path}: ${e.response?.status ?? e.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
console.log(` Could not fetch ${label} from any path`);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
const client = new TestClient(controllerBaseUrl);
|
|
||||||
await client.login();
|
|
||||||
|
|
||||||
// 1. WLAN Groups (AP groups in UniFi)
|
|
||||||
await client.tryGet("WLAN Groups", [
|
|
||||||
`/proxy/network/api/s/${site}/rest/wlangroup`,
|
|
||||||
`/api/s/${site}/rest/wlangroup`,
|
|
||||||
]);
|
|
||||||
|
|
||||||
// 2. User Groups (bandwidth/speed limit profiles)
|
|
||||||
await client.tryGet("User Groups (Speed Profiles)", [
|
|
||||||
`/proxy/network/api/s/${site}/rest/usergroup`,
|
|
||||||
`/api/s/${site}/rest/usergroup`,
|
|
||||||
]);
|
|
||||||
|
|
||||||
// 3. Devices - APs only (compact)
|
|
||||||
const devices = await client.tryGet("Devices", [
|
|
||||||
`/proxy/network/api/s/${site}/stat/device`,
|
|
||||||
]);
|
|
||||||
if (devices) {
|
|
||||||
const aps = devices.filter((d: any) => d.type === "uap");
|
|
||||||
console.log(`\n=== APs (${aps.length}) - compact ===`);
|
|
||||||
aps.forEach((ap: any) => {
|
|
||||||
console.log(
|
|
||||||
JSON.stringify({
|
|
||||||
_id: ap._id,
|
|
||||||
name: ap.name,
|
|
||||||
mac: ap.mac,
|
|
||||||
model: ap.model,
|
|
||||||
radio_table: ap.radio_table?.map((r: any) => ({
|
|
||||||
radio: r.radio,
|
|
||||||
name: r.name,
|
|
||||||
})),
|
|
||||||
wlangroup_id_ng: ap.wlangroup_id_ng,
|
|
||||||
wlangroup_id_na: ap.wlangroup_id_na,
|
|
||||||
vap_table_count: ap.vap_table?.length,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. One full WLAN to see private_preshared_keys structure
|
|
||||||
const wlans = await client.tryGet("WLANs", [
|
|
||||||
`/proxy/network/api/s/${site}/rest/wlanconf`,
|
|
||||||
]);
|
|
||||||
if (wlans) {
|
|
||||||
// Log just the PPSK-related fields from each WLAN
|
|
||||||
console.log("\n=== PPSK fields per WLAN ===");
|
|
||||||
wlans.forEach((w: any) => {
|
|
||||||
console.log(
|
|
||||||
JSON.stringify({
|
|
||||||
name: w.name,
|
|
||||||
_id: w._id,
|
|
||||||
private_preshared_keys_enabled: w.private_preshared_keys_enabled,
|
|
||||||
private_preshared_keys: w.private_preshared_keys,
|
|
||||||
ap_group_ids: w.ap_group_ids,
|
|
||||||
ap_group_mode: w.ap_group_mode,
|
|
||||||
wlan_band: w.wlan_band,
|
|
||||||
wlan_bands: w.wlan_bands,
|
|
||||||
usergroup_id: w.usergroup_id,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
main().catch((e) => console.error(e));
|
|
||||||
@@ -1,123 +0,0 @@
|
|||||||
// unifi-wifi-list.ts
|
|
||||||
import axios, { AxiosInstance } from "axios";
|
|
||||||
|
|
||||||
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
|
|
||||||
|
|
||||||
const controllerBaseUrl = "https://unifi.totaltech.net";
|
|
||||||
const site = "km9b1v8i";
|
|
||||||
const username = "admin";
|
|
||||||
const password = "Tt$Un1fiIZth3B3$t26";
|
|
||||||
|
|
||||||
interface WlanConfRaw {
|
|
||||||
_id: string;
|
|
||||||
name?: string;
|
|
||||||
ssid?: string;
|
|
||||||
x_passphrase?: string;
|
|
||||||
[key: string]: unknown;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface WlanConf {
|
|
||||||
id: string;
|
|
||||||
ssid: string;
|
|
||||||
password: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
class UnifiClient {
|
|
||||||
private client: AxiosInstance;
|
|
||||||
|
|
||||||
constructor(baseURL: string) {
|
|
||||||
this.client = axios.create({
|
|
||||||
baseURL,
|
|
||||||
validateStatus: (s) => s >= 200 && s < 400,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private persistSession(res: { headers: Record<string, unknown> }): void {
|
|
||||||
// Cookies
|
|
||||||
const raw = res.headers["set-cookie"];
|
|
||||||
if (raw) {
|
|
||||||
const cookies = (Array.isArray(raw) ? raw : [raw]) as string[];
|
|
||||||
const cookieString = cookies.map((c) => c.split(";")[0]).join("; ");
|
|
||||||
this.client.defaults.headers.common["Cookie"] = cookieString;
|
|
||||||
}
|
|
||||||
// CSRF token (UniFi OS)
|
|
||||||
const csrf = res.headers["x-csrf-token"];
|
|
||||||
if (typeof csrf === "string") {
|
|
||||||
this.client.defaults.headers.common["X-CSRF-Token"] = csrf;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async login(username: string, password: string): Promise<void> {
|
|
||||||
const body = { username, password };
|
|
||||||
|
|
||||||
try {
|
|
||||||
// UniFi OS
|
|
||||||
const res = await this.client.post("/api/auth/login", body);
|
|
||||||
console.log("Login OK (UniFi OS)", res.status);
|
|
||||||
this.persistSession(res);
|
|
||||||
} catch (e) {
|
|
||||||
// Legacy controller
|
|
||||||
console.log("UniFi OS login failed, trying legacy...");
|
|
||||||
const res = await this.client.post("/api/login", body);
|
|
||||||
console.log("Login OK (legacy)", res.status);
|
|
||||||
this.persistSession(res);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async fetchWlanConfRaw(site: string): Promise<WlanConfRaw[]> {
|
|
||||||
const paths = [
|
|
||||||
`/proxy/network/api/s/${site}/rest/wlanconf`,
|
|
||||||
`/api/s/${site}/rest/wlanconf`,
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const path of paths) {
|
|
||||||
try {
|
|
||||||
const res = await this.client.get(path);
|
|
||||||
const data = (res.data?.data ?? res.data) as WlanConfRaw[];
|
|
||||||
console.log(`Fetched wlan from ${path}`);
|
|
||||||
return data;
|
|
||||||
} catch (e) {
|
|
||||||
console.log(
|
|
||||||
`Failed ${path}:`,
|
|
||||||
axios.isAxiosError(e) ? e.response?.status : e,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error("Could not fetch WLAN config from any known path");
|
|
||||||
}
|
|
||||||
|
|
||||||
async getWlanConf(site: string): Promise<WlanConf[]> {
|
|
||||||
const raw = await this.fetchWlanConfRaw(site);
|
|
||||||
|
|
||||||
return raw.map(
|
|
||||||
(w): WlanConf => ({
|
|
||||||
id: w._id,
|
|
||||||
ssid: (w.name || w.ssid || "").toString(),
|
|
||||||
password: typeof w.x_passphrase === "string" ? w.x_passphrase : null,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
const unifi = new UnifiClient(controllerBaseUrl);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await unifi.login(username, password);
|
|
||||||
|
|
||||||
const wlans = await unifi.getWlanConf(site);
|
|
||||||
|
|
||||||
wlans.forEach((wlan) => {
|
|
||||||
console.log(`${wlan.ssid}: ${wlan.password ?? "<no password>"}`);
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
if (axios.isAxiosError(err)) {
|
|
||||||
console.error("HTTP error", err.response?.status, err.response?.data);
|
|
||||||
} else {
|
|
||||||
console.error("Error", err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
main().catch((e) => console.error(e));
|
|
||||||
@@ -1,123 +0,0 @@
|
|||||||
// unifi-wifi-list.ts
|
|
||||||
import axios, { AxiosInstance } from "axios";
|
|
||||||
|
|
||||||
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
|
|
||||||
|
|
||||||
const controllerBaseUrl = "https://unifi.totaltech.net";
|
|
||||||
const site = "km9b1v8i";
|
|
||||||
const username = "admin";
|
|
||||||
const password = "Tt$Un1fiIZth3B3$t26";
|
|
||||||
|
|
||||||
interface WlanConfRaw {
|
|
||||||
_id: string;
|
|
||||||
name?: string;
|
|
||||||
ssid?: string;
|
|
||||||
x_passphrase?: string;
|
|
||||||
[key: string]: unknown;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface WlanConf {
|
|
||||||
id: string;
|
|
||||||
ssid: string;
|
|
||||||
password: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
class UnifiClient {
|
|
||||||
private client: AxiosInstance;
|
|
||||||
|
|
||||||
constructor(baseURL: string) {
|
|
||||||
this.client = axios.create({
|
|
||||||
baseURL,
|
|
||||||
validateStatus: (s) => s >= 200 && s < 400,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private persistSession(res: { headers: Record<string, unknown> }): void {
|
|
||||||
// Cookies
|
|
||||||
const raw = res.headers["set-cookie"];
|
|
||||||
if (raw) {
|
|
||||||
const cookies = (Array.isArray(raw) ? raw : [raw]) as string[];
|
|
||||||
const cookieString = cookies.map((c) => c.split(";")[0]).join("; ");
|
|
||||||
this.client.defaults.headers.common["Cookie"] = cookieString;
|
|
||||||
}
|
|
||||||
// CSRF token (UniFi OS)
|
|
||||||
const csrf = res.headers["x-csrf-token"];
|
|
||||||
if (typeof csrf === "string") {
|
|
||||||
this.client.defaults.headers.common["X-CSRF-Token"] = csrf;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async login(username: string, password: string): Promise<void> {
|
|
||||||
const body = { username, password };
|
|
||||||
|
|
||||||
try {
|
|
||||||
// UniFi OS
|
|
||||||
const res = await this.client.post("/api/auth/login", body);
|
|
||||||
console.log("Login OK (UniFi OS)", res.status);
|
|
||||||
this.persistSession(res);
|
|
||||||
} catch (e) {
|
|
||||||
// Legacy controller
|
|
||||||
console.log("UniFi OS login failed, trying legacy...");
|
|
||||||
const res = await this.client.post("/api/login", body);
|
|
||||||
console.log("Login OK (legacy)", res.status);
|
|
||||||
this.persistSession(res);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async fetchWlanConfRaw(site: string): Promise<WlanConfRaw[]> {
|
|
||||||
const paths = [
|
|
||||||
`/proxy/network/api/s/${site}/rest/wlanconf`,
|
|
||||||
`/api/s/${site}/rest/wlanconf`,
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const path of paths) {
|
|
||||||
try {
|
|
||||||
const res = await this.client.get(path);
|
|
||||||
const data = (res.data?.data ?? res.data) as WlanConfRaw[];
|
|
||||||
console.log(`Fetched wlan from ${path}`);
|
|
||||||
return data;
|
|
||||||
} catch (e) {
|
|
||||||
console.log(
|
|
||||||
`Failed ${path}:`,
|
|
||||||
axios.isAxiosError(e) ? e.response?.status : e,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error("Could not fetch WLAN config from any known path");
|
|
||||||
}
|
|
||||||
|
|
||||||
async getWlanConf(site: string): Promise<WlanConf[]> {
|
|
||||||
const raw = await this.fetchWlanConfRaw(site);
|
|
||||||
|
|
||||||
return raw.map(
|
|
||||||
(w): WlanConf => ({
|
|
||||||
id: w._id,
|
|
||||||
ssid: (w.name || w.ssid || "").toString(),
|
|
||||||
password: typeof w.x_passphrase === "string" ? w.x_passphrase : null,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
const unifi = new UnifiClient(controllerBaseUrl);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await unifi.login(username, password);
|
|
||||||
|
|
||||||
const wlans = await unifi.getWlanConf(site);
|
|
||||||
|
|
||||||
wlans.forEach((wlan) => {
|
|
||||||
console.log(`${wlan.ssid}: ${wlan.password ?? "<no password>"}`);
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
if (axios.isAxiosError(err)) {
|
|
||||||
console.error("HTTP error", err.response?.status, err.response?.data);
|
|
||||||
} else {
|
|
||||||
console.error("Error", err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
main().catch((e) => console.error(e));
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
import { PrismaPg } from "@prisma/adapter-pg";
|
|
||||||
import { PrismaClient } from "./generated/prisma/client";
|
|
||||||
import fs from "node:fs";
|
|
||||||
|
|
||||||
const env = fs.readFileSync(".env", "utf8");
|
|
||||||
const line = env.split(/\n/).find((l) => l.startsWith("DATABASE_URL="));
|
|
||||||
const url = line?.slice("DATABASE_URL=".length)?.replace(/^"|"$/g, "");
|
|
||||||
|
|
||||||
const prisma = new PrismaClient({
|
|
||||||
adapter: new PrismaPg({ connectionString: url }),
|
|
||||||
});
|
|
||||||
|
|
||||||
const rows = await prisma.$queryRaw`select tablename from pg_tables where schemaname = 'public' order by tablename`;
|
|
||||||
console.log(rows);
|
|
||||||
await prisma.$disconnect();
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
import { prisma } from "./src/constants";
|
|
||||||
|
|
||||||
const roles = await prisma.role.findMany({ select: { title: true, moniker: true, permissions: true } });
|
|
||||||
|
|
||||||
for (const role of roles) {
|
|
||||||
const token = role.permissions;
|
|
||||||
if (!token) { console.log(role.title, "- NO PERMISSIONS"); continue; }
|
|
||||||
try {
|
|
||||||
const payload = JSON.parse(Buffer.from(token.split(".")[1], "base64").toString());
|
|
||||||
const perms: string[] = payload.permissions || [];
|
|
||||||
const relevant = perms.filter((p: string) =>
|
|
||||||
p.includes("sales.opportunity.product") ||
|
|
||||||
p.includes("sales.opportunity.*") ||
|
|
||||||
p.includes("sales.*") ||
|
|
||||||
p === "*"
|
|
||||||
);
|
|
||||||
if (relevant.length > 0) {
|
|
||||||
console.log("=== " + role.title + " (" + role.moniker + ") ===");
|
|
||||||
relevant.forEach((p: string) => console.log(" " + p));
|
|
||||||
}
|
|
||||||
} catch(e: any) { console.log(role.title, "- parse error:", e.message); }
|
|
||||||
}
|
|
||||||
|
|
||||||
await prisma.$disconnect();
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
import { prisma } from "./src/constants";
|
|
||||||
|
|
||||||
const roles = await prisma.role.findMany({ select: { title: true, moniker: true, permissions: true } });
|
|
||||||
|
|
||||||
for (const role of roles) {
|
|
||||||
const token = role.permissions;
|
|
||||||
if (!token) { console.log(role.title, "- NO PERMISSIONS"); continue; }
|
|
||||||
try {
|
|
||||||
const payload = JSON.parse(Buffer.from(token.split(".")[1], "base64").toString());
|
|
||||||
const perms: string[] = payload.permissions || [];
|
|
||||||
// Check for obj.catalogItem or wildcard that would cover it
|
|
||||||
const relevant = perms.filter((p: string) =>
|
|
||||||
p.includes("obj.catalogItem") ||
|
|
||||||
p.includes("obj.*") ||
|
|
||||||
p === "*"
|
|
||||||
);
|
|
||||||
console.log("=== " + role.title + " (" + role.moniker + ") ===");
|
|
||||||
console.log(" obj.catalogItem-related perms:", JSON.stringify(relevant));
|
|
||||||
// Also show all perms for debugging
|
|
||||||
console.log(" Total perms:", perms.length);
|
|
||||||
} catch(e: any) { console.log(role.title, "- parse error:", e.message); }
|
|
||||||
}
|
|
||||||
|
|
||||||
await prisma.$disconnect();
|
|
||||||
@@ -621,7 +621,7 @@
|
|||||||
|
|
||||||
"@types/aria-query": ["@types/aria-query@5.0.4", "", {}, "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw=="],
|
"@types/aria-query": ["@types/aria-query@5.0.4", "", {}, "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw=="],
|
||||||
|
|
||||||
"@types/bun": ["@types/bun@1.3.11", "", { "dependencies": { "bun-types": "1.3.11" } }, "sha512-5vPne5QvtpjGpsGYXiFyycfpDF2ECyPcTSsFBMa0fraoxiQyMJ3SmuQIGhzPg2WJuWxVBoxWJ2kClYTcw/4fAg=="],
|
"@types/bun": ["@types/bun@1.3.12", "", { "dependencies": { "bun-types": "1.3.12" } }, "sha512-DBv81elK+/VSwXHDlnH3Qduw+KxkTIWi7TXkAeh24zpi5l0B2kUg9Ga3tb4nJaPcOFswflgi/yAvMVBPrxMB+A=="],
|
||||||
|
|
||||||
"@types/cacheable-request": ["@types/cacheable-request@6.0.3", "", { "dependencies": { "@types/http-cache-semantics": "*", "@types/keyv": "^3.1.4", "@types/node": "*", "@types/responselike": "^1.0.0" } }, "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw=="],
|
"@types/cacheable-request": ["@types/cacheable-request@6.0.3", "", { "dependencies": { "@types/http-cache-semantics": "*", "@types/keyv": "^3.1.4", "@types/node": "*", "@types/responselike": "^1.0.0" } }, "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw=="],
|
||||||
|
|
||||||
@@ -841,7 +841,7 @@
|
|||||||
|
|
||||||
"buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="],
|
"buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="],
|
||||||
|
|
||||||
"bun-types": ["bun-types@1.3.11", "", { "dependencies": { "@types/node": "*" } }, "sha512-1KGPpoxQWl9f6wcZh57LvrPIInQMn2TQ7jsgxqpRzg+l0QPOFvJVH7HmvHo/AiPgwXy+/Thf6Ov3EdVn1vOabg=="],
|
"bun-types": ["bun-types@1.3.12", "", { "dependencies": { "@types/node": "*" } }, "sha512-HqOLj5PoFajAQciOMRiIZGNoKxDJSr6qigAttOX40vJuSp6DN/CxWp9s3C1Xwm4oH7ybueITwiaOcWXoYVoRkA=="],
|
||||||
|
|
||||||
"bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="],
|
"bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="],
|
||||||
|
|
||||||
|
|||||||
@@ -294,6 +294,22 @@ const refreshContextFromApi = async (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const cwMembers = await apiPrisma.cwMember.findMany({
|
||||||
|
select: { cwMemberId: true, identifier: true },
|
||||||
|
});
|
||||||
|
for (const member of cwMembers) {
|
||||||
|
if (
|
||||||
|
member.cwMemberId != null &&
|
||||||
|
member.identifier &&
|
||||||
|
!context.userIdentifiersByMemberRecId.has(member.cwMemberId)
|
||||||
|
) {
|
||||||
|
context.userIdentifiersByMemberRecId.set(
|
||||||
|
member.cwMemberId,
|
||||||
|
member.identifier
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for (const board of boards) {
|
for (const board of boards) {
|
||||||
context.serviceTicketBoardUidsById.set(board.id, board.uid);
|
context.serviceTicketBoardUidsById.set(board.id, board.uid);
|
||||||
}
|
}
|
||||||
@@ -426,6 +442,12 @@ const sanitizeModelData = (
|
|||||||
) {
|
) {
|
||||||
sanitized.statusId = null;
|
sanitized.statusId = null;
|
||||||
}
|
}
|
||||||
|
if (
|
||||||
|
sanitized.locationId != null &&
|
||||||
|
!context.corporateLocationIds.has(sanitized.locationId as number)
|
||||||
|
) {
|
||||||
|
sanitized.locationId = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (targetModel === "schedule") {
|
if (targetModel === "schedule") {
|
||||||
@@ -734,6 +756,11 @@ const getConfigForTable = (table: string): SyncTableConfig | null => {
|
|||||||
secondarySalesFlag: true,
|
secondarySalesFlag: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
soOppStatus: {
|
||||||
|
select: {
|
||||||
|
closedFlag: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
+208
-15
@@ -75,6 +75,136 @@ type DeleteResult = {
|
|||||||
|
|
||||||
let incrementalDeleteStepIndex = 0;
|
let incrementalDeleteStepIndex = 0;
|
||||||
|
|
||||||
|
const CRITICAL_INCREMENTAL_RECONCILE_TABLES = new Set([
|
||||||
|
"Companies",
|
||||||
|
"Company Addresses",
|
||||||
|
"Contacts",
|
||||||
|
"Opportunities",
|
||||||
|
]);
|
||||||
|
|
||||||
|
const CRITICAL_CW_WATERMARK_TABLES = new Set([
|
||||||
|
"Companies",
|
||||||
|
"Company Addresses",
|
||||||
|
"Contacts",
|
||||||
|
"Opportunities",
|
||||||
|
]);
|
||||||
|
|
||||||
|
const criticalFullSyncIntervalMinutes = Math.max(
|
||||||
|
1,
|
||||||
|
Number.parseInt(
|
||||||
|
process.env.DALPURI_CRITICAL_FULL_SYNC_INTERVAL_MINUTES ?? "15",
|
||||||
|
10
|
||||||
|
) || 15
|
||||||
|
);
|
||||||
|
|
||||||
|
const CRITICAL_FULL_SYNC_INTERVAL_MS =
|
||||||
|
criticalFullSyncIntervalMinutes * 60 * 1000;
|
||||||
|
|
||||||
|
const criticalCwWatermarkOverlapSeconds = Math.max(
|
||||||
|
5,
|
||||||
|
Number.parseInt(
|
||||||
|
process.env.DALPURI_CRITICAL_CW_WATERMARK_OVERLAP_SECONDS ?? "60",
|
||||||
|
10
|
||||||
|
) || 60
|
||||||
|
);
|
||||||
|
|
||||||
|
const CRITICAL_CW_WATERMARK_OVERLAP_MS =
|
||||||
|
criticalCwWatermarkOverlapSeconds * 1000;
|
||||||
|
|
||||||
|
const criticalCwDeltaLimit = Math.max(
|
||||||
|
100,
|
||||||
|
Number.parseInt(process.env.DALPURI_CRITICAL_CW_DELTA_LIMIT ?? "5000", 10) ||
|
||||||
|
5000
|
||||||
|
);
|
||||||
|
|
||||||
|
const lastCriticalFullSyncByStep = new Map<string, number>();
|
||||||
|
const lastCriticalCwWatermarkByStep = new Map<string, Date>();
|
||||||
|
|
||||||
|
const shouldForceCriticalFullSync = (
|
||||||
|
step: Step,
|
||||||
|
forceIncremental: boolean
|
||||||
|
): boolean => {
|
||||||
|
if (!forceIncremental) return false;
|
||||||
|
if (!CRITICAL_INCREMENTAL_RECONCILE_TABLES.has(step.name)) return false;
|
||||||
|
|
||||||
|
const now = Date.now();
|
||||||
|
const last = lastCriticalFullSyncByStep.get(step.name) ?? 0;
|
||||||
|
|
||||||
|
if (now - last < CRITICAL_FULL_SYNC_INTERVAL_MS) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
lastCriticalFullSyncByStep.set(step.name, now);
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const computeCriticalCwWatermarkDecision = async (
|
||||||
|
cwPrisma: CwPrismaClient,
|
||||||
|
step: Step,
|
||||||
|
forceIncremental: boolean
|
||||||
|
): Promise<SmartSyncDecision | null> => {
|
||||||
|
if (!forceIncremental) return null;
|
||||||
|
if (!CRITICAL_CW_WATERMARK_TABLES.has(step.name)) return null;
|
||||||
|
|
||||||
|
const cwDelegate = (
|
||||||
|
cwPrisma as unknown as Record<string, { findMany: Function } | undefined>
|
||||||
|
)[step.sourceModel];
|
||||||
|
|
||||||
|
if (!cwDelegate) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingWhere =
|
||||||
|
(step.sourceArgs as Record<string, unknown> | undefined)?.where ?? {};
|
||||||
|
|
||||||
|
const lastWatermark = lastCriticalCwWatermarkByStep.get(step.name);
|
||||||
|
const lowerBound = lastWatermark
|
||||||
|
? new Date(lastWatermark.getTime() - CRITICAL_CW_WATERMARK_OVERLAP_MS)
|
||||||
|
: new Date(Date.now() - CRITICAL_CW_WATERMARK_OVERLAP_MS);
|
||||||
|
|
||||||
|
const rows = (await cwDelegate.findMany({
|
||||||
|
select: {
|
||||||
|
[step.sourceIdField]: true,
|
||||||
|
[step.sourceUpdatedField]: true,
|
||||||
|
},
|
||||||
|
where: {
|
||||||
|
...(existingWhere as Record<string, unknown>),
|
||||||
|
[step.sourceUpdatedField]: {
|
||||||
|
gte: lowerBound,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
orderBy: { [step.sourceUpdatedField]: "asc" },
|
||||||
|
take: criticalCwDeltaLimit,
|
||||||
|
})) as Row[];
|
||||||
|
|
||||||
|
if (rows.length >= criticalCwDeltaLimit) {
|
||||||
|
console.warn(
|
||||||
|
` [smart-sync][critical-watermark] ${step.name}: delta reached limit (${criticalCwDeltaLimit}), forcing full sync`
|
||||||
|
);
|
||||||
|
return { mode: "full", differences: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rows.length > 0) {
|
||||||
|
const latest = rows[rows.length - 1][step.sourceUpdatedField] as Date | null;
|
||||||
|
if (latest) {
|
||||||
|
lastCriticalCwWatermarkByStep.set(step.name, latest);
|
||||||
|
}
|
||||||
|
} else if (!lastWatermark) {
|
||||||
|
lastCriticalCwWatermarkByStep.set(step.name, new Date());
|
||||||
|
}
|
||||||
|
|
||||||
|
const sourceIds = rows.map((r) => r[step.sourceIdField] as number);
|
||||||
|
console.log(
|
||||||
|
` [smart-sync][critical-watermark] ${step.name}: ${sourceIds.length} ids since ${lowerBound.toISOString()}`
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
mode: "incremental",
|
||||||
|
sourceIds,
|
||||||
|
differences: [],
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const parseEnvFile = (path: string): Record<string, string> => {
|
const parseEnvFile = (path: string): Record<string, string> => {
|
||||||
const envData = readFileSync(path, "utf8");
|
const envData = readFileSync(path, "utf8");
|
||||||
const out: Record<string, string> = {};
|
const out: Record<string, string> = {};
|
||||||
@@ -107,6 +237,20 @@ const resolveApiDatabaseUrl = (): string => {
|
|||||||
if (process.env.OPTIMA_API_DATABASE_URL)
|
if (process.env.OPTIMA_API_DATABASE_URL)
|
||||||
return process.env.OPTIMA_API_DATABASE_URL;
|
return process.env.OPTIMA_API_DATABASE_URL;
|
||||||
|
|
||||||
|
// Worker/runtime fallback:
|
||||||
|
// In Kubernetes we often provide CW via CW_DATABASE_URL and API Postgres via
|
||||||
|
// DATABASE_URL. Only use DATABASE_URL as API when we can safely infer that.
|
||||||
|
if (process.env.CW_DATABASE_URL && process.env.DATABASE_URL) {
|
||||||
|
return process.env.DATABASE_URL;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
process.env.DATABASE_URL &&
|
||||||
|
/^(postgres|postgresql):\/\//i.test(process.env.DATABASE_URL)
|
||||||
|
) {
|
||||||
|
return process.env.DATABASE_URL;
|
||||||
|
}
|
||||||
|
|
||||||
const candidates = [
|
const candidates = [
|
||||||
resolve(import.meta.dir, "../../api/.env"),
|
resolve(import.meta.dir, "../../api/.env"),
|
||||||
resolve(process.cwd(), "../api/.env"),
|
resolve(process.cwd(), "../api/.env"),
|
||||||
@@ -323,6 +467,22 @@ const refreshContextFromApi = async (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const cwMembers = await apiPrisma.cwMember.findMany({
|
||||||
|
select: { cwMemberId: true, identifier: true },
|
||||||
|
});
|
||||||
|
for (const member of cwMembers) {
|
||||||
|
if (
|
||||||
|
member.cwMemberId != null &&
|
||||||
|
member.identifier &&
|
||||||
|
!context.userIdentifiersByMemberRecId.has(member.cwMemberId)
|
||||||
|
) {
|
||||||
|
context.userIdentifiersByMemberRecId.set(
|
||||||
|
member.cwMemberId,
|
||||||
|
member.identifier
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for (const board of boards) {
|
for (const board of boards) {
|
||||||
context.serviceTicketBoardUidsById.set(board.id, board.uid);
|
context.serviceTicketBoardUidsById.set(board.id, board.uid);
|
||||||
}
|
}
|
||||||
@@ -636,6 +796,13 @@ const sanitizeModelData = (
|
|||||||
) {
|
) {
|
||||||
sanitized.stageId = null;
|
sanitized.stageId = null;
|
||||||
}
|
}
|
||||||
|
// Nullify locationId if the corporate location doesn't exist
|
||||||
|
if (
|
||||||
|
sanitized.locationId != null &&
|
||||||
|
!context.corporateLocationIds.has(sanitized.locationId as number)
|
||||||
|
) {
|
||||||
|
sanitized.locationId = null;
|
||||||
|
}
|
||||||
// Nullify taxCodeId if the tax code hasn't synced yet
|
// Nullify taxCodeId if the tax code hasn't synced yet
|
||||||
if (
|
if (
|
||||||
sanitized.taxCodeId != null &&
|
sanitized.taxCodeId != null &&
|
||||||
@@ -1328,6 +1495,15 @@ export const executeFullDalpuriSync = async (options?: {
|
|||||||
const isTimedOut = () => Date.now() - syncStartTime > timeoutMs;
|
const isTimedOut = () => Date.now() - syncStartTime > timeoutMs;
|
||||||
|
|
||||||
const steps: Step[] = [
|
const steps: Step[] = [
|
||||||
|
{
|
||||||
|
name: "CW Members",
|
||||||
|
sourceModel: "member",
|
||||||
|
targetModel: "cwMember",
|
||||||
|
translation: cwMemberTranslation as unknown as AnyTranslation,
|
||||||
|
uniqueField: "cwMemberId",
|
||||||
|
sourceIdField: "memberRecId",
|
||||||
|
sourceUpdatedField: "lastUpdatedUtc",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "Users",
|
name: "Users",
|
||||||
sourceModel: "member",
|
sourceModel: "member",
|
||||||
@@ -1342,15 +1518,6 @@ export const executeFullDalpuriSync = async (options?: {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: "CW Members",
|
|
||||||
sourceModel: "member",
|
|
||||||
targetModel: "cwMember",
|
|
||||||
translation: cwMemberTranslation as unknown as AnyTranslation,
|
|
||||||
uniqueField: "cwMemberId",
|
|
||||||
sourceIdField: "memberRecId",
|
|
||||||
sourceUpdatedField: "lastUpdatedUtc",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: "Companies",
|
name: "Companies",
|
||||||
sourceModel: "company",
|
sourceModel: "company",
|
||||||
@@ -1585,6 +1752,11 @@ export const executeFullDalpuriSync = async (options?: {
|
|||||||
secondarySalesFlag: true,
|
secondarySalesFlag: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
soOppStatus: {
|
||||||
|
select: {
|
||||||
|
closedFlag: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -1729,19 +1901,40 @@ export const executeFullDalpuriSync = async (options?: {
|
|||||||
step,
|
step,
|
||||||
forceIncremental
|
forceIncremental
|
||||||
);
|
);
|
||||||
|
const criticalWatermarkDecision = await computeCriticalCwWatermarkDecision(
|
||||||
|
cwPrisma,
|
||||||
|
step,
|
||||||
|
forceIncremental
|
||||||
|
);
|
||||||
|
const forceCriticalFullSync = shouldForceCriticalFullSync(
|
||||||
|
step,
|
||||||
|
forceIncremental
|
||||||
|
);
|
||||||
|
const effectiveDecision = forceCriticalFullSync
|
||||||
|
? ({ mode: "full", differences: decision.differences } as SmartSyncDecision)
|
||||||
|
: criticalWatermarkDecision ?? decision;
|
||||||
|
|
||||||
|
if (forceCriticalFullSync) {
|
||||||
|
console.log(
|
||||||
|
` [smart-sync][forced-full] ${step.name}: forcing periodic full reconciliation every ${criticalFullSyncIntervalMinutes}m`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const sourceIdsFilter =
|
const sourceIdsFilter =
|
||||||
decision.mode === "incremental" ? decision.sourceIds : undefined;
|
effectiveDecision.mode === "incremental"
|
||||||
|
? effectiveDecision.sourceIds
|
||||||
|
: undefined;
|
||||||
console.log(
|
console.log(
|
||||||
` [smart-sync]${forceIncremental ? "[forced]" : ""} mode=${
|
` [smart-sync]${forceIncremental ? "[forced]" : ""} mode=${
|
||||||
decision.mode
|
effectiveDecision.mode
|
||||||
}${
|
}${
|
||||||
decision.mode === "incremental"
|
effectiveDecision.mode === "incremental"
|
||||||
? ` (${decision.sourceIds.length} ids)`
|
? ` (${effectiveDecision.sourceIds.length} ids)`
|
||||||
: ""
|
: ""
|
||||||
}`
|
}`
|
||||||
);
|
);
|
||||||
if (logAllDifferences) {
|
if (logAllDifferences) {
|
||||||
logAllSmartSyncDifferences(step, decision.differences);
|
logAllSmartSyncDifferences(step, effectiveDecision.differences);
|
||||||
}
|
}
|
||||||
const result = await syncStep(
|
const result = await syncStep(
|
||||||
cwPrisma,
|
cwPrisma,
|
||||||
@@ -1763,7 +1956,7 @@ export const executeFullDalpuriSync = async (options?: {
|
|||||||
|
|
||||||
await writeStepLog(
|
await writeStepLog(
|
||||||
step.name,
|
step.name,
|
||||||
decision.mode,
|
effectiveDecision.mode,
|
||||||
result,
|
result,
|
||||||
{ deleted: 0, failed: 0 },
|
{ deleted: 0, failed: 0 },
|
||||||
Date.now() - stepStart
|
Date.now() - stepStart
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ export const contactTranslation: Translation<CwContact, ApiContact> = {
|
|||||||
{
|
{
|
||||||
from: "lastName",
|
from: "lastName",
|
||||||
to: "lastName",
|
to: "lastName",
|
||||||
process: (value) => (value ? value : "Contact"),
|
process: (value) => (value ? value : ""),
|
||||||
},
|
},
|
||||||
{ from: "nickName", to: "nickname" },
|
{ from: "nickName", to: "nickname" },
|
||||||
{ from: "title", to: "title" },
|
{ from: "title", to: "title" },
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
Opportunity as CwOpportunity,
|
Opportunity as CwOpportunity,
|
||||||
OpportunityMember as CwOpportunityMember,
|
OpportunityMember as CwOpportunityMember,
|
||||||
|
SoOppStatus as CwSoOppStatus,
|
||||||
} from "../../generated/prisma/client";
|
} from "../../generated/prisma/client";
|
||||||
import { OpportunityInterest } from "../../../api/generated/prisma/client";
|
import { OpportunityInterest } from "../../../api/generated/prisma/client";
|
||||||
import { Translation, skipRow } from "./types";
|
import { Translation, skipRow } from "./types";
|
||||||
@@ -30,6 +31,7 @@ type ApiOpportunityRecord = {
|
|||||||
dateBecameLead?: Date | null;
|
dateBecameLead?: Date | null;
|
||||||
closedDate?: Date | null;
|
closedDate?: Date | null;
|
||||||
closedFlag: boolean;
|
closedFlag: boolean;
|
||||||
|
locationId?: number | null;
|
||||||
closedById?: string | null;
|
closedById?: string | null;
|
||||||
updatedBy: string;
|
updatedBy: string;
|
||||||
eneteredBy: string;
|
eneteredBy: string;
|
||||||
@@ -42,6 +44,7 @@ type CwOpportunityWithMembers = CwOpportunity & {
|
|||||||
CwOpportunityMember,
|
CwOpportunityMember,
|
||||||
"memberRecId" | "primarySalesFlag" | "secondarySalesFlag"
|
"memberRecId" | "primarySalesFlag" | "secondarySalesFlag"
|
||||||
>[];
|
>[];
|
||||||
|
soOppStatus?: Pick<CwSoOppStatus, "closedFlag"> | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const toInterest = (value: number | null): OpportunityInterest | null => {
|
const toInterest = (value: number | null): OpportunityInterest | null => {
|
||||||
@@ -119,13 +122,19 @@ export const opportunityTranslation: Translation<
|
|||||||
},
|
},
|
||||||
{ from: "companyRecId", to: "companyId" },
|
{ from: "companyRecId", to: "companyId" },
|
||||||
{ from: "contactRecId", to: "contactId" },
|
{ from: "contactRecId", to: "contactId" },
|
||||||
|
{ from: "ownerLevelRecId", to: "locationId" },
|
||||||
{ from: "companyAddressRecId", to: "siteId" },
|
{ from: "companyAddressRecId", to: "siteId" },
|
||||||
{ from: "poNumber", to: "customerPO" },
|
{ from: "poNumber", to: "customerPO" },
|
||||||
{ from: "dateCloseExpected", to: "expectedCloseDate" },
|
{ from: "dateCloseExpected", to: "expectedCloseDate" },
|
||||||
{ from: "datePipelineChange", to: "pipelineChangeDate" },
|
{ from: "datePipelineChange", to: "pipelineChangeDate" },
|
||||||
{ from: "dateBecameLead", to: "dateBecameLead" },
|
{ from: "dateBecameLead", to: "dateBecameLead" },
|
||||||
{ from: "dateClosed", to: "closedDate" },
|
{ from: "dateClosed", to: "closedDate" },
|
||||||
{ from: "oldCloseFlag", to: "closedFlag" },
|
{
|
||||||
|
from: "oldCloseFlag",
|
||||||
|
to: "closedFlag",
|
||||||
|
process: (_value, _context, row) =>
|
||||||
|
row.soOppStatus?.closedFlag ?? row.oldCloseFlag ?? false,
|
||||||
|
},
|
||||||
{ from: "closedBy", to: "closedById" },
|
{ from: "closedBy", to: "closedById" },
|
||||||
{
|
{
|
||||||
from: "updatedBy",
|
from: "updatedBy",
|
||||||
|
|||||||
@@ -1,76 +0,0 @@
|
|||||||
import { PrismaMssql } from "@prisma/adapter-mssql";
|
|
||||||
import { PrismaClient } from "./generated/prisma/client";
|
|
||||||
|
|
||||||
const connectionString = process.env.DATABASE_URL;
|
|
||||||
if (!connectionString) {
|
|
||||||
throw new Error("DATABASE_URL is not set.");
|
|
||||||
}
|
|
||||||
|
|
||||||
const prisma = new PrismaClient({
|
|
||||||
adapter: new PrismaMssql(connectionString),
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
const rowSummary = await prisma.$queryRawUnsafe<
|
|
||||||
Array<{ total_rows: number; distinct_configs: number }>
|
|
||||||
>(`
|
|
||||||
SELECT
|
|
||||||
COUNT(*) AS total_rows,
|
|
||||||
COUNT(DISTINCT Config_RecID) AS distinct_configs
|
|
||||||
FROM dbo.Config_User_Defined_Field_Value;
|
|
||||||
`);
|
|
||||||
|
|
||||||
const relatedRowCounts = await prisma.$queryRawUnsafe<
|
|
||||||
Array<{
|
|
||||||
config_rows: number;
|
|
||||||
cs_result_detail_rows: number;
|
|
||||||
config_custom_field_nonempty: number;
|
|
||||||
}>
|
|
||||||
>(`
|
|
||||||
SELECT
|
|
||||||
(SELECT COUNT(*) FROM dbo.Config) AS config_rows,
|
|
||||||
(SELECT COUNT(*) FROM dbo.CS_Result_Detail) AS cs_result_detail_rows,
|
|
||||||
(SELECT COUNT(*)
|
|
||||||
FROM dbo.Config
|
|
||||||
WHERE Custom_Field IS NOT NULL
|
|
||||||
AND LEN(LTRIM(RTRIM(CONVERT(nvarchar(max), Custom_Field)))) > 0) AS config_custom_field_nonempty;
|
|
||||||
`);
|
|
||||||
|
|
||||||
const topConfigs = await prisma.$queryRawUnsafe<
|
|
||||||
Array<{ config_recid: number; field_count: number }>
|
|
||||||
>(`
|
|
||||||
SELECT TOP 10
|
|
||||||
Config_RecID AS config_recid,
|
|
||||||
COUNT(*) AS field_count
|
|
||||||
FROM dbo.Config_User_Defined_Field_Value
|
|
||||||
GROUP BY Config_RecID
|
|
||||||
ORDER BY field_count DESC, config_recid ASC;
|
|
||||||
`);
|
|
||||||
|
|
||||||
const customFieldSamples = await prisma.$queryRawUnsafe<
|
|
||||||
Array<{ config_recid: number; custom_field_prefix: string }>
|
|
||||||
>(`
|
|
||||||
SELECT TOP 5
|
|
||||||
Config_RecID AS config_recid,
|
|
||||||
LEFT(CONVERT(nvarchar(max), Custom_Field), 250) AS custom_field_prefix
|
|
||||||
FROM dbo.Config
|
|
||||||
WHERE Custom_Field IS NOT NULL
|
|
||||||
AND LEN(LTRIM(RTRIM(CONVERT(nvarchar(max), Custom_Field)))) > 0
|
|
||||||
ORDER BY Config_RecID ASC;
|
|
||||||
`);
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
JSON.stringify(
|
|
||||||
{
|
|
||||||
rowSummary: rowSummary[0] ?? null,
|
|
||||||
relatedRowCounts: relatedRowCounts[0] ?? null,
|
|
||||||
topConfigs,
|
|
||||||
customFieldSamples,
|
|
||||||
},
|
|
||||||
null,
|
|
||||||
2,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} finally {
|
|
||||||
await prisma.$disconnect();
|
|
||||||
}
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
import { PrismaMssql } from "@prisma/adapter-mssql";
|
|
||||||
import { PrismaClient } from "./generated/prisma/client";
|
|
||||||
import { writeFileSync } from "node:fs";
|
|
||||||
|
|
||||||
const outputPath =
|
|
||||||
process.argv[2] ??
|
|
||||||
process.env.CONFIG_OUTPUT_FILE ??
|
|
||||||
"configurations-first-10-with-relations.json";
|
|
||||||
|
|
||||||
const connectionString = process.env.DATABASE_URL;
|
|
||||||
if (!connectionString) {
|
|
||||||
throw new Error("DATABASE_URL is not set.");
|
|
||||||
}
|
|
||||||
|
|
||||||
const adapter = new PrismaMssql(connectionString);
|
|
||||||
const prisma = new PrismaClient({ adapter });
|
|
||||||
|
|
||||||
try {
|
|
||||||
const configurations = await prisma.configuration.findMany({
|
|
||||||
take: 10,
|
|
||||||
orderBy: { configRecId: "asc" },
|
|
||||||
include: {
|
|
||||||
configStatus: true,
|
|
||||||
configurationAudits: {
|
|
||||||
orderBy: { lastUpdatedUtc: "desc" },
|
|
||||||
include: {
|
|
||||||
configurationValues: {
|
|
||||||
orderBy: { configurationAuditValueRecId: "asc" },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (configurations.length === 0) {
|
|
||||||
console.error("No configurations found.");
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
writeFileSync(outputPath, JSON.stringify(configurations, null, 2));
|
|
||||||
} finally {
|
|
||||||
await prisma.$disconnect();
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
import fetchOpportunities from "./old-src/collectors/fetchOpportunities";
|
|
||||||
|
|
||||||
fetchOpportunities({
|
|
||||||
include: [
|
|
||||||
"company",
|
|
||||||
"activities",
|
|
||||||
"opportunityNotes",
|
|
||||||
"forecastItems",
|
|
||||||
"contacts",
|
|
||||||
],
|
|
||||||
}).then((opportunities) => {
|
|
||||||
const jsonData = JSON.stringify(opportunities, null, 2);
|
|
||||||
const { writeFileSync } = require("fs");
|
|
||||||
writeFileSync("examples/opportunity-with-relations.json", jsonData);
|
|
||||||
console.log(
|
|
||||||
`Exported ${opportunities.length} opportunities to examples/opportunity-with-relations.json`
|
|
||||||
);
|
|
||||||
process.exit(0);
|
|
||||||
});
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
import { PrismaClient } from "./generated/prisma/client";
|
|
||||||
import { PrismaMssql } from "@prisma/adapter-mssql";
|
|
||||||
import { writeFileSync } from "fs";
|
|
||||||
|
|
||||||
const connectionString = process.env.DATABASE_URL;
|
|
||||||
|
|
||||||
if (!connectionString) {
|
|
||||||
throw new Error("DATABASE_URL is not set.");
|
|
||||||
}
|
|
||||||
|
|
||||||
const adapter = new PrismaMssql(connectionString);
|
|
||||||
const prisma = new PrismaClient({ adapter });
|
|
||||||
|
|
||||||
const products = await prisma.productCatalog.findMany({
|
|
||||||
where: { inactiveFlag: false },
|
|
||||||
include: {
|
|
||||||
subcategory: { include: { category: true } },
|
|
||||||
manufacturer: true,
|
|
||||||
inventory: true,
|
|
||||||
itemVendors: true,
|
|
||||||
},
|
|
||||||
take: 100,
|
|
||||||
});
|
|
||||||
|
|
||||||
writeFileSync(
|
|
||||||
"products-with-relations.json",
|
|
||||||
JSON.stringify(products, null, 2),
|
|
||||||
);
|
|
||||||
console.log(`Exported ${products.length} products`);
|
|
||||||
await prisma.$disconnect();
|
|
||||||
@@ -1,150 +0,0 @@
|
|||||||
import { PrismaMssql } from "@prisma/adapter-mssql";
|
|
||||||
import { Prisma, PrismaClient } from "./generated/prisma/client";
|
|
||||||
|
|
||||||
const connectionString = process.env.DATABASE_URL;
|
|
||||||
if (!connectionString) {
|
|
||||||
throw new Error("DATABASE_URL is not set.");
|
|
||||||
}
|
|
||||||
|
|
||||||
const adapter = new PrismaMssql(connectionString);
|
|
||||||
const prisma = new PrismaClient({ adapter });
|
|
||||||
|
|
||||||
type CandidateTable = { table_name: string };
|
|
||||||
type CandidateColumn = { table_name: string; column_name: string };
|
|
||||||
|
|
||||||
type DmmfField = {
|
|
||||||
name: string;
|
|
||||||
dbName: string | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
type DmmfModel = {
|
|
||||||
name: string;
|
|
||||||
dbName: string | null;
|
|
||||||
fields: DmmfField[];
|
|
||||||
};
|
|
||||||
|
|
||||||
const TABLE_PATTERN = /config|configur/i;
|
|
||||||
const VALUE_COLUMN_PATTERN = /value|field|question|token/i;
|
|
||||||
const TOP_CONFIG_LIMIT = 150;
|
|
||||||
const CONFIG_KEY_COLUMNS = new Set([
|
|
||||||
"Config_RecID",
|
|
||||||
"Configuration_RecID",
|
|
||||||
"Configuration_RecId",
|
|
||||||
]);
|
|
||||||
|
|
||||||
function byName(a: string, b: string) {
|
|
||||||
return a.localeCompare(b);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const models = Prisma.dmmf.datamodel.models as unknown as DmmfModel[];
|
|
||||||
|
|
||||||
const configModels = models
|
|
||||||
.map((model) => ({
|
|
||||||
model,
|
|
||||||
tableName: model.dbName ?? model.name,
|
|
||||||
}))
|
|
||||||
.filter(({ tableName }) => TABLE_PATTERN.test(tableName));
|
|
||||||
|
|
||||||
const candidateTables: CandidateTable[] = configModels
|
|
||||||
.map(({ tableName }) => ({ table_name: tableName }))
|
|
||||||
.sort((a, b) => byName(a.table_name, b.table_name));
|
|
||||||
|
|
||||||
const candidateColumns: CandidateColumn[] = configModels
|
|
||||||
.flatMap(({ model, tableName }) =>
|
|
||||||
model.fields
|
|
||||||
.map((field) => field.dbName ?? field.name)
|
|
||||||
.filter((columnName) => VALUE_COLUMN_PATTERN.test(columnName))
|
|
||||||
.map((columnName) => ({
|
|
||||||
table_name: tableName,
|
|
||||||
column_name: columnName,
|
|
||||||
}))
|
|
||||||
)
|
|
||||||
.sort(
|
|
||||||
(a, b) =>
|
|
||||||
byName(a.table_name, b.table_name) ||
|
|
||||||
byName(a.column_name, b.column_name)
|
|
||||||
);
|
|
||||||
|
|
||||||
const valueTablesWithConfigKey: CandidateTable[] = configModels
|
|
||||||
.filter(({ model }) => {
|
|
||||||
const columnNames = model.fields.map(
|
|
||||||
(field) => field.dbName ?? field.name
|
|
||||||
);
|
|
||||||
const hasConfigKey = columnNames.some((column) =>
|
|
||||||
CONFIG_KEY_COLUMNS.has(column)
|
|
||||||
);
|
|
||||||
const hasValueLikeColumn = columnNames.some((column) =>
|
|
||||||
VALUE_COLUMN_PATTERN.test(column)
|
|
||||||
);
|
|
||||||
return hasConfigKey && hasValueLikeColumn;
|
|
||||||
})
|
|
||||||
.map(({ tableName }) => ({ table_name: tableName }))
|
|
||||||
.sort((a, b) => byName(a.table_name, b.table_name));
|
|
||||||
|
|
||||||
const [
|
|
||||||
configRows,
|
|
||||||
auditRows,
|
|
||||||
auditValueRows,
|
|
||||||
nonNullCustomFields,
|
|
||||||
groupedAuditTokens,
|
|
||||||
topConfigs,
|
|
||||||
] = await prisma.$transaction([
|
|
||||||
prisma.configuration.count(),
|
|
||||||
prisma.configurationAudit.count(),
|
|
||||||
prisma.configurationAuditValue.count(),
|
|
||||||
prisma.configuration.findMany({
|
|
||||||
where: { customField: { not: null } },
|
|
||||||
select: { customField: true },
|
|
||||||
}),
|
|
||||||
prisma.configurationAuditValue.groupBy({
|
|
||||||
by: ["auditToken"],
|
|
||||||
_count: true,
|
|
||||||
orderBy: [{ _count: { auditToken: "desc" } }, { auditToken: "asc" }],
|
|
||||||
take: 20,
|
|
||||||
}),
|
|
||||||
prisma.configuration.findMany({
|
|
||||||
take: TOP_CONFIG_LIMIT,
|
|
||||||
orderBy: { configRecId: "asc" },
|
|
||||||
include: {
|
|
||||||
configurationAudits: {
|
|
||||||
orderBy: { configurationAuditRecId: "asc" },
|
|
||||||
include: {
|
|
||||||
configurationValues: {
|
|
||||||
orderBy: { configurationAuditValueRecId: "asc" },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
|
|
||||||
const configCustomFieldNonempty = nonNullCustomFields.reduce((count, row) => {
|
|
||||||
return row.customField?.trim() ? count + 1 : count;
|
|
||||||
}, 0);
|
|
||||||
|
|
||||||
const rowStats = {
|
|
||||||
config_rows: configRows,
|
|
||||||
config_custom_field_nonempty: configCustomFieldNonempty,
|
|
||||||
audit_rows: auditRows,
|
|
||||||
audit_value_rows: auditValueRows,
|
|
||||||
};
|
|
||||||
|
|
||||||
const topAuditTokens = groupedAuditTokens.map(({ auditToken, _count }) => ({
|
|
||||||
audit_token: auditToken,
|
|
||||||
row_count: _count,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const output = {
|
|
||||||
candidateTables,
|
|
||||||
candidateColumns,
|
|
||||||
valueTablesWithConfigKey,
|
|
||||||
rowStats,
|
|
||||||
topAuditTokens,
|
|
||||||
topConfigs,
|
|
||||||
};
|
|
||||||
|
|
||||||
console.log(JSON.stringify(output, null, 2));
|
|
||||||
} finally {
|
|
||||||
await prisma.$disconnect();
|
|
||||||
}
|
|
||||||
@@ -1,334 +0,0 @@
|
|||||||
import { PrismaMssql } from "@prisma/adapter-mssql";
|
|
||||||
import { PrismaPg } from "@prisma/adapter-pg";
|
|
||||||
import { readFileSync } from "node:fs";
|
|
||||||
import { resolve } from "node:path";
|
|
||||||
|
|
||||||
import { PrismaClient as CwPrismaClient } from "./generated/prisma/client";
|
|
||||||
import { PrismaClient as ApiPrismaClient } from "../api/generated/prisma/client";
|
|
||||||
|
|
||||||
type EnvMap = Record<string, string>;
|
|
||||||
|
|
||||||
type Summary = {
|
|
||||||
cwTotal: number;
|
|
||||||
apiTotal: number;
|
|
||||||
eligibleForSync: number;
|
|
||||||
missingSrServiceRecId: number;
|
|
||||||
missingMemberRecId: number;
|
|
||||||
missingParentTicketInApi: number;
|
|
||||||
missingAuthorMappingInApi: number;
|
|
||||||
eligibleButMissingInApi: number;
|
|
||||||
sampleMissingParentTicketIds: number[];
|
|
||||||
sampleMissingAuthorMemberRecIds: number[];
|
|
||||||
sampleEligibleButMissingNoteIds: number[];
|
|
||||||
topMissingAuthorMemberRecIds: Array<{ memberRecId: number; count: number }>;
|
|
||||||
topMissingParentTicketIds: Array<{ srServiceRecId: number; count: number }>;
|
|
||||||
automateApi: {
|
|
||||||
total: number;
|
|
||||||
eligibleForSync: number;
|
|
||||||
missingSrServiceRecId: number;
|
|
||||||
missingMemberRecId: number;
|
|
||||||
missingParentTicketInApi: number;
|
|
||||||
missingAuthorMappingInApi: number;
|
|
||||||
eligibleButMissingInApi: number;
|
|
||||||
sampleMemberRecIds: number[];
|
|
||||||
sampleNoteIdsMissingInApi: number[];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const isAutomateApiAuthor = (
|
|
||||||
createdBy: string | null,
|
|
||||||
originalAuthor: string | null
|
|
||||||
): boolean => {
|
|
||||||
const normalizedCreatedBy = createdBy?.trim().toLowerCase() ?? "";
|
|
||||||
const normalizedOriginalAuthor = originalAuthor?.trim().toLowerCase() ?? "";
|
|
||||||
return (
|
|
||||||
normalizedCreatedBy.includes("automateapi") ||
|
|
||||||
normalizedOriginalAuthor.includes("automateapi")
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const parseEnvFile = (path: string): EnvMap => {
|
|
||||||
const envData = readFileSync(path, "utf8");
|
|
||||||
const out: EnvMap = {};
|
|
||||||
|
|
||||||
for (const rawLine of envData.split(/\r?\n/)) {
|
|
||||||
const line = rawLine.trim();
|
|
||||||
if (!line || line.startsWith("#")) continue;
|
|
||||||
|
|
||||||
const index = line.indexOf("=");
|
|
||||||
if (index <= 0) continue;
|
|
||||||
|
|
||||||
const key = line.slice(0, index).trim();
|
|
||||||
let value = line.slice(index + 1).trim();
|
|
||||||
|
|
||||||
if (
|
|
||||||
(value.startsWith('"') && value.endsWith('"')) ||
|
|
||||||
(value.startsWith("'") && value.endsWith("'"))
|
|
||||||
) {
|
|
||||||
value = value.slice(1, -1);
|
|
||||||
}
|
|
||||||
|
|
||||||
out[key] = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
return out;
|
|
||||||
};
|
|
||||||
|
|
||||||
const readApiEnv = (): EnvMap => {
|
|
||||||
const candidates = [
|
|
||||||
resolve(import.meta.dir, "../api/.env"),
|
|
||||||
resolve(process.cwd(), "../api/.env"),
|
|
||||||
resolve(process.cwd(), "api/.env"),
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const candidate of candidates) {
|
|
||||||
try {
|
|
||||||
return parseEnvFile(candidate);
|
|
||||||
} catch {
|
|
||||||
// Try next
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {};
|
|
||||||
};
|
|
||||||
|
|
||||||
const main = async (): Promise<void> => {
|
|
||||||
const apiEnv = readApiEnv();
|
|
||||||
|
|
||||||
const cwDatabaseUrl =
|
|
||||||
process.env.CW_DATABASE_URL ||
|
|
||||||
process.env.DATABASE_URL ||
|
|
||||||
apiEnv.CW_DATABASE_URL;
|
|
||||||
const apiDatabaseUrl =
|
|
||||||
process.env.API_DATABASE_URL ||
|
|
||||||
process.env.OPTIMA_API_DATABASE_URL ||
|
|
||||||
apiEnv.API_DATABASE_URL ||
|
|
||||||
apiEnv.OPTIMA_API_DATABASE_URL ||
|
|
||||||
apiEnv.DATABASE_URL;
|
|
||||||
|
|
||||||
if (!cwDatabaseUrl) {
|
|
||||||
throw new Error("Missing CW DB URL. Set CW_DATABASE_URL or DATABASE_URL.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!apiDatabaseUrl) {
|
|
||||||
throw new Error(
|
|
||||||
"Missing API DB URL. Set API_DATABASE_URL/OPTIMA_API_DATABASE_URL or provide api/.env DATABASE_URL."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const cwPrisma = new CwPrismaClient({
|
|
||||||
adapter: new PrismaMssql(cwDatabaseUrl),
|
|
||||||
});
|
|
||||||
const apiPrisma = new ApiPrismaClient({
|
|
||||||
adapter: new PrismaPg({ connectionString: apiDatabaseUrl }),
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
console.log("[diag] Loading API reference sets...");
|
|
||||||
const [apiNotes, apiTickets, apiUsers] = await Promise.all([
|
|
||||||
apiPrisma.serviceTicketNote.findMany({ select: { id: true } }),
|
|
||||||
apiPrisma.serviceTicket.findMany({ select: { id: true } }),
|
|
||||||
apiPrisma.user.findMany({ select: { cwMemberId: true } }),
|
|
||||||
]);
|
|
||||||
|
|
||||||
const apiNoteIds = new Set<number>(apiNotes.map((r) => r.id));
|
|
||||||
const apiTicketIds = new Set<number>(apiTickets.map((r) => r.id));
|
|
||||||
const apiUserMemberIds = new Set<number>(
|
|
||||||
apiUsers
|
|
||||||
.map((r) => r.cwMemberId)
|
|
||||||
.filter((v): v is number => Number.isInteger(v))
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
`[diag] API sets: notes=${apiNoteIds.size} tickets=${apiTicketIds.size} usersWithCwMemberId=${apiUserMemberIds.size}`
|
|
||||||
);
|
|
||||||
|
|
||||||
const cwTotal = await cwPrisma.ticketNote.count();
|
|
||||||
const apiTotal = apiNoteIds.size;
|
|
||||||
|
|
||||||
let missingSrServiceRecId = 0;
|
|
||||||
let missingMemberRecId = 0;
|
|
||||||
let missingParentTicketInApi = 0;
|
|
||||||
let missingAuthorMappingInApi = 0;
|
|
||||||
let eligibleForSync = 0;
|
|
||||||
let eligibleButMissingInApi = 0;
|
|
||||||
|
|
||||||
let automateTotal = 0;
|
|
||||||
let automateEligibleForSync = 0;
|
|
||||||
let automateMissingSrServiceRecId = 0;
|
|
||||||
let automateMissingMemberRecId = 0;
|
|
||||||
let automateMissingParentTicketInApi = 0;
|
|
||||||
let automateMissingAuthorMappingInApi = 0;
|
|
||||||
let automateEligibleButMissingInApi = 0;
|
|
||||||
|
|
||||||
const sampleMissingParentTicketIds: number[] = [];
|
|
||||||
const sampleMissingAuthorMemberRecIds: number[] = [];
|
|
||||||
const sampleEligibleButMissingNoteIds: number[] = [];
|
|
||||||
const automateSampleMemberRecIds: number[] = [];
|
|
||||||
const automateSampleNoteIdsMissingInApi: number[] = [];
|
|
||||||
const missingAuthorCounts = new Map<number, number>();
|
|
||||||
const missingParentCounts = new Map<number, number>();
|
|
||||||
|
|
||||||
let cursor = 0;
|
|
||||||
const batchSize = 5000;
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
`[diag] Scanning CW TicketNote rows in batches of ${batchSize}...`
|
|
||||||
);
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
const batch = await cwPrisma.ticketNote.findMany({
|
|
||||||
where: {
|
|
||||||
ticketNoteRecId: {
|
|
||||||
gt: cursor,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
orderBy: {
|
|
||||||
ticketNoteRecId: "asc",
|
|
||||||
},
|
|
||||||
select: {
|
|
||||||
ticketNoteRecId: true,
|
|
||||||
srServiceRecId: true,
|
|
||||||
memberRecId: true,
|
|
||||||
createdBy: true,
|
|
||||||
originalAuthor: true,
|
|
||||||
},
|
|
||||||
take: batchSize,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (batch.length === 0) break;
|
|
||||||
|
|
||||||
for (const row of batch) {
|
|
||||||
const noteId = row.ticketNoteRecId;
|
|
||||||
const srServiceRecId = row.srServiceRecId;
|
|
||||||
const memberRecId = row.memberRecId;
|
|
||||||
const isAutomate = isAutomateApiAuthor(
|
|
||||||
row.createdBy,
|
|
||||||
row.originalAuthor
|
|
||||||
);
|
|
||||||
|
|
||||||
if (isAutomate) {
|
|
||||||
automateTotal++;
|
|
||||||
if (
|
|
||||||
memberRecId &&
|
|
||||||
automateSampleMemberRecIds.length < 20 &&
|
|
||||||
!automateSampleMemberRecIds.includes(memberRecId)
|
|
||||||
) {
|
|
||||||
automateSampleMemberRecIds.push(memberRecId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let blocked = false;
|
|
||||||
|
|
||||||
if (!srServiceRecId) {
|
|
||||||
missingSrServiceRecId++;
|
|
||||||
if (isAutomate) {
|
|
||||||
automateMissingSrServiceRecId++;
|
|
||||||
}
|
|
||||||
blocked = true;
|
|
||||||
} else if (!apiTicketIds.has(srServiceRecId)) {
|
|
||||||
missingParentTicketInApi++;
|
|
||||||
if (isAutomate) {
|
|
||||||
automateMissingParentTicketInApi++;
|
|
||||||
}
|
|
||||||
blocked = true;
|
|
||||||
missingParentCounts.set(
|
|
||||||
srServiceRecId,
|
|
||||||
(missingParentCounts.get(srServiceRecId) ?? 0) + 1
|
|
||||||
);
|
|
||||||
if (sampleMissingParentTicketIds.length < 20) {
|
|
||||||
sampleMissingParentTicketIds.push(srServiceRecId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!memberRecId) {
|
|
||||||
missingMemberRecId++;
|
|
||||||
if (isAutomate) {
|
|
||||||
automateMissingMemberRecId++;
|
|
||||||
}
|
|
||||||
blocked = true;
|
|
||||||
} else if (!apiUserMemberIds.has(memberRecId)) {
|
|
||||||
missingAuthorMappingInApi++;
|
|
||||||
if (isAutomate) {
|
|
||||||
automateMissingAuthorMappingInApi++;
|
|
||||||
}
|
|
||||||
blocked = true;
|
|
||||||
missingAuthorCounts.set(
|
|
||||||
memberRecId,
|
|
||||||
(missingAuthorCounts.get(memberRecId) ?? 0) + 1
|
|
||||||
);
|
|
||||||
if (sampleMissingAuthorMemberRecIds.length < 20) {
|
|
||||||
sampleMissingAuthorMemberRecIds.push(memberRecId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!blocked) {
|
|
||||||
eligibleForSync++;
|
|
||||||
if (isAutomate) {
|
|
||||||
automateEligibleForSync++;
|
|
||||||
}
|
|
||||||
if (!apiNoteIds.has(noteId)) {
|
|
||||||
eligibleButMissingInApi++;
|
|
||||||
if (isAutomate) {
|
|
||||||
automateEligibleButMissingInApi++;
|
|
||||||
if (automateSampleNoteIdsMissingInApi.length < 20) {
|
|
||||||
automateSampleNoteIdsMissingInApi.push(noteId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (sampleEligibleButMissingNoteIds.length < 50) {
|
|
||||||
sampleEligibleButMissingNoteIds.push(noteId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cursor = batch[batch.length - 1]!.ticketNoteRecId;
|
|
||||||
if (cursor % 50000 < batchSize) {
|
|
||||||
console.log(`[diag] Progress cursor=${cursor}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const summary: Summary = {
|
|
||||||
cwTotal,
|
|
||||||
apiTotal,
|
|
||||||
eligibleForSync,
|
|
||||||
missingSrServiceRecId,
|
|
||||||
missingMemberRecId,
|
|
||||||
missingParentTicketInApi,
|
|
||||||
missingAuthorMappingInApi,
|
|
||||||
eligibleButMissingInApi,
|
|
||||||
sampleMissingParentTicketIds,
|
|
||||||
sampleMissingAuthorMemberRecIds,
|
|
||||||
sampleEligibleButMissingNoteIds,
|
|
||||||
topMissingAuthorMemberRecIds: [...missingAuthorCounts.entries()]
|
|
||||||
.sort((a, b) => b[1] - a[1])
|
|
||||||
.slice(0, 20)
|
|
||||||
.map(([memberRecId, count]) => ({ memberRecId, count })),
|
|
||||||
topMissingParentTicketIds: [...missingParentCounts.entries()]
|
|
||||||
.sort((a, b) => b[1] - a[1])
|
|
||||||
.slice(0, 20)
|
|
||||||
.map(([srServiceRecId, count]) => ({ srServiceRecId, count })),
|
|
||||||
automateApi: {
|
|
||||||
total: automateTotal,
|
|
||||||
eligibleForSync: automateEligibleForSync,
|
|
||||||
missingSrServiceRecId: automateMissingSrServiceRecId,
|
|
||||||
missingMemberRecId: automateMissingMemberRecId,
|
|
||||||
missingParentTicketInApi: automateMissingParentTicketInApi,
|
|
||||||
missingAuthorMappingInApi: automateMissingAuthorMappingInApi,
|
|
||||||
eligibleButMissingInApi: automateEligibleButMissingInApi,
|
|
||||||
sampleMemberRecIds: automateSampleMemberRecIds,
|
|
||||||
sampleNoteIdsMissingInApi: automateSampleNoteIdsMissingInApi,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
console.log("[diag] TicketNote sync gap summary:");
|
|
||||||
console.log(JSON.stringify(summary, null, 2));
|
|
||||||
} finally {
|
|
||||||
await Promise.all([cwPrisma.$disconnect(), apiPrisma.$disconnect()]);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
main().catch((error) => {
|
|
||||||
console.error("[diag] Failed:", error);
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
Reference in New Issue
Block a user