Compare commits
36 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2f17f24b3b | |||
| 1230dacfa2 | |||
| 1dc3c7ce07 | |||
| e764932c39 | |||
| 33b34d08a7 | |||
| 5afda8cb34 | |||
| ee3e0a7377 | |||
| e294791858 | |||
| 97ac4a2173 | |||
| ad7507d133 | |||
| 15ef24eb3e | |||
| f53b390e18 | |||
| c0a4d4f919 | |||
| 0ce1eda606 | |||
| 6c310ed753 | |||
| 1907bb433b | |||
| 4efca6cc53 | |||
| d5c22c8eff | |||
| a048e1e824 | |||
| 6d935e7180 | |||
| fe71248e88 | |||
| 7411310083 | |||
| 30b408e0db | |||
| d7b374f8ab | |||
| 883b648d5e | |||
| b787120461 | |||
| 1326725995 | |||
| 508fa39835 | |||
| b1f6462ac3 | |||
| 51eb36f4a6 | |||
| 827b018f25 | |||
| 5852bd7819 | |||
| 4c21245044 | |||
| ce456257ea | |||
| 8949819396 | |||
| 49faf97c9b |
@@ -24,9 +24,10 @@ Keep each layer focused:
|
|||||||
|
|
||||||
## Runtime / tooling
|
## Runtime / tooling
|
||||||
|
|
||||||
The project runs on **Bun**. DB tooling uses **Prisma**; the generated client lives under `generated/prisma` (do NOT edit generated files). Key scripts in `package.json`:
|
The project runs on **Bun** exclusively — **always use `bun` commands, never `npm`, `npx`, or `yarn`**. DB tooling uses **Prisma**; the generated client lives under `generated/prisma` (do NOT edit generated files). Test preloads are configured in `bunfig.toml` so bare `bun test` works. Key scripts in `package.json`:
|
||||||
|
|
||||||
- `dev` — `NODE_ENV=development bun --watch src/index.ts` (start dev server with hot reload)
|
- `dev` — `NODE_ENV=development bun --watch src/index.ts` (start dev server with hot reload)
|
||||||
|
- `test` — `bun test` (runs all tests with preload from `bunfig.toml`)
|
||||||
- `db:gen` — `prisma generate`
|
- `db:gen` — `prisma generate`
|
||||||
- `db:push` — `prisma migrate dev --skip-generate`
|
- `db:push` — `prisma migrate dev --skip-generate`
|
||||||
- `utils:dev` — `docker compose -f .docker/docker-compose.yml up --build`
|
- `utils:dev` — `docker compose -f .docker/docker-compose.yml up --build`
|
||||||
@@ -36,7 +37,7 @@ The project runs on **Bun**. DB tooling uses **Prisma**; the generated client li
|
|||||||
|
|
||||||
## Data layer
|
## Data layer
|
||||||
|
|
||||||
Prisma schema is at `prisma/schema.prisma`. The app imports the generated Prisma client from `generated/prisma/client.ts` (or `generated/prisma/browser.ts` for browser type contexts). The shared `prisma` instance is exported from `src/constants.ts`. Always run `npm run db:gen` after updating `schema.prisma`.
|
Prisma schema is at `prisma/schema.prisma`. The app imports the generated Prisma client from `generated/prisma/client.ts` (or `generated/prisma/browser.ts` for browser type contexts). The shared `prisma` instance is exported from `src/constants.ts`. Always run `bun run db:gen` after updating `schema.prisma`.
|
||||||
|
|
||||||
## Shared constants (`src/constants.ts`)
|
## Shared constants (`src/constants.ts`)
|
||||||
|
|
||||||
@@ -174,13 +175,14 @@ The `UnifiClient` class in `src/modules/unifi-api/UnifiClient.ts` wraps all UniF
|
|||||||
|
|
||||||
## Local dev / quick checks
|
## Local dev / quick checks
|
||||||
|
|
||||||
- Start dev server: `npm run dev`
|
- Start dev server: `bun run dev`
|
||||||
- Regenerate Prisma client: `npm run db:gen`
|
- Run tests: `bun test`
|
||||||
- Apply DB migrations locally: `npm run db:push`
|
- Regenerate Prisma client: `bun run db:gen`
|
||||||
- Docker dev utilities: `npm run utils:dev`
|
- Apply DB migrations locally: `bun run db:push`
|
||||||
- Generate private keys: `npm run utils:gen_private_keys`
|
- Docker dev utilities: `bun run utils:dev`
|
||||||
- Create admin role: `npm run utils:create_admin_role`
|
- Generate private keys: `bun run utils:gen_private_keys`
|
||||||
- Assign user role: `npm run utils:assign_user_role`
|
- Create admin role: `bun run utils:create_admin_role`
|
||||||
|
- Assign user role: `bun run utils:assign_user_role`
|
||||||
|
|
||||||
## When editing generated or infra files
|
## When editing generated or infra files
|
||||||
|
|
||||||
@@ -196,7 +198,11 @@ Whenever you add, remove, or modify API routes or permission nodes, you **must**
|
|||||||
2. `PERMISSIONS.md` — human-readable documentation of all permission nodes; must strictly reflect the data in `PermissionNodes.ts`.
|
2. `PERMISSIONS.md` — human-readable documentation of all permission nodes; must strictly reflect the data in `PermissionNodes.ts`.
|
||||||
3. `API_ROUTES.md` — comprehensive documentation of all API routes, including method, path, auth requirements, permissions, request/response examples.
|
3. `API_ROUTES.md` — comprehensive documentation of all API routes, including method, path, auth requirements, permissions, request/response examples.
|
||||||
|
|
||||||
Always verify that new routes have their required permissions listed in `PermissionNodes.ts`, that `PERMISSIONS.md` tables match the TS file exactly, and that `API_ROUTES.md` includes full documentation for every mounted route. Run through all three files at the end of any route or permission change to catch discrepancies.
|
Additionally, whenever you add, remove, or modify **caching logic** (TTL algorithms, cache key patterns, background refresh mechanics, retry settings, or invalidation behavior), you **must** update:
|
||||||
|
|
||||||
|
4. `CACHING.md` — comprehensive documentation of the Redis-backed opportunity cache, TTL algorithms, background refresh mechanics, retry logic, and debugging tools.
|
||||||
|
|
||||||
|
Always verify that new routes have their required permissions listed in `PermissionNodes.ts`, that `PERMISSIONS.md` tables match the TS file exactly, that `API_ROUTES.md` includes full documentation for every mounted route, and that `CACHING.md` accurately reflects any caching changes. Run through all relevant files at the end of any route, permission, or caching change to catch discrepancies.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -5,8 +5,30 @@ on:
|
|||||||
types: [created]
|
types: [created]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
test:
|
||||||
|
name: Test
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout source code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Bun
|
||||||
|
uses: oven-sh/setup-bun@v2
|
||||||
|
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:
|
build:
|
||||||
name: Build
|
name: Build
|
||||||
|
needs: [test]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
name: Tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: ["**"]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
name: Test
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout source code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Bun
|
||||||
|
uses: oven-sh/setup-bun@v2
|
||||||
|
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
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
# Logs
|
# Logs
|
||||||
logs
|
logs
|
||||||
*.log
|
*.log
|
||||||
|
*.jsonl
|
||||||
|
cw-api-logs/
|
||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
yarn-debug.log*
|
yarn-debug.log*
|
||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
|
|||||||
Vendored
+5
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"chat.tools.terminal.autoApprove": {
|
||||||
|
"bun": true
|
||||||
|
}
|
||||||
|
}
|
||||||
+3170
File diff suppressed because it is too large
Load Diff
+384
@@ -0,0 +1,384 @@
|
|||||||
|
# Caching Architecture
|
||||||
|
|
||||||
|
This document describes the caching layer used in the Optima API, covering the Redis-backed opportunity cache, TTL algorithms, background refresh mechanics, retry logic, and debugging tools.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The API caches expensive ConnectWise (CW) API responses in **Redis** to reduce latency and avoid CW rate limits. The primary cache layer is the **opportunity cache** (`src/modules/cache/opportunityCache.ts`), which proactively warms data for all non-closed opportunities on a background interval.
|
||||||
|
|
||||||
|
The API also maintains a Redis-backed **sales member metrics cache** (`src/modules/cache/salesOpportunityMetricsCache.ts`) refreshed every 5 minutes. It precomputes per-member dashboard/reporting figures (pipeline revenue, won/lost counts, win rate, avg days to close, and related metrics) for fast reads from `/v1/sales/opportunities/metrics`.
|
||||||
|
|
||||||
|
### Key design principles
|
||||||
|
|
||||||
|
- **Adaptive TTLs** — cache durations are computed dynamically based on how "hot" an opportunity is (recently updated = shorter TTL = fresher data).
|
||||||
|
- **Background refresh** — a 20-minute interval scans all open opportunities and re-fetches only expired cache keys.
|
||||||
|
- **Bounded concurrency** — CW API calls are throttled via thunk-based batching to prevent overwhelming the upstream API.
|
||||||
|
- **Graceful degradation** — transient CW errors (timeouts, network failures) are caught, logged, and retried on the next cycle rather than crashing the process.
|
||||||
|
- **Priority ordering** — most recently updated opportunities are refreshed first so active deals get fresh data before stale ones.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What is cached
|
||||||
|
|
||||||
|
Each non-closed opportunity can have up to 7 cached payloads in Redis:
|
||||||
|
|
||||||
|
| Cache Key Pattern | Data | Source |
|
||||||
|
| ----------------------------------- | ------------------------------------ | --------------------------------------------------------------------- |
|
||||||
|
| `opp:cw-data:{cwOpportunityId}` | Raw CW opportunity response | `GET /sales/opportunities/:id` |
|
||||||
|
| `opp:activities:{cwOpportunityId}` | CW activities array | `GET /sales/activities?conditions=opportunity/id=:id` |
|
||||||
|
| `opp:notes:{cwOpportunityId}` | CW notes array | `GET /sales/opportunities/:id/notes` |
|
||||||
|
| `opp:contacts:{cwOpportunityId}` | CW contacts array | `GET /sales/opportunities/:id/contacts` |
|
||||||
|
| `opp:products:{cwOpportunityId}` | Forecast + procurement products blob | `GET /sales/opportunities/:id/forecast` + `GET /procurement/products` |
|
||||||
|
| `opp:company-cw:{cw_CompanyId}` | Hydrated company + contacts blob | `GET /company/companies/:id` + contacts endpoints |
|
||||||
|
| `opp:site:{cwCompanyId}:{cwSiteId}` | Company site data | `GET /company/companies/:id/sites/:siteId` |
|
||||||
|
|
||||||
|
Inventory-adjustment-driven catalog sync adds a targeted product cache:
|
||||||
|
|
||||||
|
| Cache Key Pattern | Data | Source |
|
||||||
|
| ------------------------ | ---------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
|
||||||
|
| `catalog:item:cw:{cwId}` | Full CW catalog item + computed `onHand` + DB row snapshot | `GET /procurement/adjustments` + `GET /procurement/catalog/:id` + catalog inventory endpoint |
|
||||||
|
|
||||||
|
Sales opportunity metrics caching adds member-focused keys:
|
||||||
|
|
||||||
|
| Cache Key Pattern | Data | Source |
|
||||||
|
| ------------------------------------- | -------------------------------------- | ------------------------------------------------------------------------------------- |
|
||||||
|
| `sales:metrics:members:all` | Envelope of all active-member metrics | Precomputed from active CW members + assigned opportunities + products cache/CW fetch |
|
||||||
|
| `sales:metrics:member:{cwIdentifier}` | One member's computed metrics snapshot | Same as above |
|
||||||
|
| `sales:metrics:oppRevenue:{cwOppId}` | Per-opportunity computed revenue blob | Metrics refresh lookups (products cache-first, then manager/controller fallback) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## TTL Algorithms
|
||||||
|
|
||||||
|
Three algorithms compute cache TTLs. All share the same input signals:
|
||||||
|
|
||||||
|
- `closedFlag` — whether the opportunity is closed
|
||||||
|
- `closedDate` — when it was closed
|
||||||
|
- `expectedCloseDate` — projected close date (forward-looking signal)
|
||||||
|
- `lastUpdated` — last CW modification date (backward-looking signal)
|
||||||
|
|
||||||
|
### Primary TTL (`computeCacheTTL`)
|
||||||
|
|
||||||
|
**File:** `src/modules/algorithms/computeCacheTTL.ts`
|
||||||
|
|
||||||
|
Used for: opportunity CW data, activities, company CW data.
|
||||||
|
|
||||||
|
| # | Condition | TTL | Human |
|
||||||
|
| --- | ------------------------------------------------------- | ---------- | ------------ |
|
||||||
|
| 1a | Closed > 30 days ago | `null` | Do not cache |
|
||||||
|
| 1b | Closed within 30 days | 900,000 ms | 15 minutes |
|
||||||
|
| 2 | `expectedCloseDate` or `lastUpdated` within **5 days** | 30,000 ms | 30 seconds |
|
||||||
|
| 3 | `expectedCloseDate` or `lastUpdated` within **14 days** | 60,000 ms | 60 seconds |
|
||||||
|
| 4 | Everything else | 900,000 ms | 15 minutes |
|
||||||
|
|
||||||
|
Rules are evaluated top-to-bottom; first match wins.
|
||||||
|
|
||||||
|
### Sub-Resource TTL (`computeSubResourceCacheTTL`)
|
||||||
|
|
||||||
|
**File:** `src/modules/algorithms/computeSubResourceCacheTTL.ts`
|
||||||
|
|
||||||
|
Used for: notes, contacts.
|
||||||
|
|
||||||
|
| # | Condition | TTL | Human |
|
||||||
|
| --- | --------------------- | ---------- | ------------ |
|
||||||
|
| 1a | Closed > 30 days ago | `null` | Do not cache |
|
||||||
|
| 1b | Closed within 30 days | 300,000 ms | 5 minutes |
|
||||||
|
| 2 | Within **5 days** | 60,000 ms | 60 seconds |
|
||||||
|
| 3 | Within **14 days** | 120,000 ms | 2 minutes |
|
||||||
|
| 4 | Everything else | 300,000 ms | 5 minutes |
|
||||||
|
|
||||||
|
### Products TTL (`computeProductsCacheTTL`)
|
||||||
|
|
||||||
|
**File:** `src/modules/algorithms/computeProductsCacheTTL.ts`
|
||||||
|
|
||||||
|
Used for: forecast + procurement products.
|
||||||
|
|
||||||
|
| # | Condition | TTL | Human |
|
||||||
|
| --- | ------------------------------------------- | ------------ | ---------- |
|
||||||
|
| 1 | Status is Won/Lost/Pending Won/Pending Lost | `null` | No cache |
|
||||||
|
| 2 | Main cache TTL is `null` | `null` | No cache |
|
||||||
|
| 3 | `lastUpdated` within **3 days** | 15,000 ms | 15 seconds |
|
||||||
|
| 4 | Everything else | 1,200,000 ms | 20 minutes |
|
||||||
|
|
||||||
|
Products on terminal-status opportunities are never proactively cached. Non-hot products use a **lazy on-demand** cache — they're fetched when requested and cached for 20 minutes.
|
||||||
|
|
||||||
|
### Site TTL
|
||||||
|
|
||||||
|
Sites use a fixed TTL of **20 minutes** (1,200,000 ms). Site/address data rarely changes. Sites are **not** proactively warmed by the background refresh — they are populated lazily on the first detail-view request.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Background Refresh
|
||||||
|
|
||||||
|
**Function:** `refreshOpportunityCache()` in `src/modules/cache/opportunityCache.ts`
|
||||||
|
|
||||||
|
**Interval:** Every 20 minutes, triggered from `src/index.ts`.
|
||||||
|
|
||||||
|
### Refresh cycle
|
||||||
|
|
||||||
|
1. **Query DB** — fetch all non-closed opportunities + recently closed (within 30 days), ordered by `cwLastUpdated DESC` (most recently active first).
|
||||||
|
2. **Batch EXISTS check** — use a single Redis pipeline to check which cache keys already exist (5 EXISTS commands per opportunity: oppCwData, activities, notes, contacts, products).
|
||||||
|
3. **Build thunk list** — for each opportunity with missing keys, push a **thunk** (lazy function) into the task list. No HTTP requests fire at this point.
|
||||||
|
4. **Execute with bounded concurrency** — process thunks in batches of `CONCURRENCY` (currently **6**), with a `BATCH_DELAY_MS` (currently **250ms**) pause between batches. Each thunk is only invoked inside the batch loop.
|
||||||
|
5. **Emit events** — `cache:opportunities:refresh:started` and `cache:opportunities:refresh:completed` events are emitted for the event debugger.
|
||||||
|
|
||||||
|
### Inventory-adjustment listener cycle
|
||||||
|
|
||||||
|
**Function:** `listenInventoryAdjustments()` in `src/modules/cw-utils/procurement/listenInventoryAdjustments.ts`
|
||||||
|
|
||||||
|
**Interval:** Every 60 seconds, triggered from `src/index.ts`.
|
||||||
|
|
||||||
|
1. Fetch `GET /procurement/adjustments?pageSize=1000`.
|
||||||
|
2. Build a normalized snapshot of tracked inventory rows (`cwCatalogId`, `onHand`, `inventory`) per adjustment.
|
||||||
|
3. Compare to previous snapshot; extract only changed product IDs.
|
||||||
|
4. For each changed product ID, fetch fresh CW catalog item + current on-hand.
|
||||||
|
5. Upsert `CatalogItem` in Postgres and write Redis key `catalog:item:cw:{cwId}` with a 20-minute TTL.
|
||||||
|
|
||||||
|
Guardrails to prevent request storms:
|
||||||
|
|
||||||
|
- Diffing is computed at **product state** level (grouped by `cwCatalogId`), not raw adjustment-row churn.
|
||||||
|
- Per-cycle syncs are capped (`CW_ADJUSTMENT_SYNC_MAX_PER_CYCLE`, default `50`).
|
||||||
|
- Product resync cooldown is enforced (`CW_ADJUSTMENT_SYNC_COOLDOWN_MS`, default `600000` ms / 10 min).
|
||||||
|
|
||||||
|
This avoids full-catalog sweeps for small inventory movements and updates only the products implicated by adjustments.
|
||||||
|
|
||||||
|
### Full procurement catalog refresh
|
||||||
|
|
||||||
|
**Function:** `refreshCatalog()` in `src/modules/cw-utils/procurement/refreshCatalog.ts`
|
||||||
|
|
||||||
|
**Interval:** Every 30 minutes, triggered from `src/index.ts`.
|
||||||
|
|
||||||
|
The full catalog cache/DB sync uses the same slow-parallel thunk strategy as opportunity cache refreshes:
|
||||||
|
|
||||||
|
- Build arrays of thunk tasks (`() => Promise<void>`) for CW item fetches, inventory fetches, and DB upserts.
|
||||||
|
- Execute with bounded concurrency (`CONCURRENCY=6`).
|
||||||
|
- Pause between batches (`BATCH_DELAY_MS=250`) to avoid CW burst pressure.
|
||||||
|
- Log task failures and retry naturally on the next cycle.
|
||||||
|
|
||||||
|
This keeps full-catalog refresh conservative while inventory-adjustment listener handles near-real-time targeted updates.
|
||||||
|
|
||||||
|
### Full inventory sweep fallback
|
||||||
|
|
||||||
|
`refreshInventory()` remains as a safety net but is intentionally infrequent:
|
||||||
|
|
||||||
|
- Runs every **6 hours** from `src/index.ts` (no startup-time full sweep).
|
||||||
|
- Uses the same slow-parallel pattern (`CONCURRENCY=6`, `BATCH_DELAY_MS=250`) to avoid burst traffic.
|
||||||
|
|
||||||
|
Most on-hand freshness now comes from the 60-second adjustment listener plus 30-minute full catalog refresh.
|
||||||
|
|
||||||
|
### Concurrency control
|
||||||
|
|
||||||
|
The thunk pattern is critical. Previously, tasks were pushed as already-executing promises (`refreshTasks.push(fetchAndCache(...))`), which meant all HTTP requests fired simultaneously regardless of the batching loop. The fix was changing the array type from `Promise<void>[]` to `(() => Promise<void>)[]` so requests only start when explicitly invoked: `batch.map((fn) => fn())`.
|
||||||
|
|
||||||
|
### Current tuning
|
||||||
|
|
||||||
|
| Parameter | Value | Effect |
|
||||||
|
| ---------------- | ---------- | ------------------------------------------ |
|
||||||
|
| `CONCURRENCY` | 6 | Max simultaneous CW API requests per batch |
|
||||||
|
| `BATCH_DELAY_MS` | 250 | Milliseconds between batches |
|
||||||
|
| Refresh interval | 20 minutes | How often the full sweep runs |
|
||||||
|
|
||||||
|
At these settings, a full sweep of ~500 expired keys completes in ~1-2 minutes with zero CW errors and ~230ms median latency.
|
||||||
|
|
||||||
|
### Sales metrics refresh job
|
||||||
|
|
||||||
|
**Function:** `refreshSalesOpportunityMetricsCache()` in `src/modules/cache/salesOpportunityMetricsCache.ts`
|
||||||
|
|
||||||
|
**Interval:** Every 5 minutes, triggered from `src/index.ts`.
|
||||||
|
|
||||||
|
**Startup behavior:** On app startup, the refresh is invoked once with `forceColdLoad=true`, which clears metrics-owned Redis keys and bypasses metrics/product cache reuse for that initial rebuild. Subsequent interval runs use the normal warm path.
|
||||||
|
|
||||||
|
Refresh flow:
|
||||||
|
|
||||||
|
1. Fetch all active CW members (`inactiveFlag=false`).
|
||||||
|
Source: local `CwMember` table (kept in sync by the existing members refresh job).
|
||||||
|
2. Query DB opportunities assigned to those members (primary or secondary rep), scoped to open opportunities plus YTD-closed opportunities.
|
||||||
|
3. For each opportunity, compute revenue cache-first from `sales:metrics:oppRevenue:{cwOppId}` then `opp:products:{cwOpportunityId}`, and fallback through the manager/controller path (`opportunities.fetchRecord(...).fetchProducts()`) on miss.
|
||||||
|
4. Aggregate member metrics (pipeline revenue, won/lost MTD+YTD counts, avg days to close, weighted pipeline, win/loss rates, and related KPIs).
|
||||||
|
5. Write per-opportunity revenue blobs plus all-member and per-member snapshots to Redis with a 10-minute TTL.
|
||||||
|
|
||||||
|
Safety controls:
|
||||||
|
|
||||||
|
- **Single-flight lock** prevents overlapping refresh runs if a prior run is still in progress.
|
||||||
|
- **Per-opportunity timeout guard** ensures slow CW product lookups degrade to zero-revenue fallback instead of stalling the full refresh.
|
||||||
|
- **Force-cold-load mode** clears `sales:metrics:*` runtime state owned by the metrics cache before rebuilding startup data.
|
||||||
|
|
||||||
|
This cache-first model prioritizes metrics-owned opportunity revenue keys first, then opportunity product cache entries, and only reaches CW when needed.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Retry Logic (`withCwRetry`)
|
||||||
|
|
||||||
|
**File:** `src/modules/cw-utils/withCwRetry.ts`
|
||||||
|
|
||||||
|
Wraps CW API calls with exponential backoff retry on transient errors.
|
||||||
|
|
||||||
|
### Retryable errors
|
||||||
|
|
||||||
|
- `ECONNABORTED` (timeout)
|
||||||
|
- `ECONNRESET`
|
||||||
|
- `ETIMEDOUT`
|
||||||
|
- `ECONNREFUSED`
|
||||||
|
- `ERR_NETWORK`
|
||||||
|
- `ENETUNREACH`
|
||||||
|
- HTTP 5xx server errors
|
||||||
|
|
||||||
|
### Default configuration
|
||||||
|
|
||||||
|
| Parameter | Default | Description |
|
||||||
|
| ------------- | ------- | ----------------------------------------------------------- |
|
||||||
|
| `maxAttempts` | 3 | Total attempts including the first |
|
||||||
|
| `baseDelayMs` | 1,000 | Delay before first retry (doubles each retry: 1s → 2s → 4s) |
|
||||||
|
| `label` | — | Optional tag for log messages |
|
||||||
|
|
||||||
|
### Usage
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { withCwRetry } from "./withCwRetry";
|
||||||
|
|
||||||
|
const response = await withCwRetry(
|
||||||
|
() => connectWiseApi.get(`/company/companies/${id}`),
|
||||||
|
{ label: `fetchCompany#${id}`, maxAttempts: 3, baseDelayMs: 1_500 },
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
Non-transient errors (404, 400, etc.) are re-thrown immediately without retry.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## CW API Logger
|
||||||
|
|
||||||
|
**File:** `src/modules/cw-utils/cwApiLogger.ts`
|
||||||
|
|
||||||
|
Axios interceptor that logs every CW API call to a JSONL file. Logging is **opt-in** — set the `LOG_CW_API` environment variable to enable it. Each process start creates a new timestamped file in the `cw-api-logs/` directory (e.g., `cw-api-logs/2026-03-02T14-30-05.123Z.jsonl`).
|
||||||
|
|
||||||
|
### Enabling logging
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Via the dev:log shorthand script
|
||||||
|
bun run dev:log
|
||||||
|
|
||||||
|
# Or manually with any command
|
||||||
|
LOG_CW_API=1 bun run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### Log entry fields
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
| ------------ | ----------------- | ----------------------------------- |
|
||||||
|
| `timestamp` | string (ISO-8601) | When the request completed |
|
||||||
|
| `method` | string | HTTP method |
|
||||||
|
| `url` | string | Request URL (relative or absolute) |
|
||||||
|
| `baseURL` | string | Axios baseURL |
|
||||||
|
| `status` | number \| null | HTTP status (null on network error) |
|
||||||
|
| `durationMs` | number | Wall-clock time in milliseconds |
|
||||||
|
| `error` | string \| null | Error code + message, if any |
|
||||||
|
| `timeout` | number | Configured timeout in ms |
|
||||||
|
|
||||||
|
### Analysis
|
||||||
|
|
||||||
|
Run the analyzer script to analyze the most recent log file:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun run utils:analyze_cw
|
||||||
|
```
|
||||||
|
|
||||||
|
Or specify a particular file:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 debug-scripts/analyze-cw-calls.py cw-api-logs/2026-03-02T14-30-05.123Z.jsonl
|
||||||
|
```
|
||||||
|
|
||||||
|
This executes `debug-scripts/analyze-cw-calls.py` which produces:
|
||||||
|
|
||||||
|
- Overview (total calls, error rate, time span)
|
||||||
|
- Duration statistics (min, max, mean, p50, p90, p95, p99, distribution histogram)
|
||||||
|
- Error breakdown by type and endpoint
|
||||||
|
- Top 20 slowest calls
|
||||||
|
- Per-endpoint stats (count, errors, mean, p50, p95, max, total time)
|
||||||
|
- Timeline (per-minute throughput and errors)
|
||||||
|
- Concurrency hotspot detection
|
||||||
|
- Summary with recommendations
|
||||||
|
|
||||||
|
To clear all logs:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rm -rf cw-api-logs/
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Cache Invalidation
|
||||||
|
|
||||||
|
Mutation endpoints invalidate the relevant cache keys so the next read fetches fresh data from CW:
|
||||||
|
|
||||||
|
| Mutation | Cache invalidated |
|
||||||
|
| ------------------------------ | ---------------------------------------------------------------- |
|
||||||
|
| Create/update/delete note | `opp:notes:{cwOpportunityId}` via `invalidateNotesCache()` |
|
||||||
|
| Create/update/delete contact | `opp:contacts:{cwOpportunityId}` via `invalidateContactsCache()` |
|
||||||
|
| Add/update/resequence products | `opp:products:{cwOpportunityId}` via `invalidateProductsCache()` |
|
||||||
|
| Refresh opportunity | All keys for that opportunity (via re-fetch) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ConnectWise API Configuration
|
||||||
|
|
||||||
|
The shared Axios instance (`connectWiseApi`) is configured in `src/constants.ts`:
|
||||||
|
|
||||||
|
| Setting | Value | Purpose |
|
||||||
|
| --------- | ---------------------------------------------------- | ------------------------------ |
|
||||||
|
| `baseURL` | `https://ttscw.totaltech.net/v4_6_release/apis/3.0/` | CW API base |
|
||||||
|
| `timeout` | 30,000 ms (30s) | Per-request timeout |
|
||||||
|
| Logger | `attachCwApiLogger()` | Writes to `cw-api-calls.jsonl` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Architecture diagram
|
||||||
|
|
||||||
|
```
|
||||||
|
src/index.ts
|
||||||
|
│
|
||||||
|
├─ setInterval(refreshOpportunityCache, 20m)
|
||||||
|
│
|
||||||
|
└─► src/modules/cache/opportunityCache.ts
|
||||||
|
│
|
||||||
|
├─ prisma.opportunity.findMany(orderBy: cwLastUpdated DESC)
|
||||||
|
├─ redis.pipeline().exists(...) ← batch key check
|
||||||
|
│
|
||||||
|
├─ Build thunk list (lazy functions)
|
||||||
|
│
|
||||||
|
└─ Execute thunks with CONCURRENCY=6, DELAY=250ms
|
||||||
|
│
|
||||||
|
├─► fetchAndCacheOppCwData() ─► opportunityCw.fetch()
|
||||||
|
├─► fetchAndCacheActivities() ─► activityCw.fetchByOpportunityDirect()
|
||||||
|
├─► fetchAndCacheNotes() ─► opportunityCw.fetchNotes()
|
||||||
|
├─► fetchAndCacheContacts() ─► opportunityCw.fetchContacts()
|
||||||
|
├─► fetchAndCacheProducts() ─► opportunityCw.fetchProducts() + fetchProcurementProducts()
|
||||||
|
├─► fetchAndCacheCompanyCwData() ─► fetchCwCompanyById() + contacts
|
||||||
|
└─► fetchAndCacheSite() ─► fetchCompanySite() (lazy only)
|
||||||
|
│
|
||||||
|
└─► connectWiseApi.get(...) ← withCwRetry + cwApiLogger interceptors
|
||||||
|
│
|
||||||
|
└─► Redis SET with computed TTL
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## File reference
|
||||||
|
|
||||||
|
| File | Purpose |
|
||||||
|
| ---------------------------------------------------------------- | ------------------------------------------------------------- |
|
||||||
|
| `src/modules/cache/opportunityCache.ts` | Cache read/write helpers, background refresh logic |
|
||||||
|
| `src/modules/algorithms/computeCacheTTL.ts` | Primary adaptive TTL algorithm |
|
||||||
|
| `src/modules/algorithms/computeSubResourceCacheTTL.ts` | Sub-resource (notes, contacts) TTL algorithm |
|
||||||
|
| `src/modules/algorithms/computeProductsCacheTTL.ts` | Products TTL algorithm |
|
||||||
|
| `src/modules/cw-utils/withCwRetry.ts` | Retry wrapper with exponential backoff |
|
||||||
|
| `src/modules/cw-utils/cwApiLogger.ts` | Axios interceptor for JSONL call logging |
|
||||||
|
| `src/modules/cw-utils/fetchCompany.ts` | Company fetch with retry |
|
||||||
|
| `src/modules/cw-utils/procurement/listenInventoryAdjustments.ts` | Adjustment listener for targeted catalog-item cache + DB sync |
|
||||||
|
| `src/modules/cache/salesOpportunityMetricsCache.ts` | 5-minute active-member opportunity metrics cache |
|
||||||
|
| `src/constants.ts` | CW Axios instance config (timeout, logger) |
|
||||||
|
| `src/index.ts` | Refresh interval registration |
|
||||||
|
| `debug-scripts/analyze-cw-calls.py` | CW API call analysis script |
|
||||||
+10
-1
@@ -43,6 +43,12 @@ RUN apt-get update && \
|
|||||||
# Copy the compiled binary from the build stage
|
# Copy the compiled binary from the build stage
|
||||||
COPY --from=build /app/server ./server
|
COPY --from=build /app/server ./server
|
||||||
|
|
||||||
|
# Quote PDF branding asset loaded at runtime by pdf generation
|
||||||
|
COPY logo.png ./logo.png
|
||||||
|
|
||||||
|
# Sales tax lookup data loaded by expectedSalesTax at runtime
|
||||||
|
COPY --from=build /app/src/modules/sales-utils/salesTaxRates.json ./salesTaxRates.json
|
||||||
|
|
||||||
# Copy Prisma artifacts needed at runtime
|
# Copy Prisma artifacts needed at runtime
|
||||||
COPY --from=build /app/generated/ ./generated/
|
COPY --from=build /app/generated/ ./generated/
|
||||||
COPY --from=build /app/prisma/ ./prisma/
|
COPY --from=build /app/prisma/ ./prisma/
|
||||||
@@ -67,4 +73,7 @@ RUN bun install --frozen-lockfile
|
|||||||
COPY prisma/ prisma/
|
COPY prisma/ prisma/
|
||||||
COPY prisma.config.ts ./
|
COPY prisma.config.ts ./
|
||||||
|
|
||||||
CMD ["bunx", "prisma", "migrate", "deploy"]
|
COPY prisma/migrate-entrypoint.sh ./prisma/migrate-entrypoint.sh
|
||||||
|
RUN chmod +x prisma/migrate-entrypoint.sh
|
||||||
|
|
||||||
|
CMD ["sh", "prisma/migrate-entrypoint.sh"]
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
setInternalReview - The quote is ready to be review before it is ready to be sent.
|
||||||
|
setInternalApproved - The quote has been approved and is ready to be sent out.
|
||||||
|
setQuoteSent - The Quote has been sent to the customer.
|
||||||
|
setQuoteConfirmed - The quote has been recieved by the customer.
|
||||||
|
setRevisionNeeded - The quote needs to be revised and is set to stage revision
|
||||||
|
setFinalized - This locks any non-admins from modifying the quote saying that is the final iteration of the quote.
|
||||||
|
convert - This converts the quote to a ticket. It will also update all the necessary fields.
|
||||||
|
|
||||||
|
addTime(activityId, user: string)
|
||||||
|
|
||||||
|
fetchProducts
|
||||||
|
updateProduct
|
||||||
|
addProduct
|
||||||
|
|
||||||
|
fetchNotes
|
||||||
|
addNotes(note: string, user: string)
|
||||||
|
|
||||||
|
# Cat/SubCat/Bucket
|
||||||
|
|
||||||
|
## Ecosystems vs Categories
|
||||||
|
|
||||||
|
## Ecosystem Tree
|
||||||
|
|
||||||
|
- Networking
|
||||||
|
- Manufacturer: Ubiquiti
|
||||||
|
- Category: Technology
|
||||||
|
- Subcategory: Network-\*
|
||||||
|
- Manufacturer: TP-Link
|
||||||
|
- Category: Technology
|
||||||
|
- Subcategory: Network-\*
|
||||||
|
- Video Surveillance
|
||||||
|
- Manufacturer: Uniview
|
||||||
|
- Category: Field
|
||||||
|
- Subcategory: Surveillance-\*
|
||||||
|
- Manufacturer: Hikvision
|
||||||
|
- Category: Field
|
||||||
|
- Subcategory: Surveillance-\*
|
||||||
|
- Manufacturer: Alarm.com
|
||||||
|
- Category: Field
|
||||||
|
- Subcategory: Surveillance-\*
|
||||||
|
- Burg/Alarm
|
||||||
|
- Manufacturer: Qolsys
|
||||||
|
- Category: Field
|
||||||
|
- Subcategory: AlarmBurg-\*
|
||||||
|
- DSC
|
||||||
|
- Category: Field
|
||||||
|
- Subcategory: AlarmBurg-\*
|
||||||
|
|
||||||
|
## Category Tree
|
||||||
|
|
||||||
|
- Technology
|
||||||
|
- GeneralEquip
|
||||||
|
- Home Entertainment
|
||||||
|
- Monitor
|
||||||
|
- Printers
|
||||||
|
- Storage
|
||||||
|
- Network
|
||||||
|
- Network-Other
|
||||||
|
- Network-Router
|
||||||
|
- Network-Switch
|
||||||
|
- Network-Wireless
|
||||||
|
- Computer
|
||||||
|
- Computer-Components
|
||||||
|
- Computer-Desktop
|
||||||
|
- Computer-Laptop
|
||||||
|
- Recurring
|
||||||
|
- Recurring - Online
|
||||||
|
- Recurring - Other
|
||||||
|
- Recurring - Protection
|
||||||
|
- Recurring - Telephone
|
||||||
|
- Telephone
|
||||||
|
- Tele-HSet-Digital
|
||||||
|
- Tele-HSet-IP
|
||||||
|
- Tele-HSet-SLT
|
||||||
|
- Tele-Misc
|
||||||
|
- Tele-Paging
|
||||||
|
- Tele-SystemCards
|
||||||
|
- Tele-Systems
|
||||||
|
- General
|
||||||
|
- Batteries
|
||||||
|
- Battery Backups
|
||||||
|
- BulkWire
|
||||||
|
- Cables
|
||||||
|
- Cables-Adapters
|
||||||
|
- Cables-HDMI
|
||||||
|
- Cables-Network
|
||||||
|
- Cables-Other
|
||||||
|
- Cables-USB
|
||||||
|
- Cables-VGA
|
||||||
|
- Elec Cords & Adapters
|
||||||
|
- Enclosures
|
||||||
|
- PowerSupply
|
||||||
|
- RackEquip
|
||||||
|
- RackEquip-Rack
|
||||||
|
- RackEquip-Shelves
|
||||||
|
- Field
|
||||||
|
- Conduit
|
||||||
|
- Electric
|
||||||
|
- GateControl
|
||||||
|
- Locksets
|
||||||
|
- Other
|
||||||
|
- Relays
|
||||||
|
- AccessControl
|
||||||
|
- AccessControl-Controllers
|
||||||
|
- AccessControl-Credential
|
||||||
|
- AccessControl-LockDevices
|
||||||
|
- AccessControl-Other
|
||||||
|
- AccessControl-Readers
|
||||||
|
- AccessControl-VideoEntry
|
||||||
|
- AlarmBurg
|
||||||
|
- AlarmBurg-Communicators
|
||||||
|
- AlarmBurg-Keypads
|
||||||
|
- AlarmBurg-Modules
|
||||||
|
- AlarmBurg-Other
|
||||||
|
- AlarmBurg-Panels
|
||||||
|
- AlarmBurg-Sensors
|
||||||
|
- AlarmBurg-Sensors-Wireless
|
||||||
|
- AlarmBurg-Sensors-Wired
|
||||||
|
- AlarmBurg-Siren
|
||||||
|
- AlarmFire
|
||||||
|
- AlarmFire-Communicators
|
||||||
|
- AlarmFire-Devices
|
||||||
|
- AlarmFire-Modules
|
||||||
|
- AlarmFire-Other
|
||||||
|
- AlarmFire-Panels
|
||||||
|
- AlarmFire-Sensors
|
||||||
|
- Automation
|
||||||
|
- Automation-General
|
||||||
|
- Automation-HVAC
|
||||||
|
- Automation-Lights
|
||||||
|
- Automation-Locks
|
||||||
|
- Automation-Thermostat
|
||||||
|
- AV
|
||||||
|
- AV-Adapters&Cables
|
||||||
|
- AV-Components
|
||||||
|
- AV-Mounts
|
||||||
|
- AV-Other
|
||||||
|
- AV-Speakers
|
||||||
|
- AV-Television
|
||||||
|
- StrCbl?
|
||||||
|
- StrCbl-Jacks
|
||||||
|
- StrCbl-PatchPanel
|
||||||
|
- StrCbl-Plates
|
||||||
|
- Surveillance
|
||||||
|
- Surveillance-Accs
|
||||||
|
- Surveillance-CamerasAnalog
|
||||||
|
- Surveillance-CamerasIP
|
||||||
|
- Surveillance-NVR
|
||||||
+271
-7
@@ -23,13 +23,14 @@ The permission validator supports special tokens for flexible permission managem
|
|||||||
|
|
||||||
### Company Permissions
|
### Company Permissions
|
||||||
|
|
||||||
| Permission Node | Description | Used In |
|
| Permission Node | Description | Used In |
|
||||||
| ------------------------------ | ------------------------------------------------------------------- | ------------------------------------------------------------------------------------ |
|
| ------------------------------ | ----------------------------------------------------------------------- | ------------------------------------------------------------------------------------ |
|
||||||
| `company.fetch` | Fetch a single company | [src/api/companies/[id]/fetch.ts](src/api/companies/[id]/fetch.ts) |
|
| `company.fetch` | Fetch a single company | [src/api/companies/[id]/fetch.ts](src/api/companies/[id]/fetch.ts) |
|
||||||
| `company.fetch.address` | View company address information (requires `company.fetch` as well) | [src/api/companies/[id]/fetch.ts](src/api/companies/[id]/fetch.ts) |
|
| `company.fetch.address` | View company address information (requires `company.fetch` as well) | [src/api/companies/[id]/fetch.ts](src/api/companies/[id]/fetch.ts) |
|
||||||
| `company.fetch.contacts` | View all company contacts (requires `company.fetch` as well) | [src/api/companies/[id]/fetch.ts](src/api/companies/[id]/fetch.ts) |
|
| `company.fetch.contacts` | View all company contacts (requires `company.fetch` as well) | [src/api/companies/[id]/fetch.ts](src/api/companies/[id]/fetch.ts) |
|
||||||
| `company.fetch.many` | Fetch multiple companies | [src/api/companies/fetchAll.ts](src/api/companies/fetchAll.ts) |
|
| `company.fetch.many` | Fetch multiple companies | [src/api/companies/fetchAll.ts](src/api/companies/fetchAll.ts) |
|
||||||
| `company.fetch.configurations` | Fetch company configurations (requires `company.fetch` as well) | [src/api/companies/[id]/configurations.ts](src/api/companies/[id]/configurations.ts) |
|
| `company.fetch.configurations` | Fetch company configurations (requires `company.fetch` as well) | [src/api/companies/[id]/configurations.ts](src/api/companies/[id]/configurations.ts) |
|
||||||
|
| `company.fetch.sites` | Fetch company sites from ConnectWise (requires `company.fetch` as well) | [src/api/companies/[id]/sites.ts](src/api/companies/[id]/sites.ts) |
|
||||||
|
|
||||||
### Credential Permissions
|
### Credential Permissions
|
||||||
|
|
||||||
@@ -115,6 +116,97 @@ Admin-specific UI permissions that control visibility and data loading for admin
|
|||||||
- **Combine with API permissions**: A user with an admin UI permission should also have the corresponding API permission (e.g., `role.list`) to actually load data.
|
- **Combine with API permissions**: A user with an admin UI permission should also have the corresponding API permission (e.g., `role.list`) to actually load data.
|
||||||
- **Use wildcards for flexibility**: Grant `ui.navigation.*.view` to allow all navigation sections.
|
- **Use wildcards for flexibility**: Grant `ui.navigation.*.view` to allow all navigation sections.
|
||||||
|
|
||||||
|
### Procurement Permissions
|
||||||
|
|
||||||
|
| Permission Node | Description | Used In | Dependencies |
|
||||||
|
| --------------------------------------- | ---------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------- |
|
||||||
|
| `procurement.catalog.fetch` | Fetch a single catalog item | [src/api/procurement/[id]/fetch.ts](src/api/procurement/[id]/fetch.ts) | |
|
||||||
|
| `procurement.catalog.fetch.many` | Fetch multiple catalog items, count, categories/ecosystems, or filter values | [src/api/procurement/fetchAll.ts](src/api/procurement/fetchAll.ts), [src/api/procurement/count.ts](src/api/procurement/count.ts), [src/api/procurement/categories.ts](src/api/procurement/categories.ts), [src/api/procurement/filters.ts](src/api/procurement/filters.ts) | |
|
||||||
|
| `procurement.catalog.inventory.refresh` | Refresh on-hand inventory for a catalog item from ConnectWise | [src/api/procurement/[id]/refreshInventory.ts](src/api/procurement/[id]/refreshInventory.ts) | `procurement.catalog.fetch` |
|
||||||
|
| `procurement.catalog.link` | Link or unlink catalog items to each other | [src/api/procurement/[id]/link.ts](src/api/procurement/[id]/link.ts), [src/api/procurement/[id]/unlink.ts](src/api/procurement/[id]/unlink.ts) | `procurement.catalog.fetch` |
|
||||||
|
|
||||||
|
### ConnectWise Routes
|
||||||
|
|
||||||
|
`GET /v1/cw/members` requires only authentication (any logged-in user) and does **not** require a specific permission node.
|
||||||
|
|
||||||
|
`POST /v1/cw/callback/:secret/:resource` is intentionally unauthenticated for inbound ConnectWise callbacks and does **not** require a permission node.
|
||||||
|
|
||||||
|
| Permission Node | Description | Used In | Dependencies |
|
||||||
|
| --------------- | ------------------------------------------------------------------------------- | -------------------------------------------------------- | ------------ |
|
||||||
|
| _None_ | Fetch CW members (auth only) | [src/api/cw/fetchMembers.ts](src/api/cw/fetchMembers.ts) | N/A |
|
||||||
|
| _None_ | Inbound callback route; secured operationally (network controls / source trust) | [src/api/cw/callback.ts](src/api/cw/callback.ts) | N/A |
|
||||||
|
|
||||||
|
### Sales Permissions
|
||||||
|
|
||||||
|
Permissions for accessing and managing sales opportunities. Opportunities are synced from ConnectWise and stored locally; sub-resources (products, notes, contacts) are fetched live from CW.
|
||||||
|
|
||||||
|
**WebSocket note:** The `/secure` socket event chain `opp:live_quote_preview` and `opp:live_quote_preview:<id>:data` is gated by `sales.opportunity.fetch`.
|
||||||
|
|
||||||
|
| Permission Node | Description | Used In | Dependencies |
|
||||||
|
| ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------ |
|
||||||
|
| `sales.opportunity.fetch` | Fetch a single opportunity and its CW sub-resources (products, notes, contacts) | [src/api/sales/opportunities/[id]/fetch.ts](src/api/sales/opportunities/[id]/fetch.ts), [src/api/sales/opportunities/[id]/products/fetchAll.ts](src/api/sales/opportunities/[id]/products/fetchAll.ts), [src/api/sales/opportunities/[id]/notes/fetchAll.ts](src/api/sales/opportunities/[id]/notes/fetchAll.ts), [src/api/sales/opportunities/[id]/notes/fetch.ts](src/api/sales/opportunities/[id]/notes/fetch.ts), [src/api/sales/opportunities/[id]/contacts/fetchAll.ts](src/api/sales/opportunities/[id]/contacts/fetchAll.ts), [src/api/sockets/events/liveQuotePreview.ts](src/api/sockets/events/liveQuotePreview.ts) | |
|
||||||
|
| `sales.opportunity.fetch.many` | Fetch multiple opportunities (paginated/searchable), count, or opportunity types | [src/api/sales/opportunities/fetchAll.ts](src/api/sales/opportunities/fetchAll.ts), [src/api/sales/opportunities/count.ts](src/api/sales/opportunities/count.ts), [src/api/sales/opportunities/fetchTypes.ts](src/api/sales/opportunities/fetchTypes.ts) | |
|
||||||
|
| `sales.opportunity.fetch.@me` | View the personal sales dashboard showing opportunities assigned to the current user | UI-only (client-side gate) | |
|
||||||
|
| `sales.opportunity.fetch.all` | View all opportunities across all users (All Opportunities tab and View All button in the sales dashboard) | UI-only (client-side gate) | `sales.opportunity.fetch.many` |
|
||||||
|
| `sales.opportunity.metrics.all` | Allow `scope=all` on sales opportunity metrics endpoint to read cached metrics for all active members | [src/api/sales/opportunities/metrics.ts](src/api/sales/opportunities/metrics.ts) | `sales.opportunity.fetch.many` |
|
||||||
|
| `sales.opportunity.metrics.identifier.override` | Allow `identifier=<cwIdentifier>` override on sales opportunity metrics endpoint for querying another member | [src/api/sales/opportunities/metrics.ts](src/api/sales/opportunities/metrics.ts) | `sales.opportunity.fetch.many` |
|
||||||
|
| `sales.opportunity.refresh` | Refresh a single opportunity's local data from ConnectWise | [src/api/sales/opportunities/[id]/refresh.ts](src/api/sales/opportunities/[id]/refresh.ts) | `sales.opportunity.fetch` |
|
||||||
|
| `sales.opportunity.update` | Update an opportunity's fields (rating, sales rep, company, contact, site, description, etc.) in ConnectWise | [src/api/sales/opportunities/[id]/update.ts](src/api/sales/opportunities/[id]/update.ts) | `sales.opportunity.fetch` |
|
||||||
|
| `sales.opportunity.create` | Create a new opportunity in ConnectWise | [src/api/sales/opportunities/create.ts](src/api/sales/opportunities/create.ts) | |
|
||||||
|
| `sales.opportunity.delete` | Delete an opportunity from ConnectWise and the local database | [src/api/sales/opportunities/[id]/delete.ts](src/api/sales/opportunities/[id]/delete.ts) | `sales.opportunity.fetch` |
|
||||||
|
| `sales.opportunity.note.create` | Create a new note on an opportunity | [src/api/sales/opportunities/[id]/notes/create.ts](src/api/sales/opportunities/[id]/notes/create.ts) | `sales.opportunity.fetch` |
|
||||||
|
| `sales.opportunity.note.update` | Update an existing note on an opportunity | [src/api/sales/opportunities/[id]/notes/update.ts](src/api/sales/opportunities/[id]/notes/update.ts) | `sales.opportunity.fetch` |
|
||||||
|
| `sales.opportunity.note.delete` | Delete a note from an opportunity | [src/api/sales/opportunities/[id]/notes/delete.ts](src/api/sales/opportunities/[id]/notes/delete.ts) | `sales.opportunity.fetch` |
|
||||||
|
| `sales.opportunity.product.update` | Update products (forecast items) on an opportunity, including resequencing | [src/api/sales/opportunities/[id]/products/resequence.ts](src/api/sales/opportunities/[id]/products/resequence.ts), [src/api/sales/opportunities/[id]/products/update.ts](src/api/sales/opportunities/[id]/products/update.ts), [src/api/sales/opportunities/[id]/products/cancel.ts](src/api/sales/opportunities/[id]/products/cancel.ts) | `sales.opportunity.fetch` |
|
||||||
|
| `sales.opportunity.product.delete` | Delete a product (forecast item) from an opportunity | [src/api/sales/opportunities/[id]/products/delete.ts](src/api/sales/opportunities/[id]/products/delete.ts) | `sales.opportunity.fetch` |
|
||||||
|
| `sales.opportunity.product.add` | Add a new product (forecast item) to an opportunity. Individual fields gated by `sales.opportunity.product.field.<field>` permissions. | [src/api/sales/opportunities/[id]/products/add.ts](src/api/sales/opportunities/[id]/products/add.ts) | `sales.opportunity.fetch` |
|
||||||
|
| `sales.opportunity.product.add.specialOrder` | Add one or more "SPECIAL ORDER" products via the dedicated special-order route. | [src/api/sales/opportunities/[id]/products/addSpecialOrder.ts](src/api/sales/opportunities/[id]/products/addSpecialOrder.ts) | `sales.opportunity.fetch` |
|
||||||
|
| `sales.opportunity.product.add.labor` | Add labor products via the dedicated labor route with Field/Tech catalog selection and labor pricing inputs. | [src/api/sales/opportunities/[id]/products/addLabor.ts](src/api/sales/opportunities/[id]/products/addLabor.ts), [src/api/sales/opportunities/[id]/products/laborOptions.ts](src/api/sales/opportunities/[id]/products/laborOptions.ts) | `sales.opportunity.fetch` |
|
||||||
|
| `sales.opportunity.quote.fetch` | Fetch all committed quotes for an opportunity. | [src/api/sales/opportunities/[id]/quotes/fetchAll.ts](src/api/sales/opportunities/[id]/quotes/fetchAll.ts) | `sales.opportunity.fetch` |
|
||||||
|
| `sales.opportunity.quote.commit` | Generate and store a finalized quote PDF for an opportunity with regeneration metadata and creator attribution. | [src/api/sales/opportunities/[id]/quotes/commit.ts](src/api/sales/opportunities/[id]/quotes/commit.ts) | `sales.opportunity.fetch` |
|
||||||
|
| `sales.opportunity.quote.preview` | Generate a preview-stamped quote PDF for an opportunity without storing it. | [src/api/sales/opportunities/[id]/quotes/preview.ts](src/api/sales/opportunities/[id]/quotes/preview.ts) | `sales.opportunity.fetch` |
|
||||||
|
| `sales.opportunity.quote.download` | Download a committed quote PDF. Each download is recorded with timestamp and user info. | [src/api/sales/opportunities/[id]/quotes/download.ts](src/api/sales/opportunities/[id]/quotes/download.ts) | `sales.opportunity.fetch` |
|
||||||
|
| `sales.opportunity.quote.fetch_downloads` | Fetch download/print history for all quotes on an opportunity. Admin-level permission. | [src/api/sales/opportunities/[id]/quotes/fetchDownloads.ts](src/api/sales/opportunities/[id]/quotes/fetchDownloads.ts) | `sales.opportunity.fetch` |
|
||||||
|
| `sales.opportunity.view_margin` | View margin and markup data on opportunity products. Controls visibility of margin %, markup %, and related progress bars in the UI. | UI-only (client-side gate) | `sales.opportunity.fetch` |
|
||||||
|
| `sales.opportunity.view_cost` | View cost data on opportunity products. Controls visibility of unit cost, total cost, and recurring cost in the UI. | UI-only (client-side gate) | `sales.opportunity.fetch` |
|
||||||
|
| `sales.opportunity.view_profit` | View profit data on opportunity products. Controls visibility of profit values in the UI. | UI-only (client-side gate) | `sales.opportunity.fetch` |
|
||||||
|
| `sales.opportunity.workflow` | Execute opportunity workflow actions (status transitions, review decisions, quote sending, etc.). Base gate for the workflow dispatch endpoint. | [dispatch.ts](src/api/sales/opportunities/[id]/workflow/dispatch.ts) | `sales.opportunity.fetch` |
|
||||||
|
| `sales.opportunity.finalize` | Finalize an opportunity as Won or Lost. Without this permission, win/lose actions route to PendingWon/PendingLost instead. | [src/workflows/wf.opportunity.ts](src/workflows/wf.opportunity.ts), [dispatch.ts](src/api/sales/opportunities/[id]/workflow/dispatch.ts) | `sales.opportunity.workflow` |
|
||||||
|
| `sales.opportunity.cancel` | Cancel an opportunity. Required to transition any eligible opportunity to the Canceled status. | [src/workflows/wf.opportunity.ts](src/workflows/wf.opportunity.ts), [dispatch.ts](src/api/sales/opportunities/[id]/workflow/dispatch.ts) | `sales.opportunity.workflow` |
|
||||||
|
| `sales.opportunity.review` | Submit an opportunity for internal review. Required to transition an opportunity into the InternalReview status. | [src/workflows/wf.opportunity.ts](src/workflows/wf.opportunity.ts) | `sales.opportunity.workflow` |
|
||||||
|
| `sales.opportunity.send` | Send a quote to the customer. Required to transition an opportunity to QuoteSent (and compound transitions like immediate won/lost/confirmed). | [src/workflows/wf.opportunity.ts](src/workflows/wf.opportunity.ts) | `sales.opportunity.workflow` |
|
||||||
|
| `sales.opportunity.reopen` | Re-open a cancelled opportunity. Required to transition an opportunity from Canceled back to Active. | [src/workflows/wf.opportunity.ts](src/workflows/wf.opportunity.ts) | `sales.opportunity.workflow` |
|
||||||
|
| `sales.opportunity.win` | Mark an opportunity as won (or pending won). Gates the win button in the UI. Required for finalize(won), sendQuote(won), and transitionToPending(won). | [src/workflows/wf.opportunity.ts](src/workflows/wf.opportunity.ts) | `sales.opportunity.workflow` |
|
||||||
|
| `sales.opportunity.lose` | Mark an opportunity as lost (or pending lost). Gates the lose button in the UI. Required for finalize(lost), sendQuote(lost), and transitionToPending(lost). | [src/workflows/wf.opportunity.ts](src/workflows/wf.opportunity.ts) | `sales.opportunity.workflow` |
|
||||||
|
| `sales.isRepresentative` | Designates the user as a sales representative; used for reporting and filtering purposes. | _(not yet used in routes)_ | |
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><strong>Field-level permissions for <code>sales.opportunity.product.add</code></strong></summary>
|
||||||
|
|
||||||
|
Each submitted field is gated by a `sales.opportunity.product.field.<field>` permission node. Only fields the user has permission for are forwarded to ConnectWise.
|
||||||
|
|
||||||
|
| Field Permission Node | Description |
|
||||||
|
| ----------------------------------------------------- | -------------------------------------------------------- |
|
||||||
|
| `sales.opportunity.product.field.catalogItem` | Set the catalog item reference |
|
||||||
|
| `sales.opportunity.product.field.forecastDescription` | Set the forecast description |
|
||||||
|
| `sales.opportunity.product.field.productDescription` | Set the product description |
|
||||||
|
| `sales.opportunity.product.field.quantity` | Set the quantity |
|
||||||
|
| `sales.opportunity.product.field.status` | Set the status reference |
|
||||||
|
| `sales.opportunity.product.field.productClass` | Set the product class (e.g. Product, Service, Agreement) |
|
||||||
|
| `sales.opportunity.product.field.forecastType` | Set the forecast type |
|
||||||
|
| `sales.opportunity.product.field.revenue` | Set the revenue amount |
|
||||||
|
| `sales.opportunity.product.field.cost` | Set the cost amount |
|
||||||
|
| `sales.opportunity.product.field.includeFlag` | Set the include flag |
|
||||||
|
| `sales.opportunity.product.field.linkFlag` | Set the link flag |
|
||||||
|
| `sales.opportunity.product.field.recurringFlag` | Set the recurring flag |
|
||||||
|
| `sales.opportunity.product.field.taxableFlag` | Set the taxable flag |
|
||||||
|
| `sales.opportunity.product.field.recurringRevenue` | Set the recurring revenue amount |
|
||||||
|
| `sales.opportunity.product.field.recurringCost` | Set the recurring cost amount |
|
||||||
|
| `sales.opportunity.product.field.cycles` | Set the number of recurring cycles |
|
||||||
|
| `sales.opportunity.product.field.sequenceNumber` | Set the sequence number (display order) |
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
### UniFi Permissions
|
### UniFi Permissions
|
||||||
|
|
||||||
Permissions for accessing and managing UniFi network infrastructure. The `unifi.access` permission is a gate permission required for **all** UniFi routes.
|
Permissions for accessing and managing UniFi network infrastructure. The `unifi.access` permission is a gate permission required for **all** UniFi routes.
|
||||||
@@ -152,6 +244,178 @@ The WiFi fetch route uses `processObjectValuePerms` to filter each WLAN object o
|
|||||||
| `unifi.site.wifi.ppsk` | View private pre-shared keys (PPSKs) for a specific WiFi network | [src/api/unifi/site/wifi/ppskFetchAll.ts](src/api/unifi/site/wifi/ppskFetchAll.ts), [src/api/unifi/site/wifi/ppskCreate.ts](src/api/unifi/site/wifi/ppskCreate.ts) | `unifi.access`, `unifi.site.wifi` |
|
| `unifi.site.wifi.ppsk` | View private pre-shared keys (PPSKs) for a specific WiFi network | [src/api/unifi/site/wifi/ppskFetchAll.ts](src/api/unifi/site/wifi/ppskFetchAll.ts), [src/api/unifi/site/wifi/ppskCreate.ts](src/api/unifi/site/wifi/ppskCreate.ts) | `unifi.access`, `unifi.site.wifi` |
|
||||||
| `unifi.site.wifi.ppsk.create` | Create a private pre-shared key on a specific WiFi network | [src/api/unifi/site/wifi/ppskCreate.ts](src/api/unifi/site/wifi/ppskCreate.ts) | `unifi.access`, `unifi.site.wifi`, `unifi.site.wifi.ppsk` |
|
| `unifi.site.wifi.ppsk.create` | Create a private pre-shared key on a specific WiFi network | [src/api/unifi/site/wifi/ppskCreate.ts](src/api/unifi/site/wifi/ppskCreate.ts) | `unifi.access`, `unifi.site.wifi`, `unifi.site.wifi.ppsk` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Object Type Permissions (Field-Level Gating)
|
||||||
|
|
||||||
|
All fetch and fetchAll routes gate response object keys using `processObjectValuePerms`. For each object type, only fields whose corresponding `<scope>.<field>` permission the user holds are included in the response. Grant `<scope>.*` to allow all fields on that object type.
|
||||||
|
|
||||||
|
### Company (`obj.company`)
|
||||||
|
|
||||||
|
| Field Permission | Description |
|
||||||
|
| --------------------------- | ----------------------------------------- |
|
||||||
|
| `obj.company.id` | View company ID |
|
||||||
|
| `obj.company.name` | View company name |
|
||||||
|
| `obj.company.cw_Identifier` | View ConnectWise identifier |
|
||||||
|
| `obj.company.cw_CompanyId` | View ConnectWise company ID |
|
||||||
|
| `obj.company.cw_Data` | View ConnectWise data (address, contacts) |
|
||||||
|
| `obj.company.createdAt` | View creation timestamp |
|
||||||
|
| `obj.company.updatedAt` | View last-updated timestamp |
|
||||||
|
|
||||||
|
**Used in:** [src/api/companies/[id]/fetch.ts](src/api/companies/[id]/fetch.ts), [src/api/companies/fetchAll.ts](src/api/companies/fetchAll.ts)
|
||||||
|
|
||||||
|
### Credential (`obj.credential`)
|
||||||
|
|
||||||
|
| Field Permission | Description |
|
||||||
|
| ---------------------------------- | ----------------------------- |
|
||||||
|
| `obj.credential.id` | View credential ID |
|
||||||
|
| `obj.credential.name` | View credential name |
|
||||||
|
| `obj.credential.notes` | View credential notes |
|
||||||
|
| `obj.credential.typeId` | View credential type ID |
|
||||||
|
| `obj.credential.companyId` | View linked company ID |
|
||||||
|
| `obj.credential.subCredentialOfId` | View parent credential ID |
|
||||||
|
| `obj.credential.fields` | View credential field values |
|
||||||
|
| `obj.credential.type` | View credential type object |
|
||||||
|
| `obj.credential.company` | View linked company object |
|
||||||
|
| `obj.credential.subCredentials` | View sub-credentials array |
|
||||||
|
| `obj.credential.secureFieldIds` | View secure field identifiers |
|
||||||
|
| `obj.credential.createdAt` | View creation timestamp |
|
||||||
|
| `obj.credential.updatedAt` | View last-updated timestamp |
|
||||||
|
|
||||||
|
**Used in:** [src/api/credentials/fetch.ts](src/api/credentials/fetch.ts), [src/api/credentials/fetchByCompany.ts](src/api/credentials/fetchByCompany.ts), [src/api/credentials/fetchSubCredentials.ts](src/api/credentials/fetchSubCredentials.ts), [src/api/credential-types/fetchCredentials.ts](src/api/credential-types/fetchCredentials.ts)
|
||||||
|
|
||||||
|
### Credential Type (`obj.credentialType`)
|
||||||
|
|
||||||
|
| Field Permission | Description |
|
||||||
|
| ------------------------------------ | ----------------------------------------- |
|
||||||
|
| `obj.credentialType.id` | View credential type ID |
|
||||||
|
| `obj.credentialType.name` | View credential type name |
|
||||||
|
| `obj.credentialType.permissionScope` | View permission scope |
|
||||||
|
| `obj.credentialType.icon` | View icon |
|
||||||
|
| `obj.credentialType.fields` | View field definitions |
|
||||||
|
| `obj.credentialType.credentialCount` | View count of credentials using this type |
|
||||||
|
| `obj.credentialType.createdAt` | View creation timestamp |
|
||||||
|
| `obj.credentialType.updatedAt` | View last-updated timestamp |
|
||||||
|
|
||||||
|
**Used in:** [src/api/credential-types/fetch.ts](src/api/credential-types/fetch.ts), [src/api/credential-types/fetchAll.ts](src/api/credential-types/fetchAll.ts)
|
||||||
|
|
||||||
|
### User (`obj.user`)
|
||||||
|
|
||||||
|
| Field Permission | Description |
|
||||||
|
| ---------------------- | -------------------------------- |
|
||||||
|
| `obj.user.id` | View user ID |
|
||||||
|
| `obj.user.name` | View user display name |
|
||||||
|
| `obj.user.roles` | View assigned role monikers |
|
||||||
|
| `obj.user.permissions` | View aggregated permission nodes |
|
||||||
|
| `obj.user.login` | View login identifier |
|
||||||
|
| `obj.user.email` | View email address |
|
||||||
|
| `obj.user.image` | View profile image URL |
|
||||||
|
| `obj.user.createdAt` | View creation timestamp |
|
||||||
|
| `obj.user.updatedAt` | View last-updated timestamp |
|
||||||
|
|
||||||
|
**Used in:** [src/api/user/@me/fetch.ts](src/api/user/@me/fetch.ts), [src/api/user/fetch.ts](src/api/user/fetch.ts), [src/api/user/fetchAll.ts](src/api/user/fetchAll.ts), [src/api/roles/getUsers.ts](src/api/roles/getUsers.ts)
|
||||||
|
|
||||||
|
### Role (`obj.role`)
|
||||||
|
|
||||||
|
| Field Permission | Description |
|
||||||
|
| ---------------------- | -------------------------------- |
|
||||||
|
| `obj.role.id` | View role ID |
|
||||||
|
| `obj.role.title` | View role title |
|
||||||
|
| `obj.role.moniker` | View role moniker |
|
||||||
|
| `obj.role.permissions` | View role permission nodes |
|
||||||
|
| `obj.role.users` | View users assigned to this role |
|
||||||
|
| `obj.role.createdAt` | View creation timestamp |
|
||||||
|
| `obj.role.updatedAt` | View last-updated timestamp |
|
||||||
|
|
||||||
|
**Used in:** [src/api/roles/fetch.ts](src/api/roles/fetch.ts), [src/api/roles/fetchAll.ts](src/api/roles/fetchAll.ts), [src/api/user/fetchRoles.ts](src/api/user/fetchRoles.ts)
|
||||||
|
|
||||||
|
### Catalog Item (`obj.catalogItem`)
|
||||||
|
|
||||||
|
| Field Permission | Description |
|
||||||
|
| ------------------------------------- | -------------------------------- |
|
||||||
|
| `obj.catalogItem.id` | View catalog item ID |
|
||||||
|
| `obj.catalogItem.cwCatalogId` | View ConnectWise catalog ID |
|
||||||
|
| `obj.catalogItem.identifier` | View item identifier |
|
||||||
|
| `obj.catalogItem.name` | View item name |
|
||||||
|
| `obj.catalogItem.description` | View description |
|
||||||
|
| `obj.catalogItem.customerDescription` | View customer-facing description |
|
||||||
|
| `obj.catalogItem.internalNotes` | View internal notes |
|
||||||
|
| `obj.catalogItem.manufacturer` | View manufacturer name |
|
||||||
|
| `obj.catalogItem.manufactureCwId` | View manufacturer ConnectWise ID |
|
||||||
|
| `obj.catalogItem.partNumber` | View part number |
|
||||||
|
| `obj.catalogItem.vendorName` | View vendor name |
|
||||||
|
| `obj.catalogItem.vendorSku` | View vendor SKU |
|
||||||
|
| `obj.catalogItem.vendorCwId` | View vendor ConnectWise ID |
|
||||||
|
| `obj.catalogItem.price` | View price |
|
||||||
|
| `obj.catalogItem.cost` | View cost |
|
||||||
|
| `obj.catalogItem.inactive` | View inactive flag |
|
||||||
|
| `obj.catalogItem.salesTaxable` | View sales-taxable flag |
|
||||||
|
| `obj.catalogItem.onHand` | View on-hand inventory count |
|
||||||
|
| `obj.catalogItem.cwLastUpdated` | View CW last-updated timestamp |
|
||||||
|
| `obj.catalogItem.linkedItems` | View linked catalog items |
|
||||||
|
| `obj.catalogItem.createdAt` | View creation timestamp |
|
||||||
|
| `obj.catalogItem.updatedAt` | View last-updated timestamp |
|
||||||
|
|
||||||
|
**Used in:** [src/api/procurement/fetchAll.ts](src/api/procurement/fetchAll.ts), [src/api/procurement/[id]/fetch.ts](src/api/procurement/[id]/fetch.ts), [src/api/procurement/[id]/fetchLinked.ts](src/api/procurement/[id]/fetchLinked.ts)
|
||||||
|
|
||||||
|
### Opportunity (`obj.opportunity`)
|
||||||
|
|
||||||
|
| Field Permission | Description |
|
||||||
|
| ------------------------------------ | ------------------------------- |
|
||||||
|
| `obj.opportunity.id` | View opportunity ID |
|
||||||
|
| `obj.opportunity.cwOpportunityId` | View ConnectWise opportunity ID |
|
||||||
|
| `obj.opportunity.name` | View opportunity name |
|
||||||
|
| `obj.opportunity.notes` | View notes |
|
||||||
|
| `obj.opportunity.type` | View opportunity type |
|
||||||
|
| `obj.opportunity.stage` | View stage |
|
||||||
|
| `obj.opportunity.status` | View status |
|
||||||
|
| `obj.opportunity.priority` | View priority |
|
||||||
|
| `obj.opportunity.rating` | View rating |
|
||||||
|
| `obj.opportunity.source` | View source |
|
||||||
|
| `obj.opportunity.campaign` | View campaign |
|
||||||
|
| `obj.opportunity.primarySalesRep` | View primary sales rep |
|
||||||
|
| `obj.opportunity.secondarySalesRep` | View secondary sales rep |
|
||||||
|
| `obj.opportunity.company` | View company |
|
||||||
|
| `obj.opportunity.contact` | View contact |
|
||||||
|
| `obj.opportunity.site` | View site |
|
||||||
|
| `obj.opportunity.customerPO` | View customer PO |
|
||||||
|
| `obj.opportunity.totalSalesTax` | View total sales tax |
|
||||||
|
| `obj.opportunity.probability` | View probability percentage |
|
||||||
|
| `obj.opportunity.location` | View location |
|
||||||
|
| `obj.opportunity.department` | View department |
|
||||||
|
| `obj.opportunity.expectedCloseDate` | View expected close date |
|
||||||
|
| `obj.opportunity.pipelineChangeDate` | View pipeline change date |
|
||||||
|
| `obj.opportunity.dateBecameLead` | View date became lead |
|
||||||
|
| `obj.opportunity.closedDate` | View closed date |
|
||||||
|
| `obj.opportunity.closedFlag` | View closed flag |
|
||||||
|
| `obj.opportunity.closedBy` | View closed-by member |
|
||||||
|
| `obj.opportunity.companyId` | View linked company ID |
|
||||||
|
| `obj.opportunity.cwLastUpdated` | View CW last-updated timestamp |
|
||||||
|
| `obj.opportunity.createdAt` | View creation timestamp |
|
||||||
|
| `obj.opportunity.updatedAt` | View last-updated timestamp |
|
||||||
|
|
||||||
|
**Used in:** [src/api/sales/fetchAll.ts](src/api/sales/fetchAll.ts), [src/api/sales/[id]/fetch.ts](src/api/sales/[id]/fetch.ts)
|
||||||
|
|
||||||
|
### UniFi Site (`obj.unifiSite`)
|
||||||
|
|
||||||
|
| Field Permission | Description |
|
||||||
|
| ------------------------- | ----------------------------- |
|
||||||
|
| `obj.unifiSite.id` | View site internal ID |
|
||||||
|
| `obj.unifiSite.name` | View site name |
|
||||||
|
| `obj.unifiSite.siteId` | View UniFi controller site ID |
|
||||||
|
| `obj.unifiSite.companyId` | View linked company ID |
|
||||||
|
| `obj.unifiSite.company` | View linked company object |
|
||||||
|
| `obj.unifiSite.createdAt` | View creation timestamp |
|
||||||
|
| `obj.unifiSite.updatedAt` | View last-updated timestamp |
|
||||||
|
|
||||||
|
**Used in:** [src/api/unifi/sites/fetchAll.ts](src/api/unifi/sites/fetchAll.ts), [src/api/unifi/site/fetch.ts](src/api/unifi/site/fetch.ts), [src/api/companies/[id]/unifiSites.ts](src/api/companies/[id]/unifiSites.ts)
|
||||||
|
|
||||||
|
### WiFi Network (`unifi.site.wifi.read`)
|
||||||
|
|
||||||
|
See **UniFi Permissions > Field-Level Permission Gating** above for the full list of `unifi.site.wifi.read.<field>` nodes.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Permission Issuers
|
## Permission Issuers
|
||||||
|
|
||||||
Permissions can be issued by different sources:
|
Permissions can be issued by different sources:
|
||||||
|
|||||||
@@ -16,8 +16,11 @@
|
|||||||
"cors": "^2.8.6",
|
"cors": "^2.8.6",
|
||||||
"cuid": "^3.0.0",
|
"cuid": "^3.0.0",
|
||||||
"hono": "^4.11.5",
|
"hono": "^4.11.5",
|
||||||
|
"ioredis": "^5.10.0",
|
||||||
"jsonwebtoken": "^9.0.3",
|
"jsonwebtoken": "^9.0.3",
|
||||||
"keypair": "^1.0.4",
|
"keypair": "^1.0.4",
|
||||||
|
"pdf-lib": "^1.17.1",
|
||||||
|
"pdfmake": "^0.3.5",
|
||||||
"prisma": "^7.3.0",
|
"prisma": "^7.3.0",
|
||||||
"socket.io": "^4.8.3",
|
"socket.io": "^4.8.3",
|
||||||
"zod": "^4.3.6",
|
"zod": "^4.3.6",
|
||||||
@@ -57,8 +60,14 @@
|
|||||||
|
|
||||||
"@hono/node-server": ["@hono/node-server@1.19.9", "", { "peerDependencies": { "hono": "^4" } }, "sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw=="],
|
"@hono/node-server": ["@hono/node-server@1.19.9", "", { "peerDependencies": { "hono": "^4" } }, "sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw=="],
|
||||||
|
|
||||||
|
"@ioredis/commands": ["@ioredis/commands@1.5.1", "", {}, "sha512-JH8ZL/ywcJyR9MmJ5BNqZllXNZQqQbnVZOqpPQqE1vHiFgAw4NHbvE0FOduNU8IX9babitBT46571OnPTT0Zcw=="],
|
||||||
|
|
||||||
"@mrleebo/prisma-ast": ["@mrleebo/prisma-ast@0.13.1", "", { "dependencies": { "chevrotain": "^10.5.0", "lilconfig": "^2.1.0" } }, "sha512-XyroGQXcHrZdvmrGJvsA9KNeOOgGMg1Vg9OlheUsBOSKznLMDl+YChxbkboRHvtFYJEMRYmlV3uoo/njCw05iw=="],
|
"@mrleebo/prisma-ast": ["@mrleebo/prisma-ast@0.13.1", "", { "dependencies": { "chevrotain": "^10.5.0", "lilconfig": "^2.1.0" } }, "sha512-XyroGQXcHrZdvmrGJvsA9KNeOOgGMg1Vg9OlheUsBOSKznLMDl+YChxbkboRHvtFYJEMRYmlV3uoo/njCw05iw=="],
|
||||||
|
|
||||||
|
"@pdf-lib/standard-fonts": ["@pdf-lib/standard-fonts@1.0.0", "", { "dependencies": { "pako": "^1.0.6" } }, "sha512-hU30BK9IUN/su0Mn9VdlVKsWBS6GyhVfqjwl1FjZN4TxP6cCw0jP2w7V3Hf5uX7M0AZJ16vey9yE0ny7Sa59ZA=="],
|
||||||
|
|
||||||
|
"@pdf-lib/upng": ["@pdf-lib/upng@1.0.1", "", { "dependencies": { "pako": "^1.0.10" } }, "sha512-dQK2FUMQtowVP00mtIksrlZhdFXQZPC+taih1q4CvPZ5vqdxR/LKBaFg0oAfzd1GlHZXXSPdQfzQnt+ViGvEIQ=="],
|
||||||
|
|
||||||
"@prisma/adapter-pg": ["@prisma/adapter-pg@7.3.0", "", { "dependencies": { "@prisma/driver-adapter-utils": "7.3.0", "pg": "^8.16.3", "postgres-array": "3.0.4" } }, "sha512-iuYQMbIPO6i9O45Fv8TB7vWu00BXhCaNAShenqF7gLExGDbnGp5BfFB4yz1K59zQ59jF6tQ9YHrg0P6/J3OoLg=="],
|
"@prisma/adapter-pg": ["@prisma/adapter-pg@7.3.0", "", { "dependencies": { "@prisma/driver-adapter-utils": "7.3.0", "pg": "^8.16.3", "postgres-array": "3.0.4" } }, "sha512-iuYQMbIPO6i9O45Fv8TB7vWu00BXhCaNAShenqF7gLExGDbnGp5BfFB4yz1K59zQ59jF6tQ9YHrg0P6/J3OoLg=="],
|
||||||
|
|
||||||
"@prisma/client": ["@prisma/client@7.3.0", "", { "dependencies": { "@prisma/client-runtime-utils": "7.3.0" }, "peerDependencies": { "prisma": "*", "typescript": ">=5.4.0" }, "optionalPeers": ["prisma", "typescript"] }, "sha512-FXBIxirqQfdC6b6HnNgxGmU7ydCPEPk7maHMOduJJfnTP+MuOGa15X4omjR/zpPUUpm8ef/mEFQjJudOGkXFcQ=="],
|
"@prisma/client": ["@prisma/client@7.3.0", "", { "dependencies": { "@prisma/client-runtime-utils": "7.3.0" }, "peerDependencies": { "prisma": "*", "typescript": ">=5.4.0" }, "optionalPeers": ["prisma", "typescript"] }, "sha512-FXBIxirqQfdC6b6HnNgxGmU7ydCPEPk7maHMOduJJfnTP+MuOGa15X4omjR/zpPUUpm8ef/mEFQjJudOGkXFcQ=="],
|
||||||
@@ -93,6 +102,8 @@
|
|||||||
|
|
||||||
"@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
"@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||||
|
|
||||||
|
"@swc/helpers": ["@swc/helpers@0.5.19", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-QamiFeIK3txNjgUTNppE6MiG3p7TdninpZu0E0PbqVh1a9FNLT2FRhisaa4NcaX52XVhA5l7Pk58Ft7Sqi/2sA=="],
|
||||||
|
|
||||||
"@types/bun": ["@types/bun@1.3.6", "", { "dependencies": { "bun-types": "1.3.6" } }, "sha512-uWCv6FO/8LcpREhenN1d1b6fcspAB+cefwD7uti8C8VffIv0Um08TKMn98FynpTiU38+y2dUO55T11NgDt8VAA=="],
|
"@types/bun": ["@types/bun@1.3.6", "", { "dependencies": { "bun-types": "1.3.6" } }, "sha512-uWCv6FO/8LcpREhenN1d1b6fcspAB+cefwD7uti8C8VffIv0Um08TKMn98FynpTiU38+y2dUO55T11NgDt8VAA=="],
|
||||||
|
|
||||||
"@types/cors": ["@types/cors@2.8.19", "", { "dependencies": { "@types/node": "*" } }, "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg=="],
|
"@types/cors": ["@types/cors@2.8.19", "", { "dependencies": { "@types/node": "*" } }, "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg=="],
|
||||||
@@ -113,10 +124,14 @@
|
|||||||
|
|
||||||
"axios": ["axios@1.13.3", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.4", "proxy-from-env": "^1.1.0" } }, "sha512-ERT8kdX7DZjtUm7IitEyV7InTHAF42iJuMArIiDIV5YtPanJkgw4hw5Dyg9fh0mihdWNn1GKaeIWErfe56UQ1g=="],
|
"axios": ["axios@1.13.3", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.4", "proxy-from-env": "^1.1.0" } }, "sha512-ERT8kdX7DZjtUm7IitEyV7InTHAF42iJuMArIiDIV5YtPanJkgw4hw5Dyg9fh0mihdWNn1GKaeIWErfe56UQ1g=="],
|
||||||
|
|
||||||
|
"base64-js": ["base64-js@0.0.8", "", {}, "sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw=="],
|
||||||
|
|
||||||
"base64id": ["base64id@2.0.0", "", {}, "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog=="],
|
"base64id": ["base64id@2.0.0", "", {}, "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog=="],
|
||||||
|
|
||||||
"blakets": ["blakets@0.1.12", "", { "dependencies": { "@prokopschield/argv": "^0.1.0-2" }, "bin": { "blake": "lib/demo.js", "blake2b": "lib/cli.js", "blake2s": "lib/cli.js", "blakejs": "lib/demo.js", "blakets": "lib/demo.js" } }, "sha512-ReOnLTDRlbExlTXbJZoA2xkvhzauJ7ldpvhKnb1cUNw8gdAHWHWOWG8XMjwpxQmmEZCDAR7VZiM5BYTUSOLVrw=="],
|
"blakets": ["blakets@0.1.12", "", { "dependencies": { "@prokopschield/argv": "^0.1.0-2" }, "bin": { "blake": "lib/demo.js", "blake2b": "lib/cli.js", "blake2s": "lib/cli.js", "blakejs": "lib/demo.js", "blakets": "lib/demo.js" } }, "sha512-ReOnLTDRlbExlTXbJZoA2xkvhzauJ7ldpvhKnb1cUNw8gdAHWHWOWG8XMjwpxQmmEZCDAR7VZiM5BYTUSOLVrw=="],
|
||||||
|
|
||||||
|
"brotli": ["brotli@1.3.3", "", { "dependencies": { "base64-js": "^1.1.2" } }, "sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg=="],
|
||||||
|
|
||||||
"buffer-equal-constant-time": ["buffer-equal-constant-time@1.0.1", "", {}, "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="],
|
"buffer-equal-constant-time": ["buffer-equal-constant-time@1.0.1", "", {}, "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="],
|
||||||
|
|
||||||
"bun-types": ["bun-types@1.3.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-OlFwHcnNV99r//9v5IIOgQ9Uk37gZqrNMCcqEaExdkVq3Avwqok1bJFmvGMCkCE0FqzdY8VMOZpfpR3lwI+CsQ=="],
|
"bun-types": ["bun-types@1.3.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-OlFwHcnNV99r//9v5IIOgQ9Uk37gZqrNMCcqEaExdkVq3Avwqok1bJFmvGMCkCE0FqzdY8VMOZpfpR3lwI+CsQ=="],
|
||||||
@@ -131,6 +146,10 @@
|
|||||||
|
|
||||||
"citty": ["citty@0.1.6", "", { "dependencies": { "consola": "^3.2.3" } }, "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ=="],
|
"citty": ["citty@0.1.6", "", { "dependencies": { "consola": "^3.2.3" } }, "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ=="],
|
||||||
|
|
||||||
|
"clone": ["clone@2.1.2", "", {}, "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w=="],
|
||||||
|
|
||||||
|
"cluster-key-slot": ["cluster-key-slot@1.1.2", "", {}, "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA=="],
|
||||||
|
|
||||||
"combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="],
|
"combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="],
|
||||||
|
|
||||||
"confbox": ["confbox@0.2.2", "", {}, "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ=="],
|
"confbox": ["confbox@0.2.2", "", {}, "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ=="],
|
||||||
@@ -143,6 +162,8 @@
|
|||||||
|
|
||||||
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
|
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
|
||||||
|
|
||||||
|
"crypto-js": ["crypto-js@4.2.0", "", {}, "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q=="],
|
||||||
|
|
||||||
"csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
|
"csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
|
||||||
|
|
||||||
"cuid": ["cuid@3.0.0", "", {}, "sha512-WZYYkHdIDnaxdeP8Misq3Lah5vFjJwGuItJuV+tvMafosMzw0nF297T7mrm8IOWiPJkV6gc7sa8pzx27+w25Zg=="],
|
"cuid": ["cuid@3.0.0", "", {}, "sha512-WZYYkHdIDnaxdeP8Misq3Lah5vFjJwGuItJuV+tvMafosMzw0nF297T7mrm8IOWiPJkV6gc7sa8pzx27+w25Zg=="],
|
||||||
@@ -159,6 +180,8 @@
|
|||||||
|
|
||||||
"destr": ["destr@2.0.5", "", {}, "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA=="],
|
"destr": ["destr@2.0.5", "", {}, "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA=="],
|
||||||
|
|
||||||
|
"dfa": ["dfa@1.2.0", "", {}, "sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q=="],
|
||||||
|
|
||||||
"dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="],
|
"dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="],
|
||||||
|
|
||||||
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
|
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
|
||||||
@@ -185,8 +208,12 @@
|
|||||||
|
|
||||||
"fast-check": ["fast-check@3.23.2", "", { "dependencies": { "pure-rand": "^6.1.0" } }, "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A=="],
|
"fast-check": ["fast-check@3.23.2", "", { "dependencies": { "pure-rand": "^6.1.0" } }, "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A=="],
|
||||||
|
|
||||||
|
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
|
||||||
|
|
||||||
"follow-redirects": ["follow-redirects@1.15.11", "", {}, "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ=="],
|
"follow-redirects": ["follow-redirects@1.15.11", "", {}, "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ=="],
|
||||||
|
|
||||||
|
"fontkit": ["fontkit@2.0.4", "", { "dependencies": { "@swc/helpers": "^0.5.12", "brotli": "^1.3.2", "clone": "^2.1.2", "dfa": "^1.2.0", "fast-deep-equal": "^3.1.3", "restructure": "^3.0.0", "tiny-inflate": "^1.0.3", "unicode-properties": "^1.4.0", "unicode-trie": "^2.0.0" } }, "sha512-syetQadaUEDNdxdugga9CpEYVaQIxOwk7GlwZWWZ19//qW4zE5bknOKeMBDYAASwnpaSHKJITRLMF9m1fp3s6g=="],
|
||||||
|
|
||||||
"foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="],
|
"foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="],
|
||||||
|
|
||||||
"form-data": ["form-data@4.0.5", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w=="],
|
"form-data": ["form-data@4.0.5", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w=="],
|
||||||
@@ -223,12 +250,16 @@
|
|||||||
|
|
||||||
"iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="],
|
"iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="],
|
||||||
|
|
||||||
|
"ioredis": ["ioredis@5.10.0", "", { "dependencies": { "@ioredis/commands": "1.5.1", "cluster-key-slot": "^1.1.0", "debug": "^4.3.4", "denque": "^2.1.0", "lodash.defaults": "^4.2.0", "lodash.isarguments": "^3.1.0", "redis-errors": "^1.2.0", "redis-parser": "^3.0.0", "standard-as-callback": "^2.1.0" } }, "sha512-HVBe9OFuqs+Z6n64q09PQvP1/R4Bm+30PAyyD4wIEqssh3v9L21QjCVk4kRLucMBcDokJTcLjsGeVRlq/nH6DA=="],
|
||||||
|
|
||||||
"is-property": ["is-property@1.0.2", "", {}, "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g=="],
|
"is-property": ["is-property@1.0.2", "", {}, "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g=="],
|
||||||
|
|
||||||
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
||||||
|
|
||||||
"jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
|
"jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
|
||||||
|
|
||||||
|
"jpeg-exif": ["jpeg-exif@1.1.4", "", {}, "sha512-a+bKEcCjtuW5WTdgeXFzswSrdqi0jk4XlEtZlx5A94wCoBpFjfFTbo/Tra5SpNCl/YFZPvcV1dJc+TAYeg6ROQ=="],
|
||||||
|
|
||||||
"jsonwebtoken": ["jsonwebtoken@9.0.3", "", { "dependencies": { "jws": "^4.0.1", "lodash.includes": "^4.3.0", "lodash.isboolean": "^3.0.3", "lodash.isinteger": "^4.0.4", "lodash.isnumber": "^3.0.3", "lodash.isplainobject": "^4.0.6", "lodash.isstring": "^4.0.1", "lodash.once": "^4.0.0", "ms": "^2.1.1", "semver": "^7.5.4" } }, "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g=="],
|
"jsonwebtoken": ["jsonwebtoken@9.0.3", "", { "dependencies": { "jws": "^4.0.1", "lodash.includes": "^4.3.0", "lodash.isboolean": "^3.0.3", "lodash.isinteger": "^4.0.4", "lodash.isnumber": "^3.0.3", "lodash.isplainobject": "^4.0.6", "lodash.isstring": "^4.0.1", "lodash.once": "^4.0.0", "ms": "^2.1.1", "semver": "^7.5.4" } }, "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g=="],
|
||||||
|
|
||||||
"jwa": ["jwa@2.0.1", "", { "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg=="],
|
"jwa": ["jwa@2.0.1", "", { "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg=="],
|
||||||
@@ -239,10 +270,16 @@
|
|||||||
|
|
||||||
"lilconfig": ["lilconfig@2.1.0", "", {}, "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ=="],
|
"lilconfig": ["lilconfig@2.1.0", "", {}, "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ=="],
|
||||||
|
|
||||||
|
"linebreak": ["linebreak@1.1.0", "", { "dependencies": { "base64-js": "0.0.8", "unicode-trie": "^2.0.0" } }, "sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ=="],
|
||||||
|
|
||||||
"lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="],
|
"lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="],
|
||||||
|
|
||||||
|
"lodash.defaults": ["lodash.defaults@4.2.0", "", {}, "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ=="],
|
||||||
|
|
||||||
"lodash.includes": ["lodash.includes@4.3.0", "", {}, "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w=="],
|
"lodash.includes": ["lodash.includes@4.3.0", "", {}, "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w=="],
|
||||||
|
|
||||||
|
"lodash.isarguments": ["lodash.isarguments@3.1.0", "", {}, "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg=="],
|
||||||
|
|
||||||
"lodash.isboolean": ["lodash.isboolean@3.0.3", "", {}, "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg=="],
|
"lodash.isboolean": ["lodash.isboolean@3.0.3", "", {}, "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg=="],
|
||||||
|
|
||||||
"lodash.isinteger": ["lodash.isinteger@4.0.4", "", {}, "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA=="],
|
"lodash.isinteger": ["lodash.isinteger@4.0.4", "", {}, "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA=="],
|
||||||
@@ -281,10 +318,18 @@
|
|||||||
|
|
||||||
"ohash": ["ohash@2.0.11", "", {}, "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ=="],
|
"ohash": ["ohash@2.0.11", "", {}, "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ=="],
|
||||||
|
|
||||||
|
"pako": ["pako@1.0.11", "", {}, "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="],
|
||||||
|
|
||||||
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
|
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
|
||||||
|
|
||||||
"pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
|
"pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
|
||||||
|
|
||||||
|
"pdf-lib": ["pdf-lib@1.17.1", "", { "dependencies": { "@pdf-lib/standard-fonts": "^1.0.0", "@pdf-lib/upng": "^1.0.1", "pako": "^1.0.11", "tslib": "^1.11.1" } }, "sha512-V/mpyJAoTsN4cnP31vc0wfNA1+p20evqqnap0KLoRUN0Yk/p3wN52DOEsL4oBFcLdb76hlpKPtzJIgo67j/XLw=="],
|
||||||
|
|
||||||
|
"pdfkit": ["pdfkit@0.17.2", "", { "dependencies": { "crypto-js": "^4.2.0", "fontkit": "^2.0.4", "jpeg-exif": "^1.1.4", "linebreak": "^1.1.0", "png-js": "^1.0.0" } }, "sha512-UnwF5fXy08f0dnp4jchFYAROKMNTaPqb/xgR8GtCzIcqoTnbOqtp3bwKvO4688oHI6vzEEs8Q6vqqEnC5IUELw=="],
|
||||||
|
|
||||||
|
"pdfmake": ["pdfmake@0.3.5", "", { "dependencies": { "linebreak": "^1.1.0", "pdfkit": "^0.17.2", "xmldoc": "^2.0.3" } }, "sha512-DR7jRrK4lk7UiRT6pi+NeWhW1ToTsL2Y8CH+bFKNYz3M7agIVgeCtwARveEORhCAqoG3AUDrN318xU/lkOr1Bg=="],
|
||||||
|
|
||||||
"perfect-debounce": ["perfect-debounce@1.0.0", "", {}, "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA=="],
|
"perfect-debounce": ["perfect-debounce@1.0.0", "", {}, "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA=="],
|
||||||
|
|
||||||
"pg": ["pg@8.17.2", "", { "dependencies": { "pg-connection-string": "^2.10.1", "pg-pool": "^3.11.0", "pg-protocol": "^1.11.0", "pg-types": "2.2.0", "pgpass": "1.0.5" }, "optionalDependencies": { "pg-cloudflare": "^1.3.0" }, "peerDependencies": { "pg-native": ">=3.0.1" }, "optionalPeers": ["pg-native"] }, "sha512-vjbKdiBJRqzcYw1fNU5KuHyYvdJ1qpcQg1CeBrHFqV1pWgHeVR6j/+kX0E1AAXfyuLUGY1ICrN2ELKA/z2HWzw=="],
|
"pg": ["pg@8.17.2", "", { "dependencies": { "pg-connection-string": "^2.10.1", "pg-pool": "^3.11.0", "pg-protocol": "^1.11.0", "pg-types": "2.2.0", "pgpass": "1.0.5" }, "optionalDependencies": { "pg-cloudflare": "^1.3.0" }, "peerDependencies": { "pg-native": ">=3.0.1" }, "optionalPeers": ["pg-native"] }, "sha512-vjbKdiBJRqzcYw1fNU5KuHyYvdJ1qpcQg1CeBrHFqV1pWgHeVR6j/+kX0E1AAXfyuLUGY1ICrN2ELKA/z2HWzw=="],
|
||||||
@@ -305,6 +350,8 @@
|
|||||||
|
|
||||||
"pkg-types": ["pkg-types@2.3.0", "", { "dependencies": { "confbox": "^0.2.2", "exsolve": "^1.0.7", "pathe": "^2.0.3" } }, "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig=="],
|
"pkg-types": ["pkg-types@2.3.0", "", { "dependencies": { "confbox": "^0.2.2", "exsolve": "^1.0.7", "pathe": "^2.0.3" } }, "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig=="],
|
||||||
|
|
||||||
|
"png-js": ["png-js@1.0.0", "", {}, "sha512-k+YsbhpA9e+EFfKjTCH3VW6aoKlyNYI6NYdTfDL4CIvFnvsuO84ttonmZE7rc+v23SLTH8XX+5w/Ak9v0xGY4g=="],
|
||||||
|
|
||||||
"postgres": ["postgres@3.4.7", "", {}, "sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw=="],
|
"postgres": ["postgres@3.4.7", "", {}, "sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw=="],
|
||||||
|
|
||||||
"postgres-array": ["postgres-array@3.0.4", "", {}, "sha512-nAUSGfSDGOaOAEGwqsRY27GPOea7CNipJPOA7lPbdEpx5Kg3qzdP0AaWC5MlhTWV9s4hFX39nomVZ+C4tnGOJQ=="],
|
"postgres-array": ["postgres-array@3.0.4", "", {}, "sha512-nAUSGfSDGOaOAEGwqsRY27GPOea7CNipJPOA7lPbdEpx5Kg3qzdP0AaWC5MlhTWV9s4hFX39nomVZ+C4tnGOJQ=="],
|
||||||
@@ -331,16 +378,24 @@
|
|||||||
|
|
||||||
"readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
|
"readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
|
||||||
|
|
||||||
|
"redis-errors": ["redis-errors@1.2.0", "", {}, "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w=="],
|
||||||
|
|
||||||
|
"redis-parser": ["redis-parser@3.0.0", "", { "dependencies": { "redis-errors": "^1.0.0" } }, "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A=="],
|
||||||
|
|
||||||
"regexp-to-ast": ["regexp-to-ast@0.5.0", "", {}, "sha512-tlbJqcMHnPKI9zSrystikWKwHkBqu2a/Sgw01h3zFjvYrMxEDYHzzoMZnUrbIfpTFEsoRnnviOXNCzFiSc54Qw=="],
|
"regexp-to-ast": ["regexp-to-ast@0.5.0", "", {}, "sha512-tlbJqcMHnPKI9zSrystikWKwHkBqu2a/Sgw01h3zFjvYrMxEDYHzzoMZnUrbIfpTFEsoRnnviOXNCzFiSc54Qw=="],
|
||||||
|
|
||||||
"remeda": ["remeda@2.33.4", "", {}, "sha512-ygHswjlc/opg2VrtiYvUOPLjxjtdKvjGz1/plDhkG66hjNjFr1xmfrs2ClNFo/E6TyUFiwYNh53bKV26oBoMGQ=="],
|
"remeda": ["remeda@2.33.4", "", {}, "sha512-ygHswjlc/opg2VrtiYvUOPLjxjtdKvjGz1/plDhkG66hjNjFr1xmfrs2ClNFo/E6TyUFiwYNh53bKV26oBoMGQ=="],
|
||||||
|
|
||||||
|
"restructure": ["restructure@3.0.2", "", {}, "sha512-gSfoiOEA0VPE6Tukkrr7I0RBdE0s7H1eFCDBk05l1KIQT1UIKNc5JZy6jdyW6eYH3aR3g5b3PuL77rq0hvwtAw=="],
|
||||||
|
|
||||||
"retry": ["retry@0.12.0", "", {}, "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow=="],
|
"retry": ["retry@0.12.0", "", {}, "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow=="],
|
||||||
|
|
||||||
"safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
|
"safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
|
||||||
|
|
||||||
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
|
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
|
||||||
|
|
||||||
|
"sax": ["sax@1.5.0", "", {}, "sha512-21IYA3Q5cQf089Z6tgaUTr7lDAyzoTPx5HRtbhsME8Udispad8dC/+sziTNugOEx54ilvatQ9YCzl4KQLPcRHA=="],
|
||||||
|
|
||||||
"scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
|
"scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
|
||||||
|
|
||||||
"semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
|
"semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
|
||||||
@@ -363,14 +418,24 @@
|
|||||||
|
|
||||||
"sqlstring": ["sqlstring@2.3.3", "", {}, "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg=="],
|
"sqlstring": ["sqlstring@2.3.3", "", {}, "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg=="],
|
||||||
|
|
||||||
|
"standard-as-callback": ["standard-as-callback@2.1.0", "", {}, "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A=="],
|
||||||
|
|
||||||
"std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="],
|
"std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="],
|
||||||
|
|
||||||
|
"tiny-inflate": ["tiny-inflate@1.0.3", "", {}, "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw=="],
|
||||||
|
|
||||||
"tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="],
|
"tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="],
|
||||||
|
|
||||||
|
"tslib": ["tslib@1.14.1", "", {}, "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="],
|
||||||
|
|
||||||
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||||
|
|
||||||
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
|
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
|
||||||
|
|
||||||
|
"unicode-properties": ["unicode-properties@1.4.1", "", { "dependencies": { "base64-js": "^1.3.0", "unicode-trie": "^2.0.0" } }, "sha512-CLjCCLQ6UuMxWnbIylkisbRj31qxHPAurvena/0iwSVbQ2G1VY5/HjV0IRabOEbDHlzZlRdCrD4NhB0JtU40Pg=="],
|
||||||
|
|
||||||
|
"unicode-trie": ["unicode-trie@2.0.0", "", { "dependencies": { "pako": "^0.2.5", "tiny-inflate": "^1.0.0" } }, "sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ=="],
|
||||||
|
|
||||||
"uuid": ["uuid@8.3.2", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="],
|
"uuid": ["uuid@8.3.2", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="],
|
||||||
|
|
||||||
"valibot": ["valibot@1.2.0", "", { "peerDependencies": { "typescript": ">=5" }, "optionalPeers": ["typescript"] }, "sha512-mm1rxUsmOxzrwnX5arGS+U4T25RdvpPjPN4yR0u9pUBov9+zGVtO84tif1eY4r6zWxVxu3KzIyknJy3rxfRZZg=="],
|
"valibot": ["valibot@1.2.0", "", { "peerDependencies": { "typescript": ">=5" }, "optionalPeers": ["typescript"] }, "sha512-mm1rxUsmOxzrwnX5arGS+U4T25RdvpPjPN4yR0u9pUBov9+zGVtO84tif1eY4r6zWxVxu3KzIyknJy3rxfRZZg=="],
|
||||||
@@ -381,6 +446,8 @@
|
|||||||
|
|
||||||
"ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="],
|
"ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="],
|
||||||
|
|
||||||
|
"xmldoc": ["xmldoc@2.0.3", "", { "dependencies": { "sax": "^1.4.3" } }, "sha512-6gRk4NY/Jvg67xn7OzJuxLRsGgiXBaPUQplVJ/9l99uIugxh4FTOewYz5ic8WScj7Xx/2WvhENiQKwkK9RpE4w=="],
|
||||||
|
|
||||||
"xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="],
|
"xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="],
|
||||||
|
|
||||||
"zeptomatch": ["zeptomatch@2.1.0", "", { "dependencies": { "grammex": "^3.1.11", "graphmatch": "^1.1.0" } }, "sha512-KiGErG2J0G82LSpniV0CtIzjlJ10E04j02VOudJsPyPwNZgGnRKQy7I1R7GMyg/QswnE4l7ohSGrQbQbjXPPDA=="],
|
"zeptomatch": ["zeptomatch@2.1.0", "", { "dependencies": { "grammex": "^3.1.11", "graphmatch": "^1.1.0" } }, "sha512-KiGErG2J0G82LSpniV0CtIzjlJ10E04j02VOudJsPyPwNZgGnRKQy7I1R7GMyg/QswnE4l7ohSGrQbQbjXPPDA=="],
|
||||||
@@ -397,10 +464,18 @@
|
|||||||
|
|
||||||
"@prisma/get-platform/@prisma/debug": ["@prisma/debug@7.2.0", "", {}, "sha512-YSGTiSlBAVJPzX4ONZmMotL+ozJwQjRmZweQNIq/ER0tQJKJynNkRB3kyvt37eOfsbMCXk3gnLF6J9OJ4QWftw=="],
|
"@prisma/get-platform/@prisma/debug": ["@prisma/debug@7.2.0", "", {}, "sha512-YSGTiSlBAVJPzX4ONZmMotL+ozJwQjRmZweQNIq/ER0tQJKJynNkRB3kyvt37eOfsbMCXk3gnLF6J9OJ4QWftw=="],
|
||||||
|
|
||||||
|
"@swc/helpers/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||||
|
|
||||||
|
"brotli/base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
|
||||||
|
|
||||||
"nypm/citty": ["citty@0.2.0", "", {}, "sha512-8csy5IBFI2ex2hTVpaHN2j+LNE199AgiI7y4dMintrr8i0lQiFn+0AWMZrWdHKIgMOer65f8IThysYhoReqjWA=="],
|
"nypm/citty": ["citty@0.2.0", "", {}, "sha512-8csy5IBFI2ex2hTVpaHN2j+LNE199AgiI7y4dMintrr8i0lQiFn+0AWMZrWdHKIgMOer65f8IThysYhoReqjWA=="],
|
||||||
|
|
||||||
"pg-types/postgres-array": ["postgres-array@2.0.0", "", {}, "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA=="],
|
"pg-types/postgres-array": ["postgres-array@2.0.0", "", {}, "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA=="],
|
||||||
|
|
||||||
"proper-lockfile/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="],
|
"proper-lockfile/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="],
|
||||||
|
|
||||||
|
"unicode-properties/base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
|
||||||
|
|
||||||
|
"unicode-trie/pako": ["pako@0.2.9", "", {}, "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA=="],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
[test]
|
||||||
|
preload = ["./tests/setup.ts"]
|
||||||
@@ -0,0 +1,307 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Analyze ConnectWise API call logs.
|
||||||
|
|
||||||
|
Looks for the most recent log file in cw-api-logs/ by default,
|
||||||
|
or accepts an explicit path as an argument.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python3 analyze-cw-calls.py # latest file in cw-api-logs/
|
||||||
|
python3 analyze-cw-calls.py cw-api-logs/specific.jsonl
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import glob
|
||||||
|
import statistics
|
||||||
|
from collections import Counter, defaultdict
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
# ── Colours ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
RED = "\033[91m"
|
||||||
|
GREEN = "\033[92m"
|
||||||
|
YELLOW = "\033[93m"
|
||||||
|
CYAN = "\033[96m"
|
||||||
|
BOLD = "\033[1m"
|
||||||
|
DIM = "\033[2m"
|
||||||
|
RESET = "\033[0m"
|
||||||
|
|
||||||
|
def colour_duration(ms: float) -> str:
|
||||||
|
if ms >= 10_000:
|
||||||
|
return f"{RED}{ms:,.0f}ms{RESET}"
|
||||||
|
if ms >= 5_000:
|
||||||
|
return f"{YELLOW}{ms:,.0f}ms{RESET}"
|
||||||
|
return f"{GREEN}{ms:,.0f}ms{RESET}"
|
||||||
|
|
||||||
|
def header(title: str) -> str:
|
||||||
|
return f"\n{BOLD}{CYAN}{'─' * 60}\n {title}\n{'─' * 60}{RESET}"
|
||||||
|
|
||||||
|
# ── Resolve log file ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def find_latest_log() -> str:
|
||||||
|
"""Find the most recent .jsonl file in cw-api-logs/."""
|
||||||
|
log_dir = os.path.join(os.getcwd(), "cw-api-logs")
|
||||||
|
files = sorted(glob.glob(os.path.join(log_dir, "*.jsonl")))
|
||||||
|
if not files:
|
||||||
|
print(f"{RED}No log files found in cw-api-logs/{RESET}")
|
||||||
|
print(f"Run {BOLD}bun run dev:log{RESET} to start logging CW API calls.")
|
||||||
|
sys.exit(1)
|
||||||
|
return files[-1]
|
||||||
|
|
||||||
|
if len(sys.argv) > 1:
|
||||||
|
log_path = sys.argv[1]
|
||||||
|
else:
|
||||||
|
log_path = find_latest_log()
|
||||||
|
|
||||||
|
print(f"{DIM}Reading: {log_path}{RESET}")
|
||||||
|
|
||||||
|
entries = []
|
||||||
|
parse_errors = 0
|
||||||
|
with open(log_path) as f:
|
||||||
|
for line in f:
|
||||||
|
line = line.strip()
|
||||||
|
if not line:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
entries.append(json.loads(line))
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
parse_errors += 1
|
||||||
|
|
||||||
|
if not entries:
|
||||||
|
print("No entries found. Check the log file path.")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# ── Derived fields ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
durations = [e["durationMs"] for e in entries]
|
||||||
|
errors = [e for e in entries if e.get("error")]
|
||||||
|
successes = [e for e in entries if not e.get("error")]
|
||||||
|
timestamps = [datetime.fromisoformat(e["timestamp"].replace("Z", "+00:00")) for e in entries]
|
||||||
|
|
||||||
|
time_span = (timestamps[-1] - timestamps[0]) if len(timestamps) > 1 else timedelta(0)
|
||||||
|
|
||||||
|
# Normalise the URL to a route pattern for grouping
|
||||||
|
def normalise_url(url: str) -> str:
|
||||||
|
parts = url.split("?")[0].rstrip("/").split("/")
|
||||||
|
normalised = []
|
||||||
|
for p in parts:
|
||||||
|
if p.isdigit():
|
||||||
|
normalised.append(":id")
|
||||||
|
else:
|
||||||
|
normalised.append(p)
|
||||||
|
return "/".join(normalised)
|
||||||
|
|
||||||
|
# ── 1. Overview ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
print(header("OVERVIEW"))
|
||||||
|
print(f" Log file : {log_path}")
|
||||||
|
print(f" Total calls : {BOLD}{len(entries):,}{RESET}")
|
||||||
|
print(f" Successes : {GREEN}{len(successes):,}{RESET}")
|
||||||
|
print(f" Failures : {RED}{len(errors):,}{RESET} ({len(errors)/len(entries)*100:.1f}%)")
|
||||||
|
print(f" Time span : {time_span}")
|
||||||
|
if time_span.total_seconds() > 0:
|
||||||
|
rps = len(entries) / time_span.total_seconds()
|
||||||
|
print(f" Avg req/sec : {rps:.2f}")
|
||||||
|
if parse_errors:
|
||||||
|
print(f" Parse errors : {YELLOW}{parse_errors}{RESET}")
|
||||||
|
|
||||||
|
# ── 2. Duration stats ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
print(header("DURATION STATS (all calls)"))
|
||||||
|
sorted_dur = sorted(durations)
|
||||||
|
p50 = sorted_dur[len(sorted_dur) * 50 // 100]
|
||||||
|
p90 = sorted_dur[len(sorted_dur) * 90 // 100]
|
||||||
|
p95 = sorted_dur[len(sorted_dur) * 95 // 100]
|
||||||
|
p99 = sorted_dur[len(sorted_dur) * 99 // 100]
|
||||||
|
|
||||||
|
print(f" Min : {colour_duration(min(durations))}")
|
||||||
|
print(f" Max : {colour_duration(max(durations))}")
|
||||||
|
print(f" Mean : {colour_duration(statistics.mean(durations))}")
|
||||||
|
print(f" Median (p50) : {colour_duration(p50)}")
|
||||||
|
print(f" p90 : {colour_duration(p90)}")
|
||||||
|
print(f" p95 : {colour_duration(p95)}")
|
||||||
|
print(f" p99 : {colour_duration(p99)}")
|
||||||
|
print(f" Std dev : {statistics.stdev(durations):,.0f}ms" if len(durations) > 1 else "")
|
||||||
|
|
||||||
|
# Duration buckets
|
||||||
|
buckets = {"<500ms": 0, "500ms-1s": 0, "1-3s": 0, "3-5s": 0, "5-10s": 0, "10-20s": 0, "20s+": 0}
|
||||||
|
for d in durations:
|
||||||
|
if d < 500: buckets["<500ms"] += 1
|
||||||
|
elif d < 1000: buckets["500ms-1s"] += 1
|
||||||
|
elif d < 3000: buckets["1-3s"] += 1
|
||||||
|
elif d < 5000: buckets["3-5s"] += 1
|
||||||
|
elif d < 10000: buckets["5-10s"] += 1
|
||||||
|
elif d < 20000: buckets["10-20s"] += 1
|
||||||
|
else: buckets["20s+"] += 1
|
||||||
|
|
||||||
|
print(f"\n {BOLD}Distribution:{RESET}")
|
||||||
|
max_bar = 40
|
||||||
|
max_count = max(buckets.values()) if buckets else 1
|
||||||
|
for label, count in buckets.items():
|
||||||
|
bar_len = int(count / max_count * max_bar) if max_count else 0
|
||||||
|
pct = count / len(durations) * 100
|
||||||
|
bar = "█" * bar_len
|
||||||
|
clr = GREEN if "500" in label or "<" in label else (YELLOW if "1-3" in label or "3-5" in label else RED)
|
||||||
|
print(f" {label:>10s} {clr}{bar}{RESET} {count:>5,} ({pct:5.1f}%)")
|
||||||
|
|
||||||
|
# ── 3. Errors breakdown ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
print(header("ERROR BREAKDOWN"))
|
||||||
|
if not errors:
|
||||||
|
print(f" {GREEN}No errors! 🎉{RESET}")
|
||||||
|
else:
|
||||||
|
error_codes = Counter()
|
||||||
|
for e in errors:
|
||||||
|
err_str = e.get("error", "unknown")
|
||||||
|
code = err_str.split(":")[0] if ":" in err_str else err_str
|
||||||
|
error_codes[code] += 1
|
||||||
|
|
||||||
|
for code, count in error_codes.most_common():
|
||||||
|
print(f" {RED}{code:<30s}{RESET} {count:>5,} ({count/len(entries)*100:.1f}%)")
|
||||||
|
|
||||||
|
# Errored URLs
|
||||||
|
errored_urls = Counter(normalise_url(e["url"]) for e in errors)
|
||||||
|
print(f"\n {BOLD}Top errored endpoints:{RESET}")
|
||||||
|
for url, count in errored_urls.most_common(10):
|
||||||
|
print(f" {count:>5,} {url}")
|
||||||
|
|
||||||
|
# ── 4. Slowest individual calls ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
print(header("TOP 20 SLOWEST CALLS"))
|
||||||
|
slowest = sorted(entries, key=lambda e: e["durationMs"], reverse=True)[:20]
|
||||||
|
for i, e in enumerate(slowest, 1):
|
||||||
|
status = e.get("status") or f"{RED}ERR{RESET}"
|
||||||
|
err_tag = f" {RED}[{e['error'].split(':')[0]}]{RESET}" if e.get("error") else ""
|
||||||
|
print(f" {i:>2}. {colour_duration(e['durationMs']):>20s} {e['method']:>4s} {e['url'][:60]:<60s} {DIM}{status}{RESET}{err_tag}")
|
||||||
|
|
||||||
|
# ── 5. Per-endpoint stats ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
print(header("PER-ENDPOINT STATS (by route pattern)"))
|
||||||
|
by_route = defaultdict(list)
|
||||||
|
for e in entries:
|
||||||
|
route = normalise_url(e["url"])
|
||||||
|
by_route[route].append(e)
|
||||||
|
|
||||||
|
# Sort by total time spent descending (most impactful)
|
||||||
|
route_stats = []
|
||||||
|
for route, calls in by_route.items():
|
||||||
|
durs = [c["durationMs"] for c in calls]
|
||||||
|
errs = sum(1 for c in calls if c.get("error"))
|
||||||
|
sorted_d = sorted(durs)
|
||||||
|
route_stats.append({
|
||||||
|
"route": route,
|
||||||
|
"count": len(calls),
|
||||||
|
"errors": errs,
|
||||||
|
"total_ms": sum(durs),
|
||||||
|
"mean": statistics.mean(durs),
|
||||||
|
"p50": sorted_d[len(sorted_d) * 50 // 100],
|
||||||
|
"p95": sorted_d[len(sorted_d) * 95 // 100],
|
||||||
|
"max": max(durs),
|
||||||
|
})
|
||||||
|
|
||||||
|
route_stats.sort(key=lambda r: r["total_ms"], reverse=True)
|
||||||
|
|
||||||
|
print(f" {'Route':<55s} {'Count':>6s} {'Errs':>5s} {'Mean':>8s} {'p50':>8s} {'p95':>8s} {'Max':>8s} {'Total':>10s}")
|
||||||
|
print(f" {'─' * 55} {'─' * 6} {'─' * 5} {'─' * 8} {'─' * 8} {'─' * 8} {'─' * 8} {'─' * 10}")
|
||||||
|
for r in route_stats[:25]:
|
||||||
|
err_str = f"{RED}{r['errors']}{RESET}" if r['errors'] else f"{DIM}0{RESET}"
|
||||||
|
print(
|
||||||
|
f" {r['route']:<55s} {r['count']:>6,} {err_str:>14s} "
|
||||||
|
f"{r['mean']:>7,.0f}ms {r['p50']:>7,.0f}ms {r['p95']:>7,.0f}ms "
|
||||||
|
f"{r['max']:>7,.0f}ms {r['total_ms']/1000:>8,.1f}s"
|
||||||
|
)
|
||||||
|
|
||||||
|
# ── 6. HTTP method breakdown ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
print(header("BY HTTP METHOD"))
|
||||||
|
by_method = defaultdict(list)
|
||||||
|
for e in entries:
|
||||||
|
by_method[e["method"]].append(e["durationMs"])
|
||||||
|
|
||||||
|
print(f" {'Method':<8s} {'Count':>7s} {'Mean':>9s} {'p95':>9s} {'Max':>9s}")
|
||||||
|
print(f" {'─' * 8} {'─' * 7} {'─' * 9} {'─' * 9} {'─' * 9}")
|
||||||
|
for method in sorted(by_method.keys()):
|
||||||
|
durs = by_method[method]
|
||||||
|
sd = sorted(durs)
|
||||||
|
print(
|
||||||
|
f" {method:<8s} {len(durs):>7,} "
|
||||||
|
f"{statistics.mean(durs):>8,.0f}ms "
|
||||||
|
f"{sd[len(sd)*95//100]:>8,.0f}ms "
|
||||||
|
f"{max(durs):>8,.0f}ms"
|
||||||
|
)
|
||||||
|
|
||||||
|
# ── 7. Timeline (calls per minute) ──────────────────────────────────────────
|
||||||
|
|
||||||
|
if time_span.total_seconds() > 60:
|
||||||
|
print(header("TIMELINE (per-minute throughput & errors)"))
|
||||||
|
by_minute = defaultdict(lambda: {"count": 0, "errors": 0, "dur_sum": 0})
|
||||||
|
for e in entries:
|
||||||
|
ts = e["timestamp"][:16] # YYYY-MM-DDTHH:MM
|
||||||
|
by_minute[ts]["count"] += 1
|
||||||
|
by_minute[ts]["dur_sum"] += e["durationMs"]
|
||||||
|
if e.get("error"):
|
||||||
|
by_minute[ts]["errors"] += 1
|
||||||
|
|
||||||
|
for minute in sorted(by_minute.keys()):
|
||||||
|
m = by_minute[minute]
|
||||||
|
avg = m["dur_sum"] / m["count"] if m["count"] else 0
|
||||||
|
err_part = f" {RED}({m['errors']} errs){RESET}" if m["errors"] else ""
|
||||||
|
bar = "▓" * min(m["count"] // 5, 50)
|
||||||
|
avg_clr = colour_duration(avg)
|
||||||
|
print(f" {minute} {m['count']:>5,} reqs avg {avg_clr:>20s} {bar}{err_part}")
|
||||||
|
|
||||||
|
# ── 8. Concurrency hotspots ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
print(header("CONCURRENCY HOTSPOTS (calls starting within 100ms of each other)"))
|
||||||
|
ts_ms = [int(t.timestamp() * 1000) for t in timestamps]
|
||||||
|
bursts = []
|
||||||
|
i = 0
|
||||||
|
while i < len(ts_ms):
|
||||||
|
j = i
|
||||||
|
while j < len(ts_ms) - 1 and ts_ms[j + 1] - ts_ms[j] < 100:
|
||||||
|
j += 1
|
||||||
|
burst_size = j - i + 1
|
||||||
|
if burst_size >= 5:
|
||||||
|
burst_entries = entries[i:j + 1]
|
||||||
|
avg_dur = statistics.mean(e["durationMs"] for e in burst_entries)
|
||||||
|
bursts.append((burst_size, entries[i]["timestamp"], avg_dur, burst_entries))
|
||||||
|
i = j + 1
|
||||||
|
|
||||||
|
bursts.sort(key=lambda b: b[0], reverse=True)
|
||||||
|
if bursts:
|
||||||
|
print(f" Found {len(bursts)} burst(s) of ≥5 concurrent requests\n")
|
||||||
|
for size, ts, avg, _ in bursts[:10]:
|
||||||
|
print(f" {YELLOW}{size:>3} concurrent{RESET} at {ts} avg {colour_duration(avg)}")
|
||||||
|
else:
|
||||||
|
print(f" {GREEN}No major concurrency bursts detected.{RESET}")
|
||||||
|
|
||||||
|
# ── 9. Summary / recommendations ────────────────────────────────────────────
|
||||||
|
|
||||||
|
print(header("SUMMARY"))
|
||||||
|
err_rate = len(errors) / len(entries) * 100
|
||||||
|
slow_5s = sum(1 for d in durations if d >= 5000)
|
||||||
|
slow_pct = slow_5s / len(entries) * 100
|
||||||
|
|
||||||
|
if err_rate > 5:
|
||||||
|
print(f" {RED}⚠ Error rate is {err_rate:.1f}% — CW API is struggling{RESET}")
|
||||||
|
elif err_rate > 1:
|
||||||
|
print(f" {YELLOW}⚠ Error rate is {err_rate:.1f}% — some instability{RESET}")
|
||||||
|
else:
|
||||||
|
print(f" {GREEN}✓ Error rate is {err_rate:.1f}% — acceptable{RESET}")
|
||||||
|
|
||||||
|
if slow_pct > 10:
|
||||||
|
print(f" {RED}⚠ {slow_5s:,} calls ({slow_pct:.1f}%) took >5s — CW is slow or rate-limiting{RESET}")
|
||||||
|
elif slow_pct > 2:
|
||||||
|
print(f" {YELLOW}⚠ {slow_5s:,} calls ({slow_pct:.1f}%) took >5s{RESET}")
|
||||||
|
else:
|
||||||
|
print(f" {GREEN}✓ Only {slow_5s:,} calls ({slow_pct:.1f}%) over 5s{RESET}")
|
||||||
|
|
||||||
|
if bursts:
|
||||||
|
max_burst = max(b[0] for b in bursts)
|
||||||
|
print(f" {YELLOW}⚠ Max concurrency burst: {max_burst} simultaneous requests — consider lowering CONCURRENCY{RESET}")
|
||||||
|
|
||||||
|
total_time_s = sum(durations) / 1000
|
||||||
|
print(f"\n Total wall-clock time spent waiting on CW: {BOLD}{total_time_s:,.1f}s{RESET} ({total_time_s/60:,.1f} min)")
|
||||||
|
print()
|
||||||
@@ -0,0 +1,441 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
from collections import Counter, defaultdict
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
|
def parse_iso(value: str | None) -> datetime | None:
|
||||||
|
if not value:
|
||||||
|
return None
|
||||||
|
normalized = value.replace("Z", "+00:00")
|
||||||
|
try:
|
||||||
|
return datetime.fromisoformat(normalized)
|
||||||
|
except ValueError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def first_non_empty(*values: Any) -> str:
|
||||||
|
for value in values:
|
||||||
|
if value is None:
|
||||||
|
continue
|
||||||
|
if isinstance(value, str) and value.strip() == "":
|
||||||
|
continue
|
||||||
|
return str(value)
|
||||||
|
return "unknown"
|
||||||
|
|
||||||
|
|
||||||
|
def top_lines(counter: Counter[str], limit: int) -> list[str]:
|
||||||
|
return [f"{k}: {v}" for k, v in counter.most_common(limit)]
|
||||||
|
|
||||||
|
|
||||||
|
def fmt_pct(part: int, total: int) -> str:
|
||||||
|
if total == 0:
|
||||||
|
return "0.0%"
|
||||||
|
return f"{(part / total) * 100:.1f}%"
|
||||||
|
|
||||||
|
|
||||||
|
def human_duration(start: datetime | None, end: datetime | None) -> str:
|
||||||
|
if start is None or end is None:
|
||||||
|
return "unknown"
|
||||||
|
|
||||||
|
delta = end - start
|
||||||
|
total_seconds = int(delta.total_seconds())
|
||||||
|
hours, remainder = divmod(total_seconds, 3600)
|
||||||
|
minutes, seconds = divmod(remainder, 60)
|
||||||
|
return f"{hours}h {minutes}m {seconds}s"
|
||||||
|
|
||||||
|
|
||||||
|
def truncate(value: str, max_len: int = 90) -> str:
|
||||||
|
if len(value) <= max_len:
|
||||||
|
return value
|
||||||
|
return value[: max_len - 1] + "…"
|
||||||
|
|
||||||
|
|
||||||
|
def add_section(lines: list[str], title: str) -> None:
|
||||||
|
lines.append("")
|
||||||
|
lines.append(title)
|
||||||
|
lines.append("-" * len(title))
|
||||||
|
|
||||||
|
|
||||||
|
def supports_color(enabled: bool) -> bool:
|
||||||
|
return enabled
|
||||||
|
|
||||||
|
|
||||||
|
def paint(text: str, code: str, use_color: bool) -> str:
|
||||||
|
if not use_color:
|
||||||
|
return text
|
||||||
|
return f"\033[{code}m{text}\033[0m"
|
||||||
|
|
||||||
|
|
||||||
|
def good_bad_neutral(value: str, state: str, use_color: bool) -> str:
|
||||||
|
if state == "good":
|
||||||
|
return paint(value, "32", use_color)
|
||||||
|
if state == "bad":
|
||||||
|
return paint(value, "31", use_color)
|
||||||
|
return paint(value, "36", use_color)
|
||||||
|
|
||||||
|
|
||||||
|
def add_ranked_counter(
|
||||||
|
lines: list[str],
|
||||||
|
title: str,
|
||||||
|
counter: Counter[str],
|
||||||
|
top_n: int,
|
||||||
|
total: int,
|
||||||
|
truncate_labels: bool = False,
|
||||||
|
) -> None:
|
||||||
|
lines.append(f"• {title}")
|
||||||
|
items = counter.most_common(top_n)
|
||||||
|
if not items:
|
||||||
|
lines.append(" (no data)")
|
||||||
|
return
|
||||||
|
|
||||||
|
for index, (key, count) in enumerate(items, start=1):
|
||||||
|
label = truncate(key) if truncate_labels else key
|
||||||
|
lines.append(f" {index:>2}. {label:<90} {count:>4} {fmt_pct(count, total):>6}")
|
||||||
|
|
||||||
|
|
||||||
|
def stream_row_summary(row: dict[str, Any], use_color: bool, max_path: int) -> str:
|
||||||
|
request = row.get("request") or {}
|
||||||
|
response = row.get("response") or {}
|
||||||
|
body_parsed = request.get("bodyParsed") or {}
|
||||||
|
entity_parsed = request.get("entityParsed") or {}
|
||||||
|
summary = request.get("summary") or {}
|
||||||
|
|
||||||
|
timestamp = parse_iso(row.get("timestamp"))
|
||||||
|
time_label = timestamp.astimezone(timezone.utc).strftime("%H:%M:%S") if timestamp else "--:--:--"
|
||||||
|
|
||||||
|
method = first_non_empty(request.get("method"))
|
||||||
|
path = first_non_empty(request.get("path"))
|
||||||
|
endpoint = path.split("?", 1)[0]
|
||||||
|
status_code = first_non_empty(response.get("status"))
|
||||||
|
|
||||||
|
event_type = first_non_empty(body_parsed.get("Type"), summary.get("type"))
|
||||||
|
action = first_non_empty(
|
||||||
|
body_parsed.get("Action"),
|
||||||
|
summary.get("action"),
|
||||||
|
request.get("query", {}).get("params", {}).get("action"),
|
||||||
|
)
|
||||||
|
item_id = first_non_empty(body_parsed.get("ID"), summary.get("id"), request.get("query", {}).get("inferredId"))
|
||||||
|
actor = first_non_empty(
|
||||||
|
request.get("query", {}).get("params", {}).get("memberId"),
|
||||||
|
summary.get("entityUpdatedBy"),
|
||||||
|
entity_parsed.get("UpdatedBy"),
|
||||||
|
)
|
||||||
|
entity_status = first_non_empty(entity_parsed.get("StatusName"), summary.get("entityStatus"))
|
||||||
|
|
||||||
|
endpoint_label = truncate(endpoint, max_path)
|
||||||
|
status_state = "good" if status_code.startswith("2") else "bad"
|
||||||
|
status_colored = good_bad_neutral(status_code, status_state, use_color)
|
||||||
|
event_colored = paint(f"{event_type}.{action}", "36", use_color)
|
||||||
|
endpoint_colored = paint(endpoint_label, "94", use_color)
|
||||||
|
|
||||||
|
return (
|
||||||
|
f"[{time_label}] {method:<4} {endpoint_colored:<20} "
|
||||||
|
f"{status_colored:>3} {event_colored:<22} "
|
||||||
|
f"id={item_id:<7} actor={truncate(actor, 16):<16} status={truncate(entity_status, 22)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def endpoint_stream_summary(log_path: Path, use_color: bool, max_path: int) -> str:
|
||||||
|
lines: list[str] = []
|
||||||
|
lines.append(paint("ENDPOINT STREAM (chronological)", "1;95", use_color))
|
||||||
|
lines.append(paint("────────────────────────────────────────────────────────────────────────────────────────────", "90", use_color))
|
||||||
|
|
||||||
|
count = 0
|
||||||
|
invalid = 0
|
||||||
|
with log_path.open("r", encoding="utf-8") as handle:
|
||||||
|
for raw_line in handle:
|
||||||
|
line = raw_line.strip()
|
||||||
|
if not line:
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
row = json.loads(line)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
invalid += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
lines.append(stream_row_summary(row, use_color=use_color, max_path=max_path))
|
||||||
|
count += 1
|
||||||
|
|
||||||
|
lines.append(paint("────────────────────────────────────────────────────────────────────────────────────────────", "90", use_color))
|
||||||
|
lines.append(
|
||||||
|
f"events={count} invalid={good_bad_neutral(str(invalid), 'good' if invalid == 0 else 'bad', use_color)}"
|
||||||
|
)
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class LogStats:
|
||||||
|
total_rows: int = 0
|
||||||
|
parsed_rows: int = 0
|
||||||
|
invalid_rows: int = 0
|
||||||
|
earliest: datetime | None = None
|
||||||
|
latest: datetime | None = None
|
||||||
|
|
||||||
|
methods: Counter[str] = None # type: ignore[assignment]
|
||||||
|
paths: Counter[str] = None # type: ignore[assignment]
|
||||||
|
endpoint_roots: Counter[str] = None # type: ignore[assignment]
|
||||||
|
response_statuses: Counter[str] = None # type: ignore[assignment]
|
||||||
|
event_types: Counter[str] = None # type: ignore[assignment]
|
||||||
|
actions: Counter[str] = None # type: ignore[assignment]
|
||||||
|
type_action_combo: Counter[str] = None # type: ignore[assignment]
|
||||||
|
company_ids: Counter[str] = None # type: ignore[assignment]
|
||||||
|
|
||||||
|
source_members: Counter[str] = None # type: ignore[assignment]
|
||||||
|
actor_members: Counter[str] = None # type: ignore[assignment]
|
||||||
|
entity_updated_by: Counter[str] = None # type: ignore[assignment]
|
||||||
|
|
||||||
|
requests_by_hour: Counter[str] = None # type: ignore[assignment]
|
||||||
|
requests_by_minute: Counter[str] = None # type: ignore[assignment]
|
||||||
|
endpoint_by_hour: dict[str, Counter[str]] = None # type: ignore[assignment]
|
||||||
|
|
||||||
|
def __post_init__(self) -> None:
|
||||||
|
self.methods = Counter()
|
||||||
|
self.paths = Counter()
|
||||||
|
self.endpoint_roots = Counter()
|
||||||
|
self.response_statuses = Counter()
|
||||||
|
self.event_types = Counter()
|
||||||
|
self.actions = Counter()
|
||||||
|
self.type_action_combo = Counter()
|
||||||
|
self.company_ids = Counter()
|
||||||
|
|
||||||
|
self.source_members = Counter()
|
||||||
|
self.actor_members = Counter()
|
||||||
|
self.entity_updated_by = Counter()
|
||||||
|
|
||||||
|
self.requests_by_hour = Counter()
|
||||||
|
self.requests_by_minute = Counter()
|
||||||
|
self.endpoint_by_hour = defaultdict(Counter)
|
||||||
|
|
||||||
|
def add_timestamp(self, timestamp: datetime | None) -> None:
|
||||||
|
if timestamp is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.earliest = timestamp if self.earliest is None else min(self.earliest, timestamp)
|
||||||
|
self.latest = timestamp if self.latest is None else max(self.latest, timestamp)
|
||||||
|
|
||||||
|
hour_bucket = timestamp.astimezone(timezone.utc).strftime("%Y-%m-%d %H:00 UTC")
|
||||||
|
minute_bucket = timestamp.astimezone(timezone.utc).strftime("%Y-%m-%d %H:%M UTC")
|
||||||
|
self.requests_by_hour[hour_bucket] += 1
|
||||||
|
self.requests_by_minute[minute_bucket] += 1
|
||||||
|
|
||||||
|
def summarize(self, top_n: int, busiest_n: int, use_color: bool) -> str:
|
||||||
|
duration_line = human_duration(self.earliest, self.latest)
|
||||||
|
time_range_line = "unknown"
|
||||||
|
if self.earliest and self.latest:
|
||||||
|
time_range_line = f"{self.earliest.isoformat()} → {self.latest.isoformat()}"
|
||||||
|
|
||||||
|
total_requests = self.parsed_rows
|
||||||
|
success_count = self.response_statuses.get("200", 0)
|
||||||
|
success_pct = fmt_pct(success_count, sum(self.response_statuses.values()))
|
||||||
|
invalid_state = "good" if self.invalid_rows == 0 else "bad"
|
||||||
|
|
||||||
|
top_endpoints = self.endpoint_roots.most_common(2)
|
||||||
|
top_users = self.actor_members.most_common(3)
|
||||||
|
top_minutes = self.requests_by_minute.most_common(busiest_n)
|
||||||
|
|
||||||
|
lines: list[str] = []
|
||||||
|
lines.append(paint("WEBHOOK SNAPSHOT", "1;95", use_color))
|
||||||
|
lines.append(paint("────────────────────────────────────────────────────────", "90", use_color))
|
||||||
|
lines.append(
|
||||||
|
" "
|
||||||
|
+ paint("Rows", "1;97", use_color)
|
||||||
|
+ f": {self.total_rows:<4} "
|
||||||
|
+ paint("Parsed", "1;97", use_color)
|
||||||
|
+ f": {self.parsed_rows:<4} "
|
||||||
|
+ paint("Invalid", "1;97", use_color)
|
||||||
|
+ f": {good_bad_neutral(str(self.invalid_rows), invalid_state, use_color)}"
|
||||||
|
)
|
||||||
|
lines.append(
|
||||||
|
" "
|
||||||
|
+ paint("Window", "1;97", use_color)
|
||||||
|
+ f": {duration_line:<12} "
|
||||||
|
+ paint("Success", "1;97", use_color)
|
||||||
|
+ f": {good_bad_neutral(success_pct, 'good' if success_count else 'neutral', use_color)}"
|
||||||
|
)
|
||||||
|
lines.append(" " + paint("UTC Range", "1;97", use_color) + f": {time_range_line}")
|
||||||
|
|
||||||
|
lines.append("")
|
||||||
|
lines.append(paint("Top Endpoints", "1;94", use_color))
|
||||||
|
if top_endpoints:
|
||||||
|
for endpoint, count in top_endpoints:
|
||||||
|
lines.append(f" • {endpoint:<14} {count:>4} ({fmt_pct(count, total_requests)})")
|
||||||
|
if not top_endpoints:
|
||||||
|
lines.append(" • (no data)")
|
||||||
|
|
||||||
|
lines.append("")
|
||||||
|
lines.append(paint("Most Active Users (query memberId)", "1;94", use_color))
|
||||||
|
if top_users:
|
||||||
|
for user, count in top_users:
|
||||||
|
lines.append(f" • {user:<18} {count:>4} ({fmt_pct(count, total_requests)})")
|
||||||
|
if not top_users:
|
||||||
|
lines.append(" • (no data)")
|
||||||
|
|
||||||
|
lines.append("")
|
||||||
|
lines.append(paint("Busiest Minutes", "1;94", use_color))
|
||||||
|
if top_minutes:
|
||||||
|
for minute, count in top_minutes:
|
||||||
|
lines.append(f" • {minute:<22} {count:>3}")
|
||||||
|
if not top_minutes:
|
||||||
|
lines.append(" • (no data)")
|
||||||
|
|
||||||
|
lines.append("")
|
||||||
|
lines.append(paint("Request Mix", "1;94", use_color))
|
||||||
|
method_line = ", ".join([f"{k}:{v}" for k, v in self.methods.most_common(3)]) or "(no data)"
|
||||||
|
event_line = ", ".join([f"{k}:{v}" for k, v in self.event_types.most_common(3)]) or "(no data)"
|
||||||
|
action_line = ", ".join([f"{k}:{v}" for k, v in self.actions.most_common(3)]) or "(no data)"
|
||||||
|
lines.append(f" • Methods : {method_line}")
|
||||||
|
lines.append(f" • Types : {event_line}")
|
||||||
|
lines.append(f" • Actions : {action_line}")
|
||||||
|
|
||||||
|
lines.append("")
|
||||||
|
lines.append(paint("Status Codes", "1;94", use_color))
|
||||||
|
if self.response_statuses:
|
||||||
|
status_total = sum(self.response_statuses.values())
|
||||||
|
for status, count in self.response_statuses.most_common(5):
|
||||||
|
state = "good" if status.startswith("2") else "bad"
|
||||||
|
status_label = good_bad_neutral(status, state, use_color)
|
||||||
|
lines.append(f" • {status_label}: {count} ({fmt_pct(count, status_total)})")
|
||||||
|
if not self.response_statuses:
|
||||||
|
lines.append(" • (no data)")
|
||||||
|
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
def update_stats(stats: LogStats, row: dict[str, Any]) -> None:
|
||||||
|
timestamp = parse_iso(row.get("timestamp"))
|
||||||
|
stats.add_timestamp(timestamp)
|
||||||
|
|
||||||
|
request = row.get("request") or {}
|
||||||
|
response = row.get("response") or {}
|
||||||
|
body_parsed = request.get("bodyParsed") or {}
|
||||||
|
entity_parsed = request.get("entityParsed") or {}
|
||||||
|
|
||||||
|
method = first_non_empty(request.get("method"))
|
||||||
|
path = first_non_empty(request.get("path"))
|
||||||
|
endpoint_root = path.split("?", 1)[0]
|
||||||
|
status = first_non_empty(response.get("status"))
|
||||||
|
|
||||||
|
event_type = first_non_empty(
|
||||||
|
body_parsed.get("Type"),
|
||||||
|
request.get("summary", {}).get("type"),
|
||||||
|
)
|
||||||
|
action = first_non_empty(
|
||||||
|
body_parsed.get("Action"),
|
||||||
|
request.get("summary", {}).get("action"),
|
||||||
|
request.get("query", {}).get("params", {}).get("action"),
|
||||||
|
)
|
||||||
|
combo = f"{event_type}:{action}"
|
||||||
|
|
||||||
|
source_member = first_non_empty(
|
||||||
|
body_parsed.get("MemberId"),
|
||||||
|
request.get("summary", {}).get("memberId"),
|
||||||
|
)
|
||||||
|
actor_member = first_non_empty(
|
||||||
|
request.get("query", {}).get("params", {}).get("memberId"),
|
||||||
|
request.get("summary", {}).get("entityUpdatedBy"),
|
||||||
|
)
|
||||||
|
updated_by = first_non_empty(
|
||||||
|
entity_parsed.get("UpdatedBy"),
|
||||||
|
request.get("summary", {}).get("entityUpdatedBy"),
|
||||||
|
)
|
||||||
|
company_id = first_non_empty(body_parsed.get("CompanyId"), request.get("headers", {}).get("companyname"))
|
||||||
|
|
||||||
|
stats.methods[method] += 1
|
||||||
|
stats.paths[path] += 1
|
||||||
|
stats.endpoint_roots[endpoint_root] += 1
|
||||||
|
stats.response_statuses[status] += 1
|
||||||
|
stats.event_types[event_type] += 1
|
||||||
|
stats.actions[action] += 1
|
||||||
|
stats.type_action_combo[combo] += 1
|
||||||
|
stats.company_ids[company_id] += 1
|
||||||
|
|
||||||
|
stats.source_members[source_member] += 1
|
||||||
|
stats.actor_members[actor_member] += 1
|
||||||
|
stats.entity_updated_by[updated_by] += 1
|
||||||
|
|
||||||
|
if timestamp:
|
||||||
|
bucket = timestamp.astimezone(timezone.utc).strftime("%Y-%m-%d %H:00 UTC")
|
||||||
|
stats.endpoint_by_hour[endpoint_root][bucket] += 1
|
||||||
|
|
||||||
|
|
||||||
|
def analyze_file(log_path: Path) -> LogStats:
|
||||||
|
stats = LogStats()
|
||||||
|
|
||||||
|
with log_path.open("r", encoding="utf-8") as handle:
|
||||||
|
for raw_line in handle:
|
||||||
|
line = raw_line.strip()
|
||||||
|
if not line:
|
||||||
|
continue
|
||||||
|
|
||||||
|
stats.total_rows += 1
|
||||||
|
try:
|
||||||
|
row = json.loads(line)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
stats.invalid_rows += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
stats.parsed_rows += 1
|
||||||
|
update_stats(stats, row)
|
||||||
|
|
||||||
|
return stats
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Analyze webhook JSONL logs by users, time, and request types."
|
||||||
|
)
|
||||||
|
parser.add_argument("log_file", help="Path to JSONL log file")
|
||||||
|
parser.add_argument("--top", type=int, default=10, help="Top N entries per section (default: 10)")
|
||||||
|
parser.add_argument(
|
||||||
|
"--busiest-minutes",
|
||||||
|
type=int,
|
||||||
|
default=5,
|
||||||
|
help="How many top minute buckets to show (default: 5)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--no-color",
|
||||||
|
action="store_true",
|
||||||
|
help="Disable ANSI colors",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--endpoint-stream",
|
||||||
|
action="store_true",
|
||||||
|
help="Show chronological one-line summary per webhook, similar to live test webserver logs",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--max-path",
|
||||||
|
type=int,
|
||||||
|
default=18,
|
||||||
|
help="Max endpoint width in stream mode before truncation (default: 18)",
|
||||||
|
)
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
log_path = Path(args.log_file)
|
||||||
|
if not log_path.exists() or not log_path.is_file():
|
||||||
|
raise SystemExit(f"Log file not found: {log_path}")
|
||||||
|
|
||||||
|
use_color = supports_color(not args.no_color)
|
||||||
|
|
||||||
|
if args.endpoint_stream:
|
||||||
|
print(endpoint_stream_summary(log_path, use_color=use_color, max_path=max(args.max_path, 10)))
|
||||||
|
return
|
||||||
|
|
||||||
|
stats = analyze_file(log_path)
|
||||||
|
print(
|
||||||
|
stats.summarize(
|
||||||
|
top_n=max(args.top, 1),
|
||||||
|
busiest_n=max(args.busiest_minutes, 1),
|
||||||
|
use_color=use_color,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,900 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Generate a print-friendly PDF report from the latest test-webserver log file.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python3 generate_log_report.py [optional_log_file_path]
|
||||||
|
|
||||||
|
If no path is given, the script finds the latest test-webserver-*.jsonl
|
||||||
|
file in ../cw-api-logs/.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import glob
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from collections import Counter, defaultdict
|
||||||
|
|
||||||
|
from reportlab.lib import colors
|
||||||
|
from reportlab.lib.pagesizes import LETTER
|
||||||
|
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
|
||||||
|
from reportlab.lib.units import inch
|
||||||
|
from reportlab.lib.enums import TA_CENTER, TA_LEFT
|
||||||
|
from reportlab.platypus import (
|
||||||
|
SimpleDocTemplate,
|
||||||
|
Paragraph,
|
||||||
|
Spacer,
|
||||||
|
Table,
|
||||||
|
TableStyle,
|
||||||
|
PageBreak,
|
||||||
|
HRFlowable,
|
||||||
|
KeepTogether,
|
||||||
|
)
|
||||||
|
from reportlab.graphics.shapes import Drawing, String
|
||||||
|
from reportlab.graphics.charts.piecharts import Pie
|
||||||
|
from reportlab.graphics.charts.barcharts import VerticalBarChart
|
||||||
|
|
||||||
|
# ─── Print-friendly color palette ─────────────────────────────────────────────
|
||||||
|
# Minimal ink: white backgrounds, thin borders, dark text, subtle accents
|
||||||
|
HEADER_BG = colors.HexColor("#2c3e50") # Dark header (used sparingly)
|
||||||
|
ACCENT = colors.HexColor("#2980b9") # Muted blue
|
||||||
|
ACCENT_2 = colors.HexColor("#27ae60") # Muted green
|
||||||
|
ACCENT_3 = colors.HexColor("#8e44ad") # Muted purple
|
||||||
|
WHITE = colors.white
|
||||||
|
GRAY_50 = colors.HexColor("#fafafa")
|
||||||
|
GRAY_100 = colors.HexColor("#f5f5f5")
|
||||||
|
GRAY_200 = colors.HexColor("#e0e0e0")
|
||||||
|
GRAY_400 = colors.HexColor("#bdbdbd")
|
||||||
|
GRAY_600 = colors.HexColor("#757575")
|
||||||
|
GRAY_800 = colors.HexColor("#424242")
|
||||||
|
GRAY_900 = colors.HexColor("#212121")
|
||||||
|
|
||||||
|
# Pie/chart fills — muted, distinguishable in B&W too
|
||||||
|
PIE_COLORS = [
|
||||||
|
colors.HexColor("#5b9bd5"), # steel blue
|
||||||
|
colors.HexColor("#ed7d31"), # soft orange
|
||||||
|
colors.HexColor("#a5a5a5"), # gray
|
||||||
|
colors.HexColor("#ffc000"), # amber
|
||||||
|
colors.HexColor("#70ad47"), # olive green
|
||||||
|
colors.HexColor("#4472c4"), # darker blue
|
||||||
|
colors.HexColor("#c55a11"), # brown
|
||||||
|
colors.HexColor("#7030a0"), # purple
|
||||||
|
]
|
||||||
|
|
||||||
|
# ─── Helpers ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def find_latest_log(base_dir):
|
||||||
|
pattern = os.path.join(base_dir, "test-webserver-*.jsonl")
|
||||||
|
files = sorted(glob.glob(pattern))
|
||||||
|
if not files:
|
||||||
|
raise FileNotFoundError(f"No test-webserver log files found in {base_dir}")
|
||||||
|
return files[-1]
|
||||||
|
|
||||||
|
|
||||||
|
def parse_log(path):
|
||||||
|
entries = []
|
||||||
|
with open(path, "r") as f:
|
||||||
|
for line in f:
|
||||||
|
line = line.strip()
|
||||||
|
if not line:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
entries.append(json.loads(line))
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
continue
|
||||||
|
return entries
|
||||||
|
|
||||||
|
|
||||||
|
def fmt_ts(iso_str):
|
||||||
|
try:
|
||||||
|
dt = datetime.fromisoformat(iso_str.replace("Z", "+00:00"))
|
||||||
|
return dt.strftime("%Y-%m-%d %H:%M:%S UTC")
|
||||||
|
except Exception:
|
||||||
|
return str(iso_str)
|
||||||
|
|
||||||
|
|
||||||
|
def duration_str(seconds):
|
||||||
|
if seconds < 60:
|
||||||
|
return f"{seconds:.1f}s"
|
||||||
|
minutes = seconds / 60
|
||||||
|
if minutes < 60:
|
||||||
|
return f"{minutes:.1f}m"
|
||||||
|
hours = minutes / 60
|
||||||
|
return f"{hours:.1f}h"
|
||||||
|
|
||||||
|
|
||||||
|
def truncate(s, max_len=50):
|
||||||
|
s = str(s)
|
||||||
|
return s if len(s) <= max_len else s[: max_len - 3] + "..."
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_actor(entry):
|
||||||
|
"""
|
||||||
|
Derive the actor exactly like testWebserver.ts does:
|
||||||
|
entityUpdatedBy ?? query.params.memberId ?? summary.memberId ?? "-"
|
||||||
|
The summary is already stored in request.summary in the log.
|
||||||
|
"""
|
||||||
|
req = entry.get("request", {})
|
||||||
|
summary = req.get("summary") or {}
|
||||||
|
query = req.get("query") or {}
|
||||||
|
params = query.get("params") or {}
|
||||||
|
|
||||||
|
return str(
|
||||||
|
summary.get("entityUpdatedBy")
|
||||||
|
or params.get("memberId")
|
||||||
|
or summary.get("memberId")
|
||||||
|
or "-"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ─── Analysis ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def analyze(entries):
|
||||||
|
stats = {}
|
||||||
|
|
||||||
|
timestamps = []
|
||||||
|
for e in entries:
|
||||||
|
ts = e.get("timestamp")
|
||||||
|
if ts:
|
||||||
|
try:
|
||||||
|
timestamps.append(datetime.fromisoformat(ts.replace("Z", "+00:00")))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
timestamps.sort()
|
||||||
|
stats["total_entries"] = len(entries)
|
||||||
|
stats["first_ts"] = timestamps[0] if timestamps else None
|
||||||
|
stats["last_ts"] = timestamps[-1] if timestamps else None
|
||||||
|
stats["duration_seconds"] = (
|
||||||
|
(timestamps[-1] - timestamps[0]).total_seconds() if len(timestamps) >= 2 else 0
|
||||||
|
)
|
||||||
|
|
||||||
|
# Global counters
|
||||||
|
methods = Counter()
|
||||||
|
paths = Counter()
|
||||||
|
statuses = Counter()
|
||||||
|
actions = Counter()
|
||||||
|
types = Counter()
|
||||||
|
actors = Counter()
|
||||||
|
companies = Counter()
|
||||||
|
entity_ids = set()
|
||||||
|
stages = Counter()
|
||||||
|
ratings = Counter()
|
||||||
|
hourly_buckets = Counter()
|
||||||
|
minute_buckets = Counter()
|
||||||
|
|
||||||
|
for e in entries:
|
||||||
|
req = e.get("request", {})
|
||||||
|
resp = e.get("response", {})
|
||||||
|
bp = req.get("bodyParsed") or {}
|
||||||
|
entity = req.get("entityParsed") or bp.get("Entity") or {}
|
||||||
|
|
||||||
|
methods[req.get("method", "?")] += 1
|
||||||
|
raw_path = req.get("path", "?").split("?")[0]
|
||||||
|
paths[raw_path] += 1
|
||||||
|
statuses[resp.get("status", "?")] += 1
|
||||||
|
actions[bp.get("Action", "?")] += 1
|
||||||
|
types[bp.get("Type", "?")] += 1
|
||||||
|
|
||||||
|
actor = resolve_actor(e)
|
||||||
|
actors[actor] += 1
|
||||||
|
|
||||||
|
if isinstance(entity, dict):
|
||||||
|
cn = entity.get("CompanyName")
|
||||||
|
if cn:
|
||||||
|
companies[cn] += 1
|
||||||
|
eid = entity.get("Id")
|
||||||
|
if eid is not None:
|
||||||
|
entity_ids.add(eid)
|
||||||
|
stage = entity.get("StageName")
|
||||||
|
if stage:
|
||||||
|
stages[stage] += 1
|
||||||
|
rating = entity.get("Rating")
|
||||||
|
if rating:
|
||||||
|
ratings[rating] += 1
|
||||||
|
|
||||||
|
ts_str = e.get("timestamp")
|
||||||
|
if ts_str:
|
||||||
|
try:
|
||||||
|
dt = datetime.fromisoformat(ts_str.replace("Z", "+00:00"))
|
||||||
|
hourly_buckets[dt.strftime("%H:00")] += 1
|
||||||
|
minute_buckets[dt.strftime("%H:%M")] += 1
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# ── Per-actor deep stats ──
|
||||||
|
actor_details = defaultdict(lambda: {
|
||||||
|
"count": 0,
|
||||||
|
"actions": Counter(),
|
||||||
|
"types": Counter(),
|
||||||
|
"companies": Counter(),
|
||||||
|
"entity_ids": set(),
|
||||||
|
"stages": Counter(),
|
||||||
|
"ratings": Counter(),
|
||||||
|
"timestamps": [],
|
||||||
|
"statuses": Counter(),
|
||||||
|
"paths": Counter(),
|
||||||
|
"member_ids": Counter(),
|
||||||
|
"sales_reps": Counter(),
|
||||||
|
"total_estimated": 0.0,
|
||||||
|
})
|
||||||
|
|
||||||
|
for e in entries:
|
||||||
|
req = e.get("request", {})
|
||||||
|
resp = e.get("response", {})
|
||||||
|
bp = req.get("bodyParsed") or {}
|
||||||
|
entity = req.get("entityParsed") or bp.get("Entity") or {}
|
||||||
|
summary = req.get("summary") or {}
|
||||||
|
|
||||||
|
actor = resolve_actor(e)
|
||||||
|
ad = actor_details[actor]
|
||||||
|
ad["count"] += 1
|
||||||
|
ad["actions"][bp.get("Action", "?")] += 1
|
||||||
|
ad["types"][bp.get("Type", "?")] += 1
|
||||||
|
ad["statuses"][resp.get("status", "?")] += 1
|
||||||
|
raw_path = req.get("path", "?").split("?")[0]
|
||||||
|
ad["paths"][raw_path] += 1
|
||||||
|
|
||||||
|
# Track which MemberIds triggered callbacks for this actor
|
||||||
|
mid = bp.get("MemberId")
|
||||||
|
if mid:
|
||||||
|
ad["member_ids"][mid] += 1
|
||||||
|
|
||||||
|
ts_str = e.get("timestamp")
|
||||||
|
if ts_str:
|
||||||
|
try:
|
||||||
|
ad["timestamps"].append(datetime.fromisoformat(ts_str.replace("Z", "+00:00")))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if isinstance(entity, dict):
|
||||||
|
cn = entity.get("CompanyName")
|
||||||
|
if cn:
|
||||||
|
ad["companies"][cn] += 1
|
||||||
|
eid = entity.get("Id")
|
||||||
|
if eid is not None:
|
||||||
|
ad["entity_ids"].add(eid)
|
||||||
|
stage = entity.get("StageName")
|
||||||
|
if stage:
|
||||||
|
ad["stages"][stage] += 1
|
||||||
|
rating = entity.get("Rating")
|
||||||
|
if rating:
|
||||||
|
ad["ratings"][rating] += 1
|
||||||
|
et = entity.get("EstimatedTotal")
|
||||||
|
if et is not None:
|
||||||
|
ad["total_estimated"] += float(et)
|
||||||
|
pr = entity.get("PrimarySalesRep")
|
||||||
|
if pr:
|
||||||
|
ad["sales_reps"][pr] += 1
|
||||||
|
|
||||||
|
# Compute per-actor derived stats
|
||||||
|
for aid, ad in actor_details.items():
|
||||||
|
ad["timestamps"].sort()
|
||||||
|
if len(ad["timestamps"]) >= 2:
|
||||||
|
dur = (ad["timestamps"][-1] - ad["timestamps"][0]).total_seconds()
|
||||||
|
ad["duration_seconds"] = dur
|
||||||
|
ad["events_per_minute"] = ad["count"] / (dur / 60) if dur > 0 else ad["count"]
|
||||||
|
else:
|
||||||
|
ad["duration_seconds"] = 0
|
||||||
|
ad["events_per_minute"] = ad["count"]
|
||||||
|
ad["first_ts"] = ad["timestamps"][0] if ad["timestamps"] else None
|
||||||
|
ad["last_ts"] = ad["timestamps"][-1] if ad["timestamps"] else None
|
||||||
|
|
||||||
|
stats["actor_details"] = dict(actor_details)
|
||||||
|
stats["methods"] = methods
|
||||||
|
stats["paths"] = paths
|
||||||
|
stats["statuses"] = statuses
|
||||||
|
stats["actions"] = actions
|
||||||
|
stats["types"] = types
|
||||||
|
stats["actors"] = actors
|
||||||
|
stats["companies"] = companies
|
||||||
|
stats["entity_ids"] = entity_ids
|
||||||
|
stats["stages"] = stages
|
||||||
|
stats["ratings"] = ratings
|
||||||
|
stats["hourly_buckets"] = hourly_buckets
|
||||||
|
stats["minute_buckets"] = minute_buckets
|
||||||
|
|
||||||
|
if stats["duration_seconds"] > 0:
|
||||||
|
stats["events_per_minute"] = len(entries) / (stats["duration_seconds"] / 60)
|
||||||
|
else:
|
||||||
|
stats["events_per_minute"] = len(entries)
|
||||||
|
|
||||||
|
return stats
|
||||||
|
|
||||||
|
|
||||||
|
# ─── PDF building ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def build_styles():
|
||||||
|
ss = getSampleStyleSheet()
|
||||||
|
ss.add(ParagraphStyle(
|
||||||
|
"ReportTitle", parent=ss["Title"], fontSize=24, textColor=WHITE,
|
||||||
|
spaceAfter=4, fontName="Helvetica-Bold", alignment=TA_CENTER,
|
||||||
|
))
|
||||||
|
ss.add(ParagraphStyle(
|
||||||
|
"ReportSubtitle", parent=ss["Normal"], fontSize=11, textColor=GRAY_400,
|
||||||
|
spaceAfter=2, fontName="Helvetica", alignment=TA_CENTER,
|
||||||
|
))
|
||||||
|
ss.add(ParagraphStyle(
|
||||||
|
"SectionHeader", parent=ss["Heading1"], fontSize=16, textColor=GRAY_900,
|
||||||
|
spaceBefore=14, spaceAfter=6, fontName="Helvetica-Bold",
|
||||||
|
))
|
||||||
|
ss.add(ParagraphStyle(
|
||||||
|
"SubHeader", parent=ss["Heading2"], fontSize=12, textColor=GRAY_800,
|
||||||
|
spaceBefore=10, spaceAfter=4, fontName="Helvetica-Bold",
|
||||||
|
))
|
||||||
|
ss.add(ParagraphStyle(
|
||||||
|
"BodyText2", parent=ss["Normal"], fontSize=9, textColor=GRAY_800,
|
||||||
|
spaceAfter=3, fontName="Helvetica", leading=13,
|
||||||
|
))
|
||||||
|
ss.add(ParagraphStyle(
|
||||||
|
"SmallGray", parent=ss["Normal"], fontSize=8, textColor=GRAY_600,
|
||||||
|
spaceAfter=2, fontName="Helvetica",
|
||||||
|
))
|
||||||
|
ss.add(ParagraphStyle(
|
||||||
|
"KPIValue", parent=ss["Normal"], fontSize=20, textColor=GRAY_900,
|
||||||
|
fontName="Helvetica-Bold", alignment=TA_CENTER, leading=24,
|
||||||
|
))
|
||||||
|
ss.add(ParagraphStyle(
|
||||||
|
"KPILabel", parent=ss["Normal"], fontSize=8, textColor=GRAY_600,
|
||||||
|
fontName="Helvetica", alignment=TA_CENTER, spaceAfter=2,
|
||||||
|
))
|
||||||
|
ss.add(ParagraphStyle(
|
||||||
|
"BannerText", parent=ss["Normal"], fontSize=9, textColor=WHITE,
|
||||||
|
fontName="Helvetica-Bold", spaceAfter=1,
|
||||||
|
))
|
||||||
|
return ss
|
||||||
|
|
||||||
|
|
||||||
|
def make_header_banner(ss, log_path, stats):
|
||||||
|
elements = []
|
||||||
|
fname = os.path.basename(log_path)
|
||||||
|
banner_data = [[
|
||||||
|
Paragraph("Webhook Log Report", ss["ReportTitle"]),
|
||||||
|
], [
|
||||||
|
Paragraph(fname, ss["ReportSubtitle"]),
|
||||||
|
], [
|
||||||
|
Paragraph(
|
||||||
|
f"Generated {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M UTC')}",
|
||||||
|
ss["ReportSubtitle"],
|
||||||
|
),
|
||||||
|
]]
|
||||||
|
banner = Table(banner_data, colWidths=[7.0 * inch])
|
||||||
|
banner.setStyle(TableStyle([
|
||||||
|
("BACKGROUND", (0, 0), (-1, -1), HEADER_BG),
|
||||||
|
("ALIGN", (0, 0), (-1, -1), "CENTER"),
|
||||||
|
("TOPPADDING", (0, 0), (0, 0), 20),
|
||||||
|
("BOTTOMPADDING", (0, -1), (-1, -1), 16),
|
||||||
|
("LEFTPADDING", (0, 0), (-1, -1), 20),
|
||||||
|
("RIGHTPADDING", (0, 0), (-1, -1), 20),
|
||||||
|
]))
|
||||||
|
elements.append(banner)
|
||||||
|
elements.append(Spacer(1, 14))
|
||||||
|
return elements
|
||||||
|
|
||||||
|
|
||||||
|
def make_kpi_card(label, value):
|
||||||
|
ss = build_styles()
|
||||||
|
card_data = [[
|
||||||
|
Paragraph(str(value), ss["KPIValue"]),
|
||||||
|
], [
|
||||||
|
Paragraph(label, ss["KPILabel"]),
|
||||||
|
]]
|
||||||
|
card = Table(card_data, colWidths=[1.6 * inch], rowHeights=[28, 16])
|
||||||
|
card.setStyle(TableStyle([
|
||||||
|
("BACKGROUND", (0, 0), (-1, -1), GRAY_100),
|
||||||
|
("ALIGN", (0, 0), (-1, -1), "CENTER"),
|
||||||
|
("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
|
||||||
|
("TOPPADDING", (0, 0), (-1, -1), 6),
|
||||||
|
("BOTTOMPADDING", (0, 0), (-1, -1), 4),
|
||||||
|
("BOX", (0, 0), (-1, -1), 0.5, GRAY_200),
|
||||||
|
]))
|
||||||
|
return card
|
||||||
|
|
||||||
|
|
||||||
|
def make_kpi_row(cards_data):
|
||||||
|
cards = [make_kpi_card(label, value) for label, value in cards_data]
|
||||||
|
row = Table([cards], colWidths=[1.75 * inch] * len(cards))
|
||||||
|
row.setStyle(TableStyle([
|
||||||
|
("ALIGN", (0, 0), (-1, -1), "CENTER"),
|
||||||
|
("VALIGN", (0, 0), (-1, -1), "TOP"),
|
||||||
|
]))
|
||||||
|
return row
|
||||||
|
|
||||||
|
|
||||||
|
def make_table(title, counter, ss, max_rows=15):
|
||||||
|
"""Generic table from a Counter — light styling for print."""
|
||||||
|
elements = []
|
||||||
|
elements.append(Paragraph(title, ss["SubHeader"]))
|
||||||
|
|
||||||
|
items = counter.most_common(max_rows)
|
||||||
|
if not items:
|
||||||
|
elements.append(Paragraph("<i>No data</i>", ss["BodyText2"]))
|
||||||
|
return elements
|
||||||
|
|
||||||
|
total = sum(counter.values())
|
||||||
|
header = ["Item", "Count", "%"]
|
||||||
|
rows = [header]
|
||||||
|
for name, count in items:
|
||||||
|
pct = (count / total * 100) if total else 0
|
||||||
|
rows.append([truncate(str(name), 45), f"{count:,}", f"{pct:.1f}%"])
|
||||||
|
|
||||||
|
t = Table(rows, colWidths=[3.6 * inch, 1.0 * inch, 0.8 * inch])
|
||||||
|
t.setStyle(TableStyle([
|
||||||
|
("BACKGROUND", (0, 0), (-1, 0), GRAY_800),
|
||||||
|
("TEXTCOLOR", (0, 0), (-1, 0), WHITE),
|
||||||
|
("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"),
|
||||||
|
("FONTSIZE", (0, 0), (-1, 0), 9),
|
||||||
|
("FONTSIZE", (0, 1), (-1, -1), 9),
|
||||||
|
("FONTNAME", (0, 1), (-1, -1), "Helvetica"),
|
||||||
|
("BOTTOMPADDING", (0, 0), (-1, 0), 6),
|
||||||
|
("TOPPADDING", (0, 0), (-1, 0), 6),
|
||||||
|
("GRID", (0, 0), (-1, -1), 0.4, GRAY_200),
|
||||||
|
("ROWBACKGROUNDS", (0, 1), (-1, -1), [WHITE, GRAY_50]),
|
||||||
|
("ALIGN", (1, 0), (2, -1), "CENTER"),
|
||||||
|
("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
|
||||||
|
("LEFTPADDING", (0, 0), (-1, -1), 8),
|
||||||
|
("RIGHTPADDING", (0, 0), (-1, -1), 8),
|
||||||
|
("TOPPADDING", (0, 1), (-1, -1), 3),
|
||||||
|
("BOTTOMPADDING", (0, 1), (-1, -1), 3),
|
||||||
|
]))
|
||||||
|
elements.append(t)
|
||||||
|
return elements
|
||||||
|
|
||||||
|
|
||||||
|
def make_pie_chart(title, counter, width=280, height=190):
|
||||||
|
items = counter.most_common(8)
|
||||||
|
if not items:
|
||||||
|
return Spacer(1, 1)
|
||||||
|
|
||||||
|
d = Drawing(width, height)
|
||||||
|
d.add(String(width / 2, height - 12, title,
|
||||||
|
fontSize=10, fontName="Helvetica-Bold",
|
||||||
|
fillColor=GRAY_900, textAnchor="middle"))
|
||||||
|
|
||||||
|
pie = Pie()
|
||||||
|
pie.x = 50
|
||||||
|
pie.y = 10
|
||||||
|
pie.width = 110
|
||||||
|
pie.height = 110
|
||||||
|
pie.data = [v for _, v in items]
|
||||||
|
pie.labels = [truncate(str(k), 18) for k, _ in items]
|
||||||
|
pie.sideLabels = True
|
||||||
|
pie.slices.strokeWidth = 0.5
|
||||||
|
pie.slices.strokeColor = WHITE
|
||||||
|
|
||||||
|
for i in range(len(items)):
|
||||||
|
pie.slices[i].fillColor = PIE_COLORS[i % len(PIE_COLORS)]
|
||||||
|
pie.slices[i].fontName = "Helvetica"
|
||||||
|
pie.slices[i].fontSize = 7
|
||||||
|
pie.slices[i].labelRadius = 1.35
|
||||||
|
|
||||||
|
d.add(pie)
|
||||||
|
return d
|
||||||
|
|
||||||
|
|
||||||
|
def make_timeline_chart(minute_buckets, width=500, height=150):
|
||||||
|
if not minute_buckets:
|
||||||
|
return Spacer(1, 1)
|
||||||
|
|
||||||
|
sorted_keys = sorted(minute_buckets.keys())
|
||||||
|
if len(sorted_keys) > 40:
|
||||||
|
step = max(1, len(sorted_keys) // 40)
|
||||||
|
sampled_keys = sorted_keys[::step]
|
||||||
|
else:
|
||||||
|
sampled_keys = sorted_keys
|
||||||
|
|
||||||
|
values = [minute_buckets[k] for k in sampled_keys]
|
||||||
|
|
||||||
|
d = Drawing(width, height)
|
||||||
|
d.add(String(width / 2, height - 10, "Event Timeline (by minute)",
|
||||||
|
fontSize=10, fontName="Helvetica-Bold",
|
||||||
|
fillColor=GRAY_900, textAnchor="middle"))
|
||||||
|
|
||||||
|
chart = VerticalBarChart()
|
||||||
|
chart.x = 50
|
||||||
|
chart.y = 25
|
||||||
|
chart.width = width - 80
|
||||||
|
chart.height = height - 50
|
||||||
|
chart.data = [values]
|
||||||
|
chart.categoryAxis.categoryNames = sampled_keys
|
||||||
|
chart.categoryAxis.labels.angle = 45
|
||||||
|
chart.categoryAxis.labels.fontSize = 6
|
||||||
|
chart.categoryAxis.labels.fontName = "Helvetica"
|
||||||
|
chart.categoryAxis.labels.dy = -5
|
||||||
|
chart.valueAxis.labels.fontSize = 7
|
||||||
|
chart.valueAxis.labels.fontName = "Helvetica"
|
||||||
|
chart.valueAxis.valueMin = 0
|
||||||
|
chart.bars[0].fillColor = GRAY_600
|
||||||
|
chart.bars[0].strokeColor = None
|
||||||
|
chart.barWidth = max(2, int((width - 100) / len(values) * 0.7))
|
||||||
|
|
||||||
|
d.add(chart)
|
||||||
|
return d
|
||||||
|
|
||||||
|
|
||||||
|
def build_actor_activity_section(stats, ss):
|
||||||
|
"""Per-actor deep-dive. Actor = entityUpdatedBy ?? query.memberId ?? summary.memberId."""
|
||||||
|
elements = []
|
||||||
|
elements.append(PageBreak())
|
||||||
|
elements.append(Paragraph("Actor Activity Deep Dive", ss["SectionHeader"]))
|
||||||
|
elements.append(HRFlowable(width="100%", thickness=1, color=GRAY_400, spaceAfter=4))
|
||||||
|
elements.append(Paragraph(
|
||||||
|
'The <b>actor</b> is resolved as: <i>entityUpdatedBy → query.memberId → summary.memberId</i>. '
|
||||||
|
'This is the person or system that caused the change in ConnectWise.',
|
||||||
|
ss["SmallGray"],
|
||||||
|
))
|
||||||
|
elements.append(Spacer(1, 10))
|
||||||
|
|
||||||
|
actor_details = stats.get("actor_details", {})
|
||||||
|
if not actor_details:
|
||||||
|
elements.append(Paragraph("<i>No actor data available.</i>", ss["BodyText2"]))
|
||||||
|
return elements
|
||||||
|
|
||||||
|
sorted_actors = sorted(actor_details.items(), key=lambda x: -x[1]["count"])
|
||||||
|
|
||||||
|
# Actor distribution pie chart
|
||||||
|
actor_counter = Counter({aid: ad["count"] for aid, ad in sorted_actors})
|
||||||
|
elements.append(make_pie_chart("Events by Actor", actor_counter, width=350, height=210))
|
||||||
|
elements.append(Spacer(1, 10))
|
||||||
|
|
||||||
|
for idx, (aid, ad) in enumerate(sorted_actors):
|
||||||
|
# Actor header — slim dark bar
|
||||||
|
banner_data = [[
|
||||||
|
Paragraph(
|
||||||
|
f'<font size="12"><b>{aid}</b></font>'
|
||||||
|
f' '
|
||||||
|
f'<font size="9" color="#cccccc">{ad["count"]:,} events</font>',
|
||||||
|
ss["BannerText"],
|
||||||
|
),
|
||||||
|
]]
|
||||||
|
banner = Table(banner_data, colWidths=[7.0 * inch])
|
||||||
|
banner.setStyle(TableStyle([
|
||||||
|
("BACKGROUND", (0, 0), (-1, -1), HEADER_BG),
|
||||||
|
("TOPPADDING", (0, 0), (-1, -1), 7),
|
||||||
|
("BOTTOMPADDING", (0, 0), (-1, -1), 7),
|
||||||
|
("LEFTPADDING", (0, 0), (-1, -1), 12),
|
||||||
|
]))
|
||||||
|
elements.append(banner)
|
||||||
|
|
||||||
|
# KPI row
|
||||||
|
kpi = make_kpi_row([
|
||||||
|
("Events", f"{ad['count']:,}"),
|
||||||
|
("Entities", f"{len(ad['entity_ids']):,}"),
|
||||||
|
("Companies", f"{len(ad['companies']):,}"),
|
||||||
|
("Evts/Min", f"{ad['events_per_minute']:.1f}"),
|
||||||
|
])
|
||||||
|
elements.append(kpi)
|
||||||
|
elements.append(Spacer(1, 4))
|
||||||
|
|
||||||
|
# Info grid
|
||||||
|
first_str = ad["first_ts"].strftime("%Y-%m-%d %H:%M:%S") if ad["first_ts"] else "—"
|
||||||
|
last_str = ad["last_ts"].strftime("%Y-%m-%d %H:%M:%S") if ad["last_ts"] else "—"
|
||||||
|
dur = duration_str(ad["duration_seconds"])
|
||||||
|
est = ad["total_estimated"]
|
||||||
|
|
||||||
|
mid_str = ", ".join(f"{k} ({v})" for k, v in ad["member_ids"].most_common(5)) if ad["member_ids"] else "—"
|
||||||
|
sr_str = ", ".join(f"{k} ({v})" for k, v in ad["sales_reps"].most_common(5)) if ad["sales_reps"] else "—"
|
||||||
|
|
||||||
|
info_rows = [
|
||||||
|
[
|
||||||
|
Paragraph('<font color="#757575">First Event</font>', ss["BodyText2"]),
|
||||||
|
Paragraph(f'<b>{first_str}</b>', ss["BodyText2"]),
|
||||||
|
Paragraph('<font color="#757575">Last Event</font>', ss["BodyText2"]),
|
||||||
|
Paragraph(f'<b>{last_str}</b>', ss["BodyText2"]),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
Paragraph('<font color="#757575">Active Duration</font>', ss["BodyText2"]),
|
||||||
|
Paragraph(f'<b>{dur}</b>', ss["BodyText2"]),
|
||||||
|
Paragraph('<font color="#757575">Total Est. Value</font>', ss["BodyText2"]),
|
||||||
|
Paragraph(f'<b>${est:,.2f}</b>', ss["BodyText2"]),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
Paragraph('<font color="#757575">Callback Members</font>', ss["BodyText2"]),
|
||||||
|
Paragraph(f'{truncate(mid_str, 40)}', ss["BodyText2"]),
|
||||||
|
Paragraph('<font color="#757575">Sales Reps</font>', ss["BodyText2"]),
|
||||||
|
Paragraph(f'{truncate(sr_str, 40)}', ss["BodyText2"]),
|
||||||
|
],
|
||||||
|
]
|
||||||
|
info_table = Table(info_rows, colWidths=[1.3 * inch, 2.1 * inch, 1.3 * inch, 2.1 * inch])
|
||||||
|
info_table.setStyle(TableStyle([
|
||||||
|
("BACKGROUND", (0, 0), (-1, -1), GRAY_50),
|
||||||
|
("TOPPADDING", (0, 0), (-1, -1), 4),
|
||||||
|
("BOTTOMPADDING", (0, 0), (-1, -1), 4),
|
||||||
|
("LEFTPADDING", (0, 0), (-1, -1), 8),
|
||||||
|
("GRID", (0, 0), (-1, -1), 0.3, GRAY_200),
|
||||||
|
]))
|
||||||
|
elements.append(info_table)
|
||||||
|
elements.append(Spacer(1, 6))
|
||||||
|
|
||||||
|
# Breakdown tables
|
||||||
|
if ad["types"]:
|
||||||
|
elements.extend(make_table(f"{aid} — Entity Types", ad["types"], ss, max_rows=8))
|
||||||
|
elements.append(Spacer(1, 6))
|
||||||
|
|
||||||
|
if ad["companies"]:
|
||||||
|
elements.extend(make_table(f"{aid} — Companies", ad["companies"], ss, max_rows=10))
|
||||||
|
elements.append(Spacer(1, 6))
|
||||||
|
|
||||||
|
if ad["stages"]:
|
||||||
|
elements.extend(make_table(f"{aid} — Stages", ad["stages"], ss, max_rows=8))
|
||||||
|
elements.append(Spacer(1, 6))
|
||||||
|
|
||||||
|
if ad["ratings"]:
|
||||||
|
elements.extend(make_table(f"{aid} — Ratings", ad["ratings"], ss, max_rows=8))
|
||||||
|
elements.append(Spacer(1, 6))
|
||||||
|
|
||||||
|
# Entity IDs
|
||||||
|
if ad["entity_ids"]:
|
||||||
|
id_list = sorted(ad["entity_ids"])
|
||||||
|
id_str = ", ".join(str(i) for i in id_list[:30])
|
||||||
|
if len(id_list) > 30:
|
||||||
|
id_str += f" ... (+{len(id_list) - 30} more)"
|
||||||
|
elements.append(Paragraph(f"{aid} — Entity IDs Touched", ss["SubHeader"]))
|
||||||
|
elements.append(Paragraph(f'<font size="8">{id_str}</font>', ss["BodyText2"]))
|
||||||
|
elements.append(Spacer(1, 6))
|
||||||
|
|
||||||
|
# Divider
|
||||||
|
if idx < len(sorted_actors) - 1:
|
||||||
|
elements.append(Spacer(1, 4))
|
||||||
|
elements.append(HRFlowable(width="100%", thickness=0.5, color=GRAY_200, spaceAfter=4))
|
||||||
|
elements.append(Spacer(1, 4))
|
||||||
|
|
||||||
|
return elements
|
||||||
|
|
||||||
|
|
||||||
|
def build_summary_log_table(entries, ss, max_rows=30):
|
||||||
|
elements = []
|
||||||
|
elements.append(PageBreak())
|
||||||
|
elements.append(Paragraph("Event Summary", ss["SectionHeader"]))
|
||||||
|
elements.append(HRFlowable(width="100%", thickness=1, color=GRAY_400, spaceAfter=6))
|
||||||
|
elements.append(Paragraph(
|
||||||
|
f"Aggregated view of {len(entries):,} webhook events — grouped by entity.",
|
||||||
|
ss["SmallGray"],
|
||||||
|
))
|
||||||
|
elements.append(Spacer(1, 8))
|
||||||
|
|
||||||
|
entity_groups = defaultdict(lambda: {
|
||||||
|
"count": 0, "name": "—", "company": "—",
|
||||||
|
"actions": Counter(), "actors": set(),
|
||||||
|
"est_total": None,
|
||||||
|
})
|
||||||
|
|
||||||
|
for e in entries:
|
||||||
|
req = e.get("request", {})
|
||||||
|
bp = req.get("bodyParsed") or {}
|
||||||
|
entity = req.get("entityParsed") or bp.get("Entity") or {}
|
||||||
|
if not isinstance(entity, dict):
|
||||||
|
continue
|
||||||
|
eid = entity.get("Id")
|
||||||
|
if eid is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
eg = entity_groups[eid]
|
||||||
|
eg["count"] += 1
|
||||||
|
eg["name"] = entity.get("OpportunityName") or entity.get("CompanyName") or eg["name"]
|
||||||
|
eg["company"] = entity.get("CompanyName") or eg["company"]
|
||||||
|
eg["actions"][bp.get("Action", "?")] += 1
|
||||||
|
actor = resolve_actor(e)
|
||||||
|
eg["actors"].add(actor)
|
||||||
|
et = entity.get("EstimatedTotal")
|
||||||
|
if et is not None:
|
||||||
|
eg["est_total"] = et
|
||||||
|
|
||||||
|
sorted_entities = sorted(entity_groups.items(), key=lambda x: -x[1]["count"])
|
||||||
|
|
||||||
|
header = ["ID", "Entity Name", "Company", "Events", "Actions", "Actors", "Est. Total"]
|
||||||
|
rows = [header]
|
||||||
|
|
||||||
|
for eid, eg in sorted_entities[:max_rows]:
|
||||||
|
actions_str = ", ".join(f"{a}({c})" for a, c in eg["actions"].most_common(3))
|
||||||
|
actors_str = ", ".join(sorted(eg["actors"]))
|
||||||
|
total_str = f"${eg['est_total']:,.2f}" if eg["est_total"] is not None else "—"
|
||||||
|
rows.append([
|
||||||
|
str(eid),
|
||||||
|
truncate(eg["name"], 28),
|
||||||
|
truncate(eg["company"], 18),
|
||||||
|
f"{eg['count']:,}",
|
||||||
|
truncate(actions_str, 24),
|
||||||
|
truncate(actors_str, 18),
|
||||||
|
total_str,
|
||||||
|
])
|
||||||
|
|
||||||
|
t = Table(rows, colWidths=[0.45 * inch, 1.7 * inch, 1.1 * inch, 0.55 * inch, 1.2 * inch, 1.0 * inch, 0.8 * inch])
|
||||||
|
t.setStyle(TableStyle([
|
||||||
|
("BACKGROUND", (0, 0), (-1, 0), GRAY_800),
|
||||||
|
("TEXTCOLOR", (0, 0), (-1, 0), WHITE),
|
||||||
|
("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"),
|
||||||
|
("FONTSIZE", (0, 0), (-1, 0), 8),
|
||||||
|
("FONTSIZE", (0, 1), (-1, -1), 8),
|
||||||
|
("FONTNAME", (0, 1), (-1, -1), "Helvetica"),
|
||||||
|
("GRID", (0, 0), (-1, -1), 0.3, GRAY_200),
|
||||||
|
("ROWBACKGROUNDS", (0, 1), (-1, -1), [WHITE, GRAY_50]),
|
||||||
|
("ALIGN", (0, 0), (0, -1), "CENTER"),
|
||||||
|
("ALIGN", (3, 0), (3, -1), "CENTER"),
|
||||||
|
("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
|
||||||
|
("LEFTPADDING", (0, 0), (-1, -1), 5),
|
||||||
|
("RIGHTPADDING", (0, 0), (-1, -1), 5),
|
||||||
|
("TOPPADDING", (0, 1), (-1, -1), 2),
|
||||||
|
("BOTTOMPADDING", (0, 1), (-1, -1), 2),
|
||||||
|
]))
|
||||||
|
elements.append(t)
|
||||||
|
|
||||||
|
if len(sorted_entities) > max_rows:
|
||||||
|
elements.append(Spacer(1, 4))
|
||||||
|
elements.append(Paragraph(
|
||||||
|
f'Showing top {max_rows} of {len(sorted_entities)} entities.',
|
||||||
|
ss["SmallGray"],
|
||||||
|
))
|
||||||
|
|
||||||
|
return elements
|
||||||
|
|
||||||
|
|
||||||
|
def add_page_number(canvas, doc):
|
||||||
|
canvas.saveState()
|
||||||
|
canvas.setFillColor(GRAY_800)
|
||||||
|
canvas.rect(0, 0, LETTER[0], 22, fill=1, stroke=0)
|
||||||
|
canvas.setFillColor(WHITE)
|
||||||
|
canvas.setFont("Helvetica", 7)
|
||||||
|
canvas.drawCentredString(LETTER[0] / 2, 8, f"Page {doc.page}")
|
||||||
|
canvas.setFillColor(GRAY_400)
|
||||||
|
canvas.setFont("Helvetica", 7)
|
||||||
|
canvas.drawString(30, 8, "Optima API · Webhook Log Report")
|
||||||
|
canvas.drawRightString(LETTER[0] - 30, 8, datetime.now(timezone.utc).strftime("%Y-%m-%d"))
|
||||||
|
canvas.restoreState()
|
||||||
|
|
||||||
|
|
||||||
|
# ─── Main ─────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def main():
|
||||||
|
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
log_dir = os.path.join(script_dir, "..", "cw-api-logs")
|
||||||
|
|
||||||
|
if len(sys.argv) > 1:
|
||||||
|
log_path = sys.argv[1]
|
||||||
|
else:
|
||||||
|
log_path = find_latest_log(log_dir)
|
||||||
|
|
||||||
|
print(f"📄 Reading log: {log_path}")
|
||||||
|
entries = parse_log(log_path)
|
||||||
|
print(f" → {len(entries)} entries parsed")
|
||||||
|
|
||||||
|
stats = analyze(entries)
|
||||||
|
ss = build_styles()
|
||||||
|
|
||||||
|
log_basename = os.path.splitext(os.path.basename(log_path))[0]
|
||||||
|
out_path = os.path.join(script_dir, "..", "cw-api-logs", f"{log_basename}-report.pdf")
|
||||||
|
|
||||||
|
doc = SimpleDocTemplate(
|
||||||
|
out_path,
|
||||||
|
pagesize=LETTER,
|
||||||
|
leftMargin=0.6 * inch,
|
||||||
|
rightMargin=0.6 * inch,
|
||||||
|
topMargin=0.5 * inch,
|
||||||
|
bottomMargin=0.5 * inch,
|
||||||
|
)
|
||||||
|
|
||||||
|
elements = []
|
||||||
|
|
||||||
|
# ── Title Banner ──
|
||||||
|
elements.extend(make_header_banner(ss, log_path, stats))
|
||||||
|
|
||||||
|
# ── Overview ──
|
||||||
|
elements.append(Paragraph("Overview", ss["SectionHeader"]))
|
||||||
|
elements.append(HRFlowable(width="100%", thickness=1, color=GRAY_400, spaceAfter=10))
|
||||||
|
|
||||||
|
elements.append(make_kpi_row([
|
||||||
|
("Total Events", f"{stats['total_entries']:,}"),
|
||||||
|
("Unique Entities", f"{len(stats['entity_ids']):,}"),
|
||||||
|
("Companies", f"{len(stats['companies']):,}"),
|
||||||
|
("Duration", duration_str(stats["duration_seconds"])),
|
||||||
|
]))
|
||||||
|
elements.append(Spacer(1, 8))
|
||||||
|
elements.append(make_kpi_row([
|
||||||
|
("Events / Min", f"{stats['events_per_minute']:.1f}"),
|
||||||
|
("HTTP Methods", f"{len(stats['methods']):,}"),
|
||||||
|
("Action Types", f"{len(stats['actions']):,}"),
|
||||||
|
("Actors", f"{len(stats['actors']):,}"),
|
||||||
|
]))
|
||||||
|
elements.append(Spacer(1, 12))
|
||||||
|
|
||||||
|
# Time range
|
||||||
|
if stats["first_ts"] and stats["last_ts"]:
|
||||||
|
info = [
|
||||||
|
[
|
||||||
|
Paragraph('<font color="#757575">First Event</font>', ss["BodyText2"]),
|
||||||
|
Paragraph(f'<b>{stats["first_ts"].strftime("%Y-%m-%d %H:%M:%S UTC")}</b>', ss["BodyText2"]),
|
||||||
|
Paragraph('<font color="#757575">Last Event</font>', ss["BodyText2"]),
|
||||||
|
Paragraph(f'<b>{stats["last_ts"].strftime("%Y-%m-%d %H:%M:%S UTC")}</b>', ss["BodyText2"]),
|
||||||
|
]
|
||||||
|
]
|
||||||
|
ti = Table(info, colWidths=[1.2 * inch, 2.4 * inch, 1.2 * inch, 2.4 * inch])
|
||||||
|
ti.setStyle(TableStyle([
|
||||||
|
("BACKGROUND", (0, 0), (-1, -1), GRAY_50),
|
||||||
|
("TOPPADDING", (0, 0), (-1, -1), 5),
|
||||||
|
("BOTTOMPADDING", (0, 0), (-1, -1), 5),
|
||||||
|
("LEFTPADDING", (0, 0), (-1, -1), 8),
|
||||||
|
("BOX", (0, 0), (-1, -1), 0.4, GRAY_200),
|
||||||
|
]))
|
||||||
|
elements.append(ti)
|
||||||
|
elements.append(Spacer(1, 14))
|
||||||
|
|
||||||
|
# ── Charts ──
|
||||||
|
elements.append(Paragraph("Visual Breakdown", ss["SectionHeader"]))
|
||||||
|
elements.append(HRFlowable(width="100%", thickness=1, color=GRAY_400, spaceAfter=10))
|
||||||
|
|
||||||
|
elements.append(make_timeline_chart(stats["minute_buckets"]))
|
||||||
|
elements.append(Spacer(1, 12))
|
||||||
|
|
||||||
|
pie_row = [[
|
||||||
|
make_pie_chart("By Action", stats["actions"]),
|
||||||
|
make_pie_chart("By Type", stats["types"]),
|
||||||
|
]]
|
||||||
|
pt = Table(pie_row, colWidths=[3.5 * inch, 3.5 * inch])
|
||||||
|
pt.setStyle(TableStyle([
|
||||||
|
("ALIGN", (0, 0), (-1, -1), "CENTER"),
|
||||||
|
("VALIGN", (0, 0), (-1, -1), "TOP"),
|
||||||
|
]))
|
||||||
|
elements.append(pt)
|
||||||
|
elements.append(Spacer(1, 6))
|
||||||
|
|
||||||
|
if len(stats["stages"]) > 1 or len(stats["ratings"]) > 1:
|
||||||
|
pie_row2 = [[
|
||||||
|
make_pie_chart("By Stage", stats["stages"]),
|
||||||
|
make_pie_chart("By Rating", stats["ratings"]),
|
||||||
|
]]
|
||||||
|
pt2 = Table(pie_row2, colWidths=[3.5 * inch, 3.5 * inch])
|
||||||
|
pt2.setStyle(TableStyle([
|
||||||
|
("ALIGN", (0, 0), (-1, -1), "CENTER"),
|
||||||
|
("VALIGN", (0, 0), (-1, -1), "TOP"),
|
||||||
|
]))
|
||||||
|
elements.append(pt2)
|
||||||
|
|
||||||
|
# Actor pie chart
|
||||||
|
elements.append(Spacer(1, 6))
|
||||||
|
elements.append(make_pie_chart("By Actor", stats["actors"], width=350, height=210))
|
||||||
|
|
||||||
|
# ── General Information ──
|
||||||
|
elements.append(PageBreak())
|
||||||
|
elements.append(Paragraph("General Information", ss["SectionHeader"]))
|
||||||
|
elements.append(HRFlowable(width="100%", thickness=1, color=GRAY_400, spaceAfter=10))
|
||||||
|
|
||||||
|
elements.extend(make_table("Response Status Codes", stats["statuses"], ss))
|
||||||
|
elements.append(Spacer(1, 10))
|
||||||
|
elements.extend(make_table("HTTP Methods", stats["methods"], ss))
|
||||||
|
elements.append(Spacer(1, 10))
|
||||||
|
elements.extend(make_table("Webhook Actions", stats["actions"], ss))
|
||||||
|
elements.append(Spacer(1, 10))
|
||||||
|
elements.extend(make_table("Entity Types", stats["types"], ss))
|
||||||
|
elements.append(Spacer(1, 10))
|
||||||
|
elements.extend(make_table("Request Paths", stats["paths"], ss))
|
||||||
|
elements.append(Spacer(1, 10))
|
||||||
|
elements.extend(make_table("Actors", stats["actors"], ss))
|
||||||
|
elements.append(Spacer(1, 10))
|
||||||
|
|
||||||
|
if stats["companies"]:
|
||||||
|
elements.extend(make_table("Companies", stats["companies"], ss))
|
||||||
|
elements.append(Spacer(1, 10))
|
||||||
|
if stats["stages"]:
|
||||||
|
elements.extend(make_table("Opportunity Stages", stats["stages"], ss))
|
||||||
|
elements.append(Spacer(1, 10))
|
||||||
|
if stats["ratings"]:
|
||||||
|
elements.extend(make_table("Opportunity Ratings", stats["ratings"], ss))
|
||||||
|
elements.append(Spacer(1, 10))
|
||||||
|
|
||||||
|
elements.extend(make_table("Hourly Distribution", stats["hourly_buckets"], ss))
|
||||||
|
|
||||||
|
# ── Actor Deep Dive ──
|
||||||
|
elements.extend(build_actor_activity_section(stats, ss))
|
||||||
|
|
||||||
|
# ── Entity Summary ──
|
||||||
|
elements.extend(build_summary_log_table(entries, ss, max_rows=30))
|
||||||
|
|
||||||
|
# Build
|
||||||
|
doc.build(elements, onFirstPage=add_page_number, onLaterPages=add_page_number)
|
||||||
|
print(f"✅ Report generated: {out_path}")
|
||||||
|
print(f" File size: {os.path.getsize(out_path) / 1024:.1f} KB")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -47,6 +47,11 @@ export type Company = Prisma.CompanyModel
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export type CatalogItem = Prisma.CatalogItemModel
|
export type CatalogItem = Prisma.CatalogItemModel
|
||||||
|
/**
|
||||||
|
* Model Opportunity
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export type Opportunity = Prisma.OpportunityModel
|
||||||
/**
|
/**
|
||||||
* Model CredentialType
|
* Model CredentialType
|
||||||
*
|
*
|
||||||
@@ -62,3 +67,13 @@ export type SecureValue = Prisma.SecureValueModel
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export type Credential = Prisma.CredentialModel
|
export type Credential = Prisma.CredentialModel
|
||||||
|
/**
|
||||||
|
* Model GeneratedQuotes
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export type GeneratedQuotes = Prisma.GeneratedQuotesModel
|
||||||
|
/**
|
||||||
|
* Model CwMember
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export type CwMember = Prisma.CwMemberModel
|
||||||
|
|||||||
@@ -69,6 +69,11 @@ export type Company = Prisma.CompanyModel
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export type CatalogItem = Prisma.CatalogItemModel
|
export type CatalogItem = Prisma.CatalogItemModel
|
||||||
|
/**
|
||||||
|
* Model Opportunity
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export type Opportunity = Prisma.OpportunityModel
|
||||||
/**
|
/**
|
||||||
* Model CredentialType
|
* Model CredentialType
|
||||||
*
|
*
|
||||||
@@ -84,3 +89,13 @@ export type SecureValue = Prisma.SecureValueModel
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export type Credential = Prisma.CredentialModel
|
export type Credential = Prisma.CredentialModel
|
||||||
|
/**
|
||||||
|
* Model GeneratedQuotes
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export type GeneratedQuotes = Prisma.GeneratedQuotesModel
|
||||||
|
/**
|
||||||
|
* Model CwMember
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export type CwMember = Prisma.CwMemberModel
|
||||||
|
|||||||
@@ -280,6 +280,23 @@ export type JsonWithAggregatesFilterBase<$PrismaModel = never> = {
|
|||||||
_max?: Prisma.NestedJsonFilter<$PrismaModel>
|
_max?: Prisma.NestedJsonFilter<$PrismaModel>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type BytesFilter<$PrismaModel = never> = {
|
||||||
|
equals?: runtime.Bytes | Prisma.BytesFieldRefInput<$PrismaModel>
|
||||||
|
in?: runtime.Bytes[] | Prisma.ListBytesFieldRefInput<$PrismaModel>
|
||||||
|
notIn?: runtime.Bytes[] | Prisma.ListBytesFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedBytesFilter<$PrismaModel> | runtime.Bytes
|
||||||
|
}
|
||||||
|
|
||||||
|
export type BytesWithAggregatesFilter<$PrismaModel = never> = {
|
||||||
|
equals?: runtime.Bytes | Prisma.BytesFieldRefInput<$PrismaModel>
|
||||||
|
in?: runtime.Bytes[] | Prisma.ListBytesFieldRefInput<$PrismaModel>
|
||||||
|
notIn?: runtime.Bytes[] | Prisma.ListBytesFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedBytesWithAggregatesFilter<$PrismaModel> | runtime.Bytes
|
||||||
|
_count?: Prisma.NestedIntFilter<$PrismaModel>
|
||||||
|
_min?: Prisma.NestedBytesFilter<$PrismaModel>
|
||||||
|
_max?: Prisma.NestedBytesFilter<$PrismaModel>
|
||||||
|
}
|
||||||
|
|
||||||
export type NestedStringFilter<$PrismaModel = never> = {
|
export type NestedStringFilter<$PrismaModel = never> = {
|
||||||
equals?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
equals?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel>
|
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel>
|
||||||
@@ -521,4 +538,21 @@ export type NestedJsonFilterBase<$PrismaModel = never> = {
|
|||||||
not?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter
|
not?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type NestedBytesFilter<$PrismaModel = never> = {
|
||||||
|
equals?: runtime.Bytes | Prisma.BytesFieldRefInput<$PrismaModel>
|
||||||
|
in?: runtime.Bytes[] | Prisma.ListBytesFieldRefInput<$PrismaModel>
|
||||||
|
notIn?: runtime.Bytes[] | Prisma.ListBytesFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedBytesFilter<$PrismaModel> | runtime.Bytes
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NestedBytesWithAggregatesFilter<$PrismaModel = never> = {
|
||||||
|
equals?: runtime.Bytes | Prisma.BytesFieldRefInput<$PrismaModel>
|
||||||
|
in?: runtime.Bytes[] | Prisma.ListBytesFieldRefInput<$PrismaModel>
|
||||||
|
notIn?: runtime.Bytes[] | Prisma.ListBytesFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedBytesWithAggregatesFilter<$PrismaModel> | runtime.Bytes
|
||||||
|
_count?: Prisma.NestedIntFilter<$PrismaModel>
|
||||||
|
_min?: Prisma.NestedBytesFilter<$PrismaModel>
|
||||||
|
_max?: Prisma.NestedBytesFilter<$PrismaModel>
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -390,9 +390,12 @@ export const ModelName = {
|
|||||||
UnifiSite: 'UnifiSite',
|
UnifiSite: 'UnifiSite',
|
||||||
Company: 'Company',
|
Company: 'Company',
|
||||||
CatalogItem: 'CatalogItem',
|
CatalogItem: 'CatalogItem',
|
||||||
|
Opportunity: 'Opportunity',
|
||||||
CredentialType: 'CredentialType',
|
CredentialType: 'CredentialType',
|
||||||
SecureValue: 'SecureValue',
|
SecureValue: 'SecureValue',
|
||||||
Credential: 'Credential'
|
Credential: 'Credential',
|
||||||
|
GeneratedQuotes: 'GeneratedQuotes',
|
||||||
|
CwMember: 'CwMember'
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
export type ModelName = (typeof ModelName)[keyof typeof ModelName]
|
export type ModelName = (typeof ModelName)[keyof typeof ModelName]
|
||||||
@@ -408,7 +411,7 @@ export type TypeMap<ExtArgs extends runtime.Types.Extensions.InternalArgs = runt
|
|||||||
omit: GlobalOmitOptions
|
omit: GlobalOmitOptions
|
||||||
}
|
}
|
||||||
meta: {
|
meta: {
|
||||||
modelProps: "session" | "user" | "role" | "unifiSite" | "company" | "catalogItem" | "credentialType" | "secureValue" | "credential"
|
modelProps: "session" | "user" | "role" | "unifiSite" | "company" | "catalogItem" | "opportunity" | "credentialType" | "secureValue" | "credential" | "generatedQuotes" | "cwMember"
|
||||||
txIsolationLevel: TransactionIsolationLevel
|
txIsolationLevel: TransactionIsolationLevel
|
||||||
}
|
}
|
||||||
model: {
|
model: {
|
||||||
@@ -856,6 +859,80 @@ export type TypeMap<ExtArgs extends runtime.Types.Extensions.InternalArgs = runt
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Opportunity: {
|
||||||
|
payload: Prisma.$OpportunityPayload<ExtArgs>
|
||||||
|
fields: Prisma.OpportunityFieldRefs
|
||||||
|
operations: {
|
||||||
|
findUnique: {
|
||||||
|
args: Prisma.OpportunityFindUniqueArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$OpportunityPayload> | null
|
||||||
|
}
|
||||||
|
findUniqueOrThrow: {
|
||||||
|
args: Prisma.OpportunityFindUniqueOrThrowArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$OpportunityPayload>
|
||||||
|
}
|
||||||
|
findFirst: {
|
||||||
|
args: Prisma.OpportunityFindFirstArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$OpportunityPayload> | null
|
||||||
|
}
|
||||||
|
findFirstOrThrow: {
|
||||||
|
args: Prisma.OpportunityFindFirstOrThrowArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$OpportunityPayload>
|
||||||
|
}
|
||||||
|
findMany: {
|
||||||
|
args: Prisma.OpportunityFindManyArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$OpportunityPayload>[]
|
||||||
|
}
|
||||||
|
create: {
|
||||||
|
args: Prisma.OpportunityCreateArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$OpportunityPayload>
|
||||||
|
}
|
||||||
|
createMany: {
|
||||||
|
args: Prisma.OpportunityCreateManyArgs<ExtArgs>
|
||||||
|
result: BatchPayload
|
||||||
|
}
|
||||||
|
createManyAndReturn: {
|
||||||
|
args: Prisma.OpportunityCreateManyAndReturnArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$OpportunityPayload>[]
|
||||||
|
}
|
||||||
|
delete: {
|
||||||
|
args: Prisma.OpportunityDeleteArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$OpportunityPayload>
|
||||||
|
}
|
||||||
|
update: {
|
||||||
|
args: Prisma.OpportunityUpdateArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$OpportunityPayload>
|
||||||
|
}
|
||||||
|
deleteMany: {
|
||||||
|
args: Prisma.OpportunityDeleteManyArgs<ExtArgs>
|
||||||
|
result: BatchPayload
|
||||||
|
}
|
||||||
|
updateMany: {
|
||||||
|
args: Prisma.OpportunityUpdateManyArgs<ExtArgs>
|
||||||
|
result: BatchPayload
|
||||||
|
}
|
||||||
|
updateManyAndReturn: {
|
||||||
|
args: Prisma.OpportunityUpdateManyAndReturnArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$OpportunityPayload>[]
|
||||||
|
}
|
||||||
|
upsert: {
|
||||||
|
args: Prisma.OpportunityUpsertArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$OpportunityPayload>
|
||||||
|
}
|
||||||
|
aggregate: {
|
||||||
|
args: Prisma.OpportunityAggregateArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.Optional<Prisma.AggregateOpportunity>
|
||||||
|
}
|
||||||
|
groupBy: {
|
||||||
|
args: Prisma.OpportunityGroupByArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.Optional<Prisma.OpportunityGroupByOutputType>[]
|
||||||
|
}
|
||||||
|
count: {
|
||||||
|
args: Prisma.OpportunityCountArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.Optional<Prisma.OpportunityCountAggregateOutputType> | number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
CredentialType: {
|
CredentialType: {
|
||||||
payload: Prisma.$CredentialTypePayload<ExtArgs>
|
payload: Prisma.$CredentialTypePayload<ExtArgs>
|
||||||
fields: Prisma.CredentialTypeFieldRefs
|
fields: Prisma.CredentialTypeFieldRefs
|
||||||
@@ -1078,6 +1155,154 @@ export type TypeMap<ExtArgs extends runtime.Types.Extensions.InternalArgs = runt
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
GeneratedQuotes: {
|
||||||
|
payload: Prisma.$GeneratedQuotesPayload<ExtArgs>
|
||||||
|
fields: Prisma.GeneratedQuotesFieldRefs
|
||||||
|
operations: {
|
||||||
|
findUnique: {
|
||||||
|
args: Prisma.GeneratedQuotesFindUniqueArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$GeneratedQuotesPayload> | null
|
||||||
|
}
|
||||||
|
findUniqueOrThrow: {
|
||||||
|
args: Prisma.GeneratedQuotesFindUniqueOrThrowArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$GeneratedQuotesPayload>
|
||||||
|
}
|
||||||
|
findFirst: {
|
||||||
|
args: Prisma.GeneratedQuotesFindFirstArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$GeneratedQuotesPayload> | null
|
||||||
|
}
|
||||||
|
findFirstOrThrow: {
|
||||||
|
args: Prisma.GeneratedQuotesFindFirstOrThrowArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$GeneratedQuotesPayload>
|
||||||
|
}
|
||||||
|
findMany: {
|
||||||
|
args: Prisma.GeneratedQuotesFindManyArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$GeneratedQuotesPayload>[]
|
||||||
|
}
|
||||||
|
create: {
|
||||||
|
args: Prisma.GeneratedQuotesCreateArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$GeneratedQuotesPayload>
|
||||||
|
}
|
||||||
|
createMany: {
|
||||||
|
args: Prisma.GeneratedQuotesCreateManyArgs<ExtArgs>
|
||||||
|
result: BatchPayload
|
||||||
|
}
|
||||||
|
createManyAndReturn: {
|
||||||
|
args: Prisma.GeneratedQuotesCreateManyAndReturnArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$GeneratedQuotesPayload>[]
|
||||||
|
}
|
||||||
|
delete: {
|
||||||
|
args: Prisma.GeneratedQuotesDeleteArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$GeneratedQuotesPayload>
|
||||||
|
}
|
||||||
|
update: {
|
||||||
|
args: Prisma.GeneratedQuotesUpdateArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$GeneratedQuotesPayload>
|
||||||
|
}
|
||||||
|
deleteMany: {
|
||||||
|
args: Prisma.GeneratedQuotesDeleteManyArgs<ExtArgs>
|
||||||
|
result: BatchPayload
|
||||||
|
}
|
||||||
|
updateMany: {
|
||||||
|
args: Prisma.GeneratedQuotesUpdateManyArgs<ExtArgs>
|
||||||
|
result: BatchPayload
|
||||||
|
}
|
||||||
|
updateManyAndReturn: {
|
||||||
|
args: Prisma.GeneratedQuotesUpdateManyAndReturnArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$GeneratedQuotesPayload>[]
|
||||||
|
}
|
||||||
|
upsert: {
|
||||||
|
args: Prisma.GeneratedQuotesUpsertArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$GeneratedQuotesPayload>
|
||||||
|
}
|
||||||
|
aggregate: {
|
||||||
|
args: Prisma.GeneratedQuotesAggregateArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.Optional<Prisma.AggregateGeneratedQuotes>
|
||||||
|
}
|
||||||
|
groupBy: {
|
||||||
|
args: Prisma.GeneratedQuotesGroupByArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.Optional<Prisma.GeneratedQuotesGroupByOutputType>[]
|
||||||
|
}
|
||||||
|
count: {
|
||||||
|
args: Prisma.GeneratedQuotesCountArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.Optional<Prisma.GeneratedQuotesCountAggregateOutputType> | number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CwMember: {
|
||||||
|
payload: Prisma.$CwMemberPayload<ExtArgs>
|
||||||
|
fields: Prisma.CwMemberFieldRefs
|
||||||
|
operations: {
|
||||||
|
findUnique: {
|
||||||
|
args: Prisma.CwMemberFindUniqueArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$CwMemberPayload> | null
|
||||||
|
}
|
||||||
|
findUniqueOrThrow: {
|
||||||
|
args: Prisma.CwMemberFindUniqueOrThrowArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$CwMemberPayload>
|
||||||
|
}
|
||||||
|
findFirst: {
|
||||||
|
args: Prisma.CwMemberFindFirstArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$CwMemberPayload> | null
|
||||||
|
}
|
||||||
|
findFirstOrThrow: {
|
||||||
|
args: Prisma.CwMemberFindFirstOrThrowArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$CwMemberPayload>
|
||||||
|
}
|
||||||
|
findMany: {
|
||||||
|
args: Prisma.CwMemberFindManyArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$CwMemberPayload>[]
|
||||||
|
}
|
||||||
|
create: {
|
||||||
|
args: Prisma.CwMemberCreateArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$CwMemberPayload>
|
||||||
|
}
|
||||||
|
createMany: {
|
||||||
|
args: Prisma.CwMemberCreateManyArgs<ExtArgs>
|
||||||
|
result: BatchPayload
|
||||||
|
}
|
||||||
|
createManyAndReturn: {
|
||||||
|
args: Prisma.CwMemberCreateManyAndReturnArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$CwMemberPayload>[]
|
||||||
|
}
|
||||||
|
delete: {
|
||||||
|
args: Prisma.CwMemberDeleteArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$CwMemberPayload>
|
||||||
|
}
|
||||||
|
update: {
|
||||||
|
args: Prisma.CwMemberUpdateArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$CwMemberPayload>
|
||||||
|
}
|
||||||
|
deleteMany: {
|
||||||
|
args: Prisma.CwMemberDeleteManyArgs<ExtArgs>
|
||||||
|
result: BatchPayload
|
||||||
|
}
|
||||||
|
updateMany: {
|
||||||
|
args: Prisma.CwMemberUpdateManyArgs<ExtArgs>
|
||||||
|
result: BatchPayload
|
||||||
|
}
|
||||||
|
updateManyAndReturn: {
|
||||||
|
args: Prisma.CwMemberUpdateManyAndReturnArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$CwMemberPayload>[]
|
||||||
|
}
|
||||||
|
upsert: {
|
||||||
|
args: Prisma.CwMemberUpsertArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$CwMemberPayload>
|
||||||
|
}
|
||||||
|
aggregate: {
|
||||||
|
args: Prisma.CwMemberAggregateArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.Optional<Prisma.AggregateCwMember>
|
||||||
|
}
|
||||||
|
groupBy: {
|
||||||
|
args: Prisma.CwMemberGroupByArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.Optional<Prisma.CwMemberGroupByOutputType>[]
|
||||||
|
}
|
||||||
|
count: {
|
||||||
|
args: Prisma.CwMemberCountArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.Optional<Prisma.CwMemberCountAggregateOutputType> | number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} & {
|
} & {
|
||||||
other: {
|
other: {
|
||||||
@@ -1138,6 +1363,7 @@ export const UserScalarFieldEnum = {
|
|||||||
email: 'email',
|
email: 'email',
|
||||||
emailVerified: 'emailVerified',
|
emailVerified: 'emailVerified',
|
||||||
image: 'image',
|
image: 'image',
|
||||||
|
cwIdentifier: 'cwIdentifier',
|
||||||
userId: 'userId',
|
userId: 'userId',
|
||||||
token: 'token',
|
token: 'token',
|
||||||
createdAt: 'createdAt',
|
createdAt: 'createdAt',
|
||||||
@@ -1186,10 +1412,15 @@ export type CompanyScalarFieldEnum = (typeof CompanyScalarFieldEnum)[keyof typeo
|
|||||||
export const CatalogItemScalarFieldEnum = {
|
export const CatalogItemScalarFieldEnum = {
|
||||||
id: 'id',
|
id: 'id',
|
||||||
cwCatalogId: 'cwCatalogId',
|
cwCatalogId: 'cwCatalogId',
|
||||||
|
identifier: 'identifier',
|
||||||
name: 'name',
|
name: 'name',
|
||||||
description: 'description',
|
description: 'description',
|
||||||
customerDescription: 'customerDescription',
|
customerDescription: 'customerDescription',
|
||||||
internalNotes: 'internalNotes',
|
internalNotes: 'internalNotes',
|
||||||
|
category: 'category',
|
||||||
|
categoryCwId: 'categoryCwId',
|
||||||
|
subcategory: 'subcategory',
|
||||||
|
subcategoryCwId: 'subcategoryCwId',
|
||||||
manufacturer: 'manufacturer',
|
manufacturer: 'manufacturer',
|
||||||
manufactureCwId: 'manufactureCwId',
|
manufactureCwId: 'manufactureCwId',
|
||||||
partNumber: 'partNumber',
|
partNumber: 'partNumber',
|
||||||
@@ -1209,6 +1440,61 @@ export const CatalogItemScalarFieldEnum = {
|
|||||||
export type CatalogItemScalarFieldEnum = (typeof CatalogItemScalarFieldEnum)[keyof typeof CatalogItemScalarFieldEnum]
|
export type CatalogItemScalarFieldEnum = (typeof CatalogItemScalarFieldEnum)[keyof typeof CatalogItemScalarFieldEnum]
|
||||||
|
|
||||||
|
|
||||||
|
export const OpportunityScalarFieldEnum = {
|
||||||
|
id: 'id',
|
||||||
|
cwOpportunityId: 'cwOpportunityId',
|
||||||
|
name: 'name',
|
||||||
|
notes: 'notes',
|
||||||
|
typeName: 'typeName',
|
||||||
|
typeCwId: 'typeCwId',
|
||||||
|
stageName: 'stageName',
|
||||||
|
stageCwId: 'stageCwId',
|
||||||
|
statusName: 'statusName',
|
||||||
|
statusCwId: 'statusCwId',
|
||||||
|
priorityName: 'priorityName',
|
||||||
|
priorityCwId: 'priorityCwId',
|
||||||
|
ratingName: 'ratingName',
|
||||||
|
ratingCwId: 'ratingCwId',
|
||||||
|
source: 'source',
|
||||||
|
campaignName: 'campaignName',
|
||||||
|
campaignCwId: 'campaignCwId',
|
||||||
|
primarySalesRepName: 'primarySalesRepName',
|
||||||
|
primarySalesRepIdentifier: 'primarySalesRepIdentifier',
|
||||||
|
primarySalesRepCwId: 'primarySalesRepCwId',
|
||||||
|
secondarySalesRepName: 'secondarySalesRepName',
|
||||||
|
secondarySalesRepIdentifier: 'secondarySalesRepIdentifier',
|
||||||
|
secondarySalesRepCwId: 'secondarySalesRepCwId',
|
||||||
|
companyCwId: 'companyCwId',
|
||||||
|
companyName: 'companyName',
|
||||||
|
contactCwId: 'contactCwId',
|
||||||
|
contactName: 'contactName',
|
||||||
|
siteCwId: 'siteCwId',
|
||||||
|
siteName: 'siteName',
|
||||||
|
customerPO: 'customerPO',
|
||||||
|
totalSalesTax: 'totalSalesTax',
|
||||||
|
probability: 'probability',
|
||||||
|
locationName: 'locationName',
|
||||||
|
locationCwId: 'locationCwId',
|
||||||
|
departmentName: 'departmentName',
|
||||||
|
departmentCwId: 'departmentCwId',
|
||||||
|
expectedCloseDate: 'expectedCloseDate',
|
||||||
|
pipelineChangeDate: 'pipelineChangeDate',
|
||||||
|
dateBecameLead: 'dateBecameLead',
|
||||||
|
closedDate: 'closedDate',
|
||||||
|
closedFlag: 'closedFlag',
|
||||||
|
closedByName: 'closedByName',
|
||||||
|
closedByCwId: 'closedByCwId',
|
||||||
|
companyId: 'companyId',
|
||||||
|
productSequence: 'productSequence',
|
||||||
|
cwLastUpdated: 'cwLastUpdated',
|
||||||
|
cwDateEntered: 'cwDateEntered',
|
||||||
|
createdAt: 'createdAt',
|
||||||
|
updatedAt: 'updatedAt'
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type OpportunityScalarFieldEnum = (typeof OpportunityScalarFieldEnum)[keyof typeof OpportunityScalarFieldEnum]
|
||||||
|
|
||||||
|
|
||||||
export const CredentialTypeScalarFieldEnum = {
|
export const CredentialTypeScalarFieldEnum = {
|
||||||
id: 'id',
|
id: 'id',
|
||||||
name: 'name',
|
name: 'name',
|
||||||
@@ -1250,6 +1536,40 @@ export const CredentialScalarFieldEnum = {
|
|||||||
export type CredentialScalarFieldEnum = (typeof CredentialScalarFieldEnum)[keyof typeof CredentialScalarFieldEnum]
|
export type CredentialScalarFieldEnum = (typeof CredentialScalarFieldEnum)[keyof typeof CredentialScalarFieldEnum]
|
||||||
|
|
||||||
|
|
||||||
|
export const GeneratedQuotesScalarFieldEnum = {
|
||||||
|
id: 'id',
|
||||||
|
quoteRegenData: 'quoteRegenData',
|
||||||
|
quoteRegenParams: 'quoteRegenParams',
|
||||||
|
quoteRegenHash: 'quoteRegenHash',
|
||||||
|
downloads: 'downloads',
|
||||||
|
quoteFile: 'quoteFile',
|
||||||
|
quoteFileName: 'quoteFileName',
|
||||||
|
opportunityId: 'opportunityId',
|
||||||
|
createdById: 'createdById',
|
||||||
|
createdAt: 'createdAt',
|
||||||
|
updatedAt: 'updatedAt'
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type GeneratedQuotesScalarFieldEnum = (typeof GeneratedQuotesScalarFieldEnum)[keyof typeof GeneratedQuotesScalarFieldEnum]
|
||||||
|
|
||||||
|
|
||||||
|
export const CwMemberScalarFieldEnum = {
|
||||||
|
id: 'id',
|
||||||
|
cwMemberId: 'cwMemberId',
|
||||||
|
identifier: 'identifier',
|
||||||
|
firstName: 'firstName',
|
||||||
|
lastName: 'lastName',
|
||||||
|
officeEmail: 'officeEmail',
|
||||||
|
inactiveFlag: 'inactiveFlag',
|
||||||
|
apiKey: 'apiKey',
|
||||||
|
cwLastUpdated: 'cwLastUpdated',
|
||||||
|
createdAt: 'createdAt',
|
||||||
|
updatedAt: 'updatedAt'
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type CwMemberScalarFieldEnum = (typeof CwMemberScalarFieldEnum)[keyof typeof CwMemberScalarFieldEnum]
|
||||||
|
|
||||||
|
|
||||||
export const SortOrder = {
|
export const SortOrder = {
|
||||||
asc: 'asc',
|
asc: 'asc',
|
||||||
desc: 'desc'
|
desc: 'desc'
|
||||||
@@ -1372,6 +1692,20 @@ export type JsonFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'J
|
|||||||
export type EnumQueryModeFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'QueryMode'>
|
export type EnumQueryModeFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'QueryMode'>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reference to a field of type 'Bytes'
|
||||||
|
*/
|
||||||
|
export type BytesFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'Bytes'>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reference to a field of type 'Bytes[]'
|
||||||
|
*/
|
||||||
|
export type ListBytesFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'Bytes[]'>
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Batch Payload for updateMany & deleteMany & createMany
|
* Batch Payload for updateMany & deleteMany & createMany
|
||||||
*/
|
*/
|
||||||
@@ -1473,9 +1807,12 @@ export type GlobalOmitConfig = {
|
|||||||
unifiSite?: Prisma.UnifiSiteOmit
|
unifiSite?: Prisma.UnifiSiteOmit
|
||||||
company?: Prisma.CompanyOmit
|
company?: Prisma.CompanyOmit
|
||||||
catalogItem?: Prisma.CatalogItemOmit
|
catalogItem?: Prisma.CatalogItemOmit
|
||||||
|
opportunity?: Prisma.OpportunityOmit
|
||||||
credentialType?: Prisma.CredentialTypeOmit
|
credentialType?: Prisma.CredentialTypeOmit
|
||||||
secureValue?: Prisma.SecureValueOmit
|
secureValue?: Prisma.SecureValueOmit
|
||||||
credential?: Prisma.CredentialOmit
|
credential?: Prisma.CredentialOmit
|
||||||
|
generatedQuotes?: Prisma.GeneratedQuotesOmit
|
||||||
|
cwMember?: Prisma.CwMemberOmit
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Types for Logging */
|
/* Types for Logging */
|
||||||
|
|||||||
@@ -57,9 +57,12 @@ export const ModelName = {
|
|||||||
UnifiSite: 'UnifiSite',
|
UnifiSite: 'UnifiSite',
|
||||||
Company: 'Company',
|
Company: 'Company',
|
||||||
CatalogItem: 'CatalogItem',
|
CatalogItem: 'CatalogItem',
|
||||||
|
Opportunity: 'Opportunity',
|
||||||
CredentialType: 'CredentialType',
|
CredentialType: 'CredentialType',
|
||||||
SecureValue: 'SecureValue',
|
SecureValue: 'SecureValue',
|
||||||
Credential: 'Credential'
|
Credential: 'Credential',
|
||||||
|
GeneratedQuotes: 'GeneratedQuotes',
|
||||||
|
CwMember: 'CwMember'
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
export type ModelName = (typeof ModelName)[keyof typeof ModelName]
|
export type ModelName = (typeof ModelName)[keyof typeof ModelName]
|
||||||
@@ -99,6 +102,7 @@ export const UserScalarFieldEnum = {
|
|||||||
email: 'email',
|
email: 'email',
|
||||||
emailVerified: 'emailVerified',
|
emailVerified: 'emailVerified',
|
||||||
image: 'image',
|
image: 'image',
|
||||||
|
cwIdentifier: 'cwIdentifier',
|
||||||
userId: 'userId',
|
userId: 'userId',
|
||||||
token: 'token',
|
token: 'token',
|
||||||
createdAt: 'createdAt',
|
createdAt: 'createdAt',
|
||||||
@@ -147,10 +151,15 @@ export type CompanyScalarFieldEnum = (typeof CompanyScalarFieldEnum)[keyof typeo
|
|||||||
export const CatalogItemScalarFieldEnum = {
|
export const CatalogItemScalarFieldEnum = {
|
||||||
id: 'id',
|
id: 'id',
|
||||||
cwCatalogId: 'cwCatalogId',
|
cwCatalogId: 'cwCatalogId',
|
||||||
|
identifier: 'identifier',
|
||||||
name: 'name',
|
name: 'name',
|
||||||
description: 'description',
|
description: 'description',
|
||||||
customerDescription: 'customerDescription',
|
customerDescription: 'customerDescription',
|
||||||
internalNotes: 'internalNotes',
|
internalNotes: 'internalNotes',
|
||||||
|
category: 'category',
|
||||||
|
categoryCwId: 'categoryCwId',
|
||||||
|
subcategory: 'subcategory',
|
||||||
|
subcategoryCwId: 'subcategoryCwId',
|
||||||
manufacturer: 'manufacturer',
|
manufacturer: 'manufacturer',
|
||||||
manufactureCwId: 'manufactureCwId',
|
manufactureCwId: 'manufactureCwId',
|
||||||
partNumber: 'partNumber',
|
partNumber: 'partNumber',
|
||||||
@@ -170,6 +179,61 @@ export const CatalogItemScalarFieldEnum = {
|
|||||||
export type CatalogItemScalarFieldEnum = (typeof CatalogItemScalarFieldEnum)[keyof typeof CatalogItemScalarFieldEnum]
|
export type CatalogItemScalarFieldEnum = (typeof CatalogItemScalarFieldEnum)[keyof typeof CatalogItemScalarFieldEnum]
|
||||||
|
|
||||||
|
|
||||||
|
export const OpportunityScalarFieldEnum = {
|
||||||
|
id: 'id',
|
||||||
|
cwOpportunityId: 'cwOpportunityId',
|
||||||
|
name: 'name',
|
||||||
|
notes: 'notes',
|
||||||
|
typeName: 'typeName',
|
||||||
|
typeCwId: 'typeCwId',
|
||||||
|
stageName: 'stageName',
|
||||||
|
stageCwId: 'stageCwId',
|
||||||
|
statusName: 'statusName',
|
||||||
|
statusCwId: 'statusCwId',
|
||||||
|
priorityName: 'priorityName',
|
||||||
|
priorityCwId: 'priorityCwId',
|
||||||
|
ratingName: 'ratingName',
|
||||||
|
ratingCwId: 'ratingCwId',
|
||||||
|
source: 'source',
|
||||||
|
campaignName: 'campaignName',
|
||||||
|
campaignCwId: 'campaignCwId',
|
||||||
|
primarySalesRepName: 'primarySalesRepName',
|
||||||
|
primarySalesRepIdentifier: 'primarySalesRepIdentifier',
|
||||||
|
primarySalesRepCwId: 'primarySalesRepCwId',
|
||||||
|
secondarySalesRepName: 'secondarySalesRepName',
|
||||||
|
secondarySalesRepIdentifier: 'secondarySalesRepIdentifier',
|
||||||
|
secondarySalesRepCwId: 'secondarySalesRepCwId',
|
||||||
|
companyCwId: 'companyCwId',
|
||||||
|
companyName: 'companyName',
|
||||||
|
contactCwId: 'contactCwId',
|
||||||
|
contactName: 'contactName',
|
||||||
|
siteCwId: 'siteCwId',
|
||||||
|
siteName: 'siteName',
|
||||||
|
customerPO: 'customerPO',
|
||||||
|
totalSalesTax: 'totalSalesTax',
|
||||||
|
probability: 'probability',
|
||||||
|
locationName: 'locationName',
|
||||||
|
locationCwId: 'locationCwId',
|
||||||
|
departmentName: 'departmentName',
|
||||||
|
departmentCwId: 'departmentCwId',
|
||||||
|
expectedCloseDate: 'expectedCloseDate',
|
||||||
|
pipelineChangeDate: 'pipelineChangeDate',
|
||||||
|
dateBecameLead: 'dateBecameLead',
|
||||||
|
closedDate: 'closedDate',
|
||||||
|
closedFlag: 'closedFlag',
|
||||||
|
closedByName: 'closedByName',
|
||||||
|
closedByCwId: 'closedByCwId',
|
||||||
|
companyId: 'companyId',
|
||||||
|
productSequence: 'productSequence',
|
||||||
|
cwLastUpdated: 'cwLastUpdated',
|
||||||
|
cwDateEntered: 'cwDateEntered',
|
||||||
|
createdAt: 'createdAt',
|
||||||
|
updatedAt: 'updatedAt'
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type OpportunityScalarFieldEnum = (typeof OpportunityScalarFieldEnum)[keyof typeof OpportunityScalarFieldEnum]
|
||||||
|
|
||||||
|
|
||||||
export const CredentialTypeScalarFieldEnum = {
|
export const CredentialTypeScalarFieldEnum = {
|
||||||
id: 'id',
|
id: 'id',
|
||||||
name: 'name',
|
name: 'name',
|
||||||
@@ -211,6 +275,40 @@ export const CredentialScalarFieldEnum = {
|
|||||||
export type CredentialScalarFieldEnum = (typeof CredentialScalarFieldEnum)[keyof typeof CredentialScalarFieldEnum]
|
export type CredentialScalarFieldEnum = (typeof CredentialScalarFieldEnum)[keyof typeof CredentialScalarFieldEnum]
|
||||||
|
|
||||||
|
|
||||||
|
export const GeneratedQuotesScalarFieldEnum = {
|
||||||
|
id: 'id',
|
||||||
|
quoteRegenData: 'quoteRegenData',
|
||||||
|
quoteRegenParams: 'quoteRegenParams',
|
||||||
|
quoteRegenHash: 'quoteRegenHash',
|
||||||
|
downloads: 'downloads',
|
||||||
|
quoteFile: 'quoteFile',
|
||||||
|
quoteFileName: 'quoteFileName',
|
||||||
|
opportunityId: 'opportunityId',
|
||||||
|
createdById: 'createdById',
|
||||||
|
createdAt: 'createdAt',
|
||||||
|
updatedAt: 'updatedAt'
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type GeneratedQuotesScalarFieldEnum = (typeof GeneratedQuotesScalarFieldEnum)[keyof typeof GeneratedQuotesScalarFieldEnum]
|
||||||
|
|
||||||
|
|
||||||
|
export const CwMemberScalarFieldEnum = {
|
||||||
|
id: 'id',
|
||||||
|
cwMemberId: 'cwMemberId',
|
||||||
|
identifier: 'identifier',
|
||||||
|
firstName: 'firstName',
|
||||||
|
lastName: 'lastName',
|
||||||
|
officeEmail: 'officeEmail',
|
||||||
|
inactiveFlag: 'inactiveFlag',
|
||||||
|
apiKey: 'apiKey',
|
||||||
|
cwLastUpdated: 'cwLastUpdated',
|
||||||
|
createdAt: 'createdAt',
|
||||||
|
updatedAt: 'updatedAt'
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type CwMemberScalarFieldEnum = (typeof CwMemberScalarFieldEnum)[keyof typeof CwMemberScalarFieldEnum]
|
||||||
|
|
||||||
|
|
||||||
export const SortOrder = {
|
export const SortOrder = {
|
||||||
asc: 'asc',
|
asc: 'asc',
|
||||||
desc: 'desc'
|
desc: 'desc'
|
||||||
|
|||||||
@@ -14,7 +14,10 @@ export type * from './models/Role.ts'
|
|||||||
export type * from './models/UnifiSite.ts'
|
export type * from './models/UnifiSite.ts'
|
||||||
export type * from './models/Company.ts'
|
export type * from './models/Company.ts'
|
||||||
export type * from './models/CatalogItem.ts'
|
export type * from './models/CatalogItem.ts'
|
||||||
|
export type * from './models/Opportunity.ts'
|
||||||
export type * from './models/CredentialType.ts'
|
export type * from './models/CredentialType.ts'
|
||||||
export type * from './models/SecureValue.ts'
|
export type * from './models/SecureValue.ts'
|
||||||
export type * from './models/Credential.ts'
|
export type * from './models/Credential.ts'
|
||||||
|
export type * from './models/GeneratedQuotes.ts'
|
||||||
|
export type * from './models/CwMember.ts'
|
||||||
export type * from './commonInputTypes.ts'
|
export type * from './commonInputTypes.ts'
|
||||||
@@ -28,6 +28,8 @@ export type AggregateCatalogItem = {
|
|||||||
|
|
||||||
export type CatalogItemAvgAggregateOutputType = {
|
export type CatalogItemAvgAggregateOutputType = {
|
||||||
cwCatalogId: number | null
|
cwCatalogId: number | null
|
||||||
|
categoryCwId: number | null
|
||||||
|
subcategoryCwId: number | null
|
||||||
manufactureCwId: number | null
|
manufactureCwId: number | null
|
||||||
vendorCwId: number | null
|
vendorCwId: number | null
|
||||||
price: number | null
|
price: number | null
|
||||||
@@ -37,6 +39,8 @@ export type CatalogItemAvgAggregateOutputType = {
|
|||||||
|
|
||||||
export type CatalogItemSumAggregateOutputType = {
|
export type CatalogItemSumAggregateOutputType = {
|
||||||
cwCatalogId: number | null
|
cwCatalogId: number | null
|
||||||
|
categoryCwId: number | null
|
||||||
|
subcategoryCwId: number | null
|
||||||
manufactureCwId: number | null
|
manufactureCwId: number | null
|
||||||
vendorCwId: number | null
|
vendorCwId: number | null
|
||||||
price: number | null
|
price: number | null
|
||||||
@@ -47,10 +51,15 @@ export type CatalogItemSumAggregateOutputType = {
|
|||||||
export type CatalogItemMinAggregateOutputType = {
|
export type CatalogItemMinAggregateOutputType = {
|
||||||
id: string | null
|
id: string | null
|
||||||
cwCatalogId: number | null
|
cwCatalogId: number | null
|
||||||
|
identifier: string | null
|
||||||
name: string | null
|
name: string | null
|
||||||
description: string | null
|
description: string | null
|
||||||
customerDescription: string | null
|
customerDescription: string | null
|
||||||
internalNotes: string | null
|
internalNotes: string | null
|
||||||
|
category: string | null
|
||||||
|
categoryCwId: number | null
|
||||||
|
subcategory: string | null
|
||||||
|
subcategoryCwId: number | null
|
||||||
manufacturer: string | null
|
manufacturer: string | null
|
||||||
manufactureCwId: number | null
|
manufactureCwId: number | null
|
||||||
partNumber: string | null
|
partNumber: string | null
|
||||||
@@ -70,10 +79,15 @@ export type CatalogItemMinAggregateOutputType = {
|
|||||||
export type CatalogItemMaxAggregateOutputType = {
|
export type CatalogItemMaxAggregateOutputType = {
|
||||||
id: string | null
|
id: string | null
|
||||||
cwCatalogId: number | null
|
cwCatalogId: number | null
|
||||||
|
identifier: string | null
|
||||||
name: string | null
|
name: string | null
|
||||||
description: string | null
|
description: string | null
|
||||||
customerDescription: string | null
|
customerDescription: string | null
|
||||||
internalNotes: string | null
|
internalNotes: string | null
|
||||||
|
category: string | null
|
||||||
|
categoryCwId: number | null
|
||||||
|
subcategory: string | null
|
||||||
|
subcategoryCwId: number | null
|
||||||
manufacturer: string | null
|
manufacturer: string | null
|
||||||
manufactureCwId: number | null
|
manufactureCwId: number | null
|
||||||
partNumber: string | null
|
partNumber: string | null
|
||||||
@@ -93,10 +107,15 @@ export type CatalogItemMaxAggregateOutputType = {
|
|||||||
export type CatalogItemCountAggregateOutputType = {
|
export type CatalogItemCountAggregateOutputType = {
|
||||||
id: number
|
id: number
|
||||||
cwCatalogId: number
|
cwCatalogId: number
|
||||||
|
identifier: number
|
||||||
name: number
|
name: number
|
||||||
description: number
|
description: number
|
||||||
customerDescription: number
|
customerDescription: number
|
||||||
internalNotes: number
|
internalNotes: number
|
||||||
|
category: number
|
||||||
|
categoryCwId: number
|
||||||
|
subcategory: number
|
||||||
|
subcategoryCwId: number
|
||||||
manufacturer: number
|
manufacturer: number
|
||||||
manufactureCwId: number
|
manufactureCwId: number
|
||||||
partNumber: number
|
partNumber: number
|
||||||
@@ -117,6 +136,8 @@ export type CatalogItemCountAggregateOutputType = {
|
|||||||
|
|
||||||
export type CatalogItemAvgAggregateInputType = {
|
export type CatalogItemAvgAggregateInputType = {
|
||||||
cwCatalogId?: true
|
cwCatalogId?: true
|
||||||
|
categoryCwId?: true
|
||||||
|
subcategoryCwId?: true
|
||||||
manufactureCwId?: true
|
manufactureCwId?: true
|
||||||
vendorCwId?: true
|
vendorCwId?: true
|
||||||
price?: true
|
price?: true
|
||||||
@@ -126,6 +147,8 @@ export type CatalogItemAvgAggregateInputType = {
|
|||||||
|
|
||||||
export type CatalogItemSumAggregateInputType = {
|
export type CatalogItemSumAggregateInputType = {
|
||||||
cwCatalogId?: true
|
cwCatalogId?: true
|
||||||
|
categoryCwId?: true
|
||||||
|
subcategoryCwId?: true
|
||||||
manufactureCwId?: true
|
manufactureCwId?: true
|
||||||
vendorCwId?: true
|
vendorCwId?: true
|
||||||
price?: true
|
price?: true
|
||||||
@@ -136,10 +159,15 @@ export type CatalogItemSumAggregateInputType = {
|
|||||||
export type CatalogItemMinAggregateInputType = {
|
export type CatalogItemMinAggregateInputType = {
|
||||||
id?: true
|
id?: true
|
||||||
cwCatalogId?: true
|
cwCatalogId?: true
|
||||||
|
identifier?: true
|
||||||
name?: true
|
name?: true
|
||||||
description?: true
|
description?: true
|
||||||
customerDescription?: true
|
customerDescription?: true
|
||||||
internalNotes?: true
|
internalNotes?: true
|
||||||
|
category?: true
|
||||||
|
categoryCwId?: true
|
||||||
|
subcategory?: true
|
||||||
|
subcategoryCwId?: true
|
||||||
manufacturer?: true
|
manufacturer?: true
|
||||||
manufactureCwId?: true
|
manufactureCwId?: true
|
||||||
partNumber?: true
|
partNumber?: true
|
||||||
@@ -159,10 +187,15 @@ export type CatalogItemMinAggregateInputType = {
|
|||||||
export type CatalogItemMaxAggregateInputType = {
|
export type CatalogItemMaxAggregateInputType = {
|
||||||
id?: true
|
id?: true
|
||||||
cwCatalogId?: true
|
cwCatalogId?: true
|
||||||
|
identifier?: true
|
||||||
name?: true
|
name?: true
|
||||||
description?: true
|
description?: true
|
||||||
customerDescription?: true
|
customerDescription?: true
|
||||||
internalNotes?: true
|
internalNotes?: true
|
||||||
|
category?: true
|
||||||
|
categoryCwId?: true
|
||||||
|
subcategory?: true
|
||||||
|
subcategoryCwId?: true
|
||||||
manufacturer?: true
|
manufacturer?: true
|
||||||
manufactureCwId?: true
|
manufactureCwId?: true
|
||||||
partNumber?: true
|
partNumber?: true
|
||||||
@@ -182,10 +215,15 @@ export type CatalogItemMaxAggregateInputType = {
|
|||||||
export type CatalogItemCountAggregateInputType = {
|
export type CatalogItemCountAggregateInputType = {
|
||||||
id?: true
|
id?: true
|
||||||
cwCatalogId?: true
|
cwCatalogId?: true
|
||||||
|
identifier?: true
|
||||||
name?: true
|
name?: true
|
||||||
description?: true
|
description?: true
|
||||||
customerDescription?: true
|
customerDescription?: true
|
||||||
internalNotes?: true
|
internalNotes?: true
|
||||||
|
category?: true
|
||||||
|
categoryCwId?: true
|
||||||
|
subcategory?: true
|
||||||
|
subcategoryCwId?: true
|
||||||
manufacturer?: true
|
manufacturer?: true
|
||||||
manufactureCwId?: true
|
manufactureCwId?: true
|
||||||
partNumber?: true
|
partNumber?: true
|
||||||
@@ -292,10 +330,15 @@ export type CatalogItemGroupByArgs<ExtArgs extends runtime.Types.Extensions.Inte
|
|||||||
export type CatalogItemGroupByOutputType = {
|
export type CatalogItemGroupByOutputType = {
|
||||||
id: string
|
id: string
|
||||||
cwCatalogId: number
|
cwCatalogId: number
|
||||||
|
identifier: string | null
|
||||||
name: string
|
name: string
|
||||||
description: string | null
|
description: string | null
|
||||||
customerDescription: string | null
|
customerDescription: string | null
|
||||||
internalNotes: string | null
|
internalNotes: string | null
|
||||||
|
category: string | null
|
||||||
|
categoryCwId: number | null
|
||||||
|
subcategory: string | null
|
||||||
|
subcategoryCwId: number | null
|
||||||
manufacturer: string | null
|
manufacturer: string | null
|
||||||
manufactureCwId: number | null
|
manufactureCwId: number | null
|
||||||
partNumber: string | null
|
partNumber: string | null
|
||||||
@@ -338,10 +381,15 @@ export type CatalogItemWhereInput = {
|
|||||||
NOT?: Prisma.CatalogItemWhereInput | Prisma.CatalogItemWhereInput[]
|
NOT?: Prisma.CatalogItemWhereInput | Prisma.CatalogItemWhereInput[]
|
||||||
id?: Prisma.StringFilter<"CatalogItem"> | string
|
id?: Prisma.StringFilter<"CatalogItem"> | string
|
||||||
cwCatalogId?: Prisma.IntFilter<"CatalogItem"> | number
|
cwCatalogId?: Prisma.IntFilter<"CatalogItem"> | number
|
||||||
|
identifier?: Prisma.StringNullableFilter<"CatalogItem"> | string | null
|
||||||
name?: Prisma.StringFilter<"CatalogItem"> | string
|
name?: Prisma.StringFilter<"CatalogItem"> | string
|
||||||
description?: Prisma.StringNullableFilter<"CatalogItem"> | string | null
|
description?: Prisma.StringNullableFilter<"CatalogItem"> | string | null
|
||||||
customerDescription?: Prisma.StringNullableFilter<"CatalogItem"> | string | null
|
customerDescription?: Prisma.StringNullableFilter<"CatalogItem"> | string | null
|
||||||
internalNotes?: Prisma.StringNullableFilter<"CatalogItem"> | string | null
|
internalNotes?: Prisma.StringNullableFilter<"CatalogItem"> | string | null
|
||||||
|
category?: Prisma.StringNullableFilter<"CatalogItem"> | string | null
|
||||||
|
categoryCwId?: Prisma.IntNullableFilter<"CatalogItem"> | number | null
|
||||||
|
subcategory?: Prisma.StringNullableFilter<"CatalogItem"> | string | null
|
||||||
|
subcategoryCwId?: Prisma.IntNullableFilter<"CatalogItem"> | number | null
|
||||||
manufacturer?: Prisma.StringNullableFilter<"CatalogItem"> | string | null
|
manufacturer?: Prisma.StringNullableFilter<"CatalogItem"> | string | null
|
||||||
manufactureCwId?: Prisma.IntNullableFilter<"CatalogItem"> | number | null
|
manufactureCwId?: Prisma.IntNullableFilter<"CatalogItem"> | number | null
|
||||||
partNumber?: Prisma.StringNullableFilter<"CatalogItem"> | string | null
|
partNumber?: Prisma.StringNullableFilter<"CatalogItem"> | string | null
|
||||||
@@ -363,10 +411,15 @@ export type CatalogItemWhereInput = {
|
|||||||
export type CatalogItemOrderByWithRelationInput = {
|
export type CatalogItemOrderByWithRelationInput = {
|
||||||
id?: Prisma.SortOrder
|
id?: Prisma.SortOrder
|
||||||
cwCatalogId?: Prisma.SortOrder
|
cwCatalogId?: Prisma.SortOrder
|
||||||
|
identifier?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||||
name?: Prisma.SortOrder
|
name?: Prisma.SortOrder
|
||||||
description?: Prisma.SortOrderInput | Prisma.SortOrder
|
description?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||||
customerDescription?: Prisma.SortOrderInput | Prisma.SortOrder
|
customerDescription?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||||
internalNotes?: Prisma.SortOrderInput | Prisma.SortOrder
|
internalNotes?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||||
|
category?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||||
|
categoryCwId?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||||
|
subcategory?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||||
|
subcategoryCwId?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||||
manufacturer?: Prisma.SortOrderInput | Prisma.SortOrder
|
manufacturer?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||||
manufactureCwId?: Prisma.SortOrderInput | Prisma.SortOrder
|
manufactureCwId?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||||
partNumber?: Prisma.SortOrderInput | Prisma.SortOrder
|
partNumber?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||||
@@ -388,6 +441,7 @@ export type CatalogItemOrderByWithRelationInput = {
|
|||||||
export type CatalogItemWhereUniqueInput = Prisma.AtLeast<{
|
export type CatalogItemWhereUniqueInput = Prisma.AtLeast<{
|
||||||
id?: string
|
id?: string
|
||||||
cwCatalogId?: number
|
cwCatalogId?: number
|
||||||
|
identifier?: string
|
||||||
AND?: Prisma.CatalogItemWhereInput | Prisma.CatalogItemWhereInput[]
|
AND?: Prisma.CatalogItemWhereInput | Prisma.CatalogItemWhereInput[]
|
||||||
OR?: Prisma.CatalogItemWhereInput[]
|
OR?: Prisma.CatalogItemWhereInput[]
|
||||||
NOT?: Prisma.CatalogItemWhereInput | Prisma.CatalogItemWhereInput[]
|
NOT?: Prisma.CatalogItemWhereInput | Prisma.CatalogItemWhereInput[]
|
||||||
@@ -395,6 +449,10 @@ export type CatalogItemWhereUniqueInput = Prisma.AtLeast<{
|
|||||||
description?: Prisma.StringNullableFilter<"CatalogItem"> | string | null
|
description?: Prisma.StringNullableFilter<"CatalogItem"> | string | null
|
||||||
customerDescription?: Prisma.StringNullableFilter<"CatalogItem"> | string | null
|
customerDescription?: Prisma.StringNullableFilter<"CatalogItem"> | string | null
|
||||||
internalNotes?: Prisma.StringNullableFilter<"CatalogItem"> | string | null
|
internalNotes?: Prisma.StringNullableFilter<"CatalogItem"> | string | null
|
||||||
|
category?: Prisma.StringNullableFilter<"CatalogItem"> | string | null
|
||||||
|
categoryCwId?: Prisma.IntNullableFilter<"CatalogItem"> | number | null
|
||||||
|
subcategory?: Prisma.StringNullableFilter<"CatalogItem"> | string | null
|
||||||
|
subcategoryCwId?: Prisma.IntNullableFilter<"CatalogItem"> | number | null
|
||||||
manufacturer?: Prisma.StringNullableFilter<"CatalogItem"> | string | null
|
manufacturer?: Prisma.StringNullableFilter<"CatalogItem"> | string | null
|
||||||
manufactureCwId?: Prisma.IntNullableFilter<"CatalogItem"> | number | null
|
manufactureCwId?: Prisma.IntNullableFilter<"CatalogItem"> | number | null
|
||||||
partNumber?: Prisma.StringNullableFilter<"CatalogItem"> | string | null
|
partNumber?: Prisma.StringNullableFilter<"CatalogItem"> | string | null
|
||||||
@@ -411,15 +469,20 @@ export type CatalogItemWhereUniqueInput = Prisma.AtLeast<{
|
|||||||
updatedAt?: Prisma.DateTimeFilter<"CatalogItem"> | Date | string
|
updatedAt?: Prisma.DateTimeFilter<"CatalogItem"> | Date | string
|
||||||
linkedItems?: Prisma.CatalogItemListRelationFilter
|
linkedItems?: Prisma.CatalogItemListRelationFilter
|
||||||
linkedTo?: Prisma.CatalogItemListRelationFilter
|
linkedTo?: Prisma.CatalogItemListRelationFilter
|
||||||
}, "id" | "cwCatalogId">
|
}, "id" | "cwCatalogId" | "identifier">
|
||||||
|
|
||||||
export type CatalogItemOrderByWithAggregationInput = {
|
export type CatalogItemOrderByWithAggregationInput = {
|
||||||
id?: Prisma.SortOrder
|
id?: Prisma.SortOrder
|
||||||
cwCatalogId?: Prisma.SortOrder
|
cwCatalogId?: Prisma.SortOrder
|
||||||
|
identifier?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||||
name?: Prisma.SortOrder
|
name?: Prisma.SortOrder
|
||||||
description?: Prisma.SortOrderInput | Prisma.SortOrder
|
description?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||||
customerDescription?: Prisma.SortOrderInput | Prisma.SortOrder
|
customerDescription?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||||
internalNotes?: Prisma.SortOrderInput | Prisma.SortOrder
|
internalNotes?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||||
|
category?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||||
|
categoryCwId?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||||
|
subcategory?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||||
|
subcategoryCwId?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||||
manufacturer?: Prisma.SortOrderInput | Prisma.SortOrder
|
manufacturer?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||||
manufactureCwId?: Prisma.SortOrderInput | Prisma.SortOrder
|
manufactureCwId?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||||
partNumber?: Prisma.SortOrderInput | Prisma.SortOrder
|
partNumber?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||||
@@ -447,10 +510,15 @@ export type CatalogItemScalarWhereWithAggregatesInput = {
|
|||||||
NOT?: Prisma.CatalogItemScalarWhereWithAggregatesInput | Prisma.CatalogItemScalarWhereWithAggregatesInput[]
|
NOT?: Prisma.CatalogItemScalarWhereWithAggregatesInput | Prisma.CatalogItemScalarWhereWithAggregatesInput[]
|
||||||
id?: Prisma.StringWithAggregatesFilter<"CatalogItem"> | string
|
id?: Prisma.StringWithAggregatesFilter<"CatalogItem"> | string
|
||||||
cwCatalogId?: Prisma.IntWithAggregatesFilter<"CatalogItem"> | number
|
cwCatalogId?: Prisma.IntWithAggregatesFilter<"CatalogItem"> | number
|
||||||
|
identifier?: Prisma.StringNullableWithAggregatesFilter<"CatalogItem"> | string | null
|
||||||
name?: Prisma.StringWithAggregatesFilter<"CatalogItem"> | string
|
name?: Prisma.StringWithAggregatesFilter<"CatalogItem"> | string
|
||||||
description?: Prisma.StringNullableWithAggregatesFilter<"CatalogItem"> | string | null
|
description?: Prisma.StringNullableWithAggregatesFilter<"CatalogItem"> | string | null
|
||||||
customerDescription?: Prisma.StringNullableWithAggregatesFilter<"CatalogItem"> | string | null
|
customerDescription?: Prisma.StringNullableWithAggregatesFilter<"CatalogItem"> | string | null
|
||||||
internalNotes?: Prisma.StringNullableWithAggregatesFilter<"CatalogItem"> | string | null
|
internalNotes?: Prisma.StringNullableWithAggregatesFilter<"CatalogItem"> | string | null
|
||||||
|
category?: Prisma.StringNullableWithAggregatesFilter<"CatalogItem"> | string | null
|
||||||
|
categoryCwId?: Prisma.IntNullableWithAggregatesFilter<"CatalogItem"> | number | null
|
||||||
|
subcategory?: Prisma.StringNullableWithAggregatesFilter<"CatalogItem"> | string | null
|
||||||
|
subcategoryCwId?: Prisma.IntNullableWithAggregatesFilter<"CatalogItem"> | number | null
|
||||||
manufacturer?: Prisma.StringNullableWithAggregatesFilter<"CatalogItem"> | string | null
|
manufacturer?: Prisma.StringNullableWithAggregatesFilter<"CatalogItem"> | string | null
|
||||||
manufactureCwId?: Prisma.IntNullableWithAggregatesFilter<"CatalogItem"> | number | null
|
manufactureCwId?: Prisma.IntNullableWithAggregatesFilter<"CatalogItem"> | number | null
|
||||||
partNumber?: Prisma.StringNullableWithAggregatesFilter<"CatalogItem"> | string | null
|
partNumber?: Prisma.StringNullableWithAggregatesFilter<"CatalogItem"> | string | null
|
||||||
@@ -470,10 +538,15 @@ export type CatalogItemScalarWhereWithAggregatesInput = {
|
|||||||
export type CatalogItemCreateInput = {
|
export type CatalogItemCreateInput = {
|
||||||
id?: string
|
id?: string
|
||||||
cwCatalogId: number
|
cwCatalogId: number
|
||||||
|
identifier?: string | null
|
||||||
name: string
|
name: string
|
||||||
description?: string | null
|
description?: string | null
|
||||||
customerDescription?: string | null
|
customerDescription?: string | null
|
||||||
internalNotes?: string | null
|
internalNotes?: string | null
|
||||||
|
category?: string | null
|
||||||
|
categoryCwId?: number | null
|
||||||
|
subcategory?: string | null
|
||||||
|
subcategoryCwId?: number | null
|
||||||
manufacturer?: string | null
|
manufacturer?: string | null
|
||||||
manufactureCwId?: number | null
|
manufactureCwId?: number | null
|
||||||
partNumber?: string | null
|
partNumber?: string | null
|
||||||
@@ -495,10 +568,15 @@ export type CatalogItemCreateInput = {
|
|||||||
export type CatalogItemUncheckedCreateInput = {
|
export type CatalogItemUncheckedCreateInput = {
|
||||||
id?: string
|
id?: string
|
||||||
cwCatalogId: number
|
cwCatalogId: number
|
||||||
|
identifier?: string | null
|
||||||
name: string
|
name: string
|
||||||
description?: string | null
|
description?: string | null
|
||||||
customerDescription?: string | null
|
customerDescription?: string | null
|
||||||
internalNotes?: string | null
|
internalNotes?: string | null
|
||||||
|
category?: string | null
|
||||||
|
categoryCwId?: number | null
|
||||||
|
subcategory?: string | null
|
||||||
|
subcategoryCwId?: number | null
|
||||||
manufacturer?: string | null
|
manufacturer?: string | null
|
||||||
manufactureCwId?: number | null
|
manufactureCwId?: number | null
|
||||||
partNumber?: string | null
|
partNumber?: string | null
|
||||||
@@ -520,10 +598,15 @@ export type CatalogItemUncheckedCreateInput = {
|
|||||||
export type CatalogItemUpdateInput = {
|
export type CatalogItemUpdateInput = {
|
||||||
id?: Prisma.StringFieldUpdateOperationsInput | string
|
id?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
cwCatalogId?: Prisma.IntFieldUpdateOperationsInput | number
|
cwCatalogId?: Prisma.IntFieldUpdateOperationsInput | number
|
||||||
|
identifier?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
customerDescription?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
customerDescription?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
internalNotes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
internalNotes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
category?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
categoryCwId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
|
subcategory?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
subcategoryCwId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
manufacturer?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
manufacturer?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
manufactureCwId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
manufactureCwId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
partNumber?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
partNumber?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
@@ -545,10 +628,15 @@ export type CatalogItemUpdateInput = {
|
|||||||
export type CatalogItemUncheckedUpdateInput = {
|
export type CatalogItemUncheckedUpdateInput = {
|
||||||
id?: Prisma.StringFieldUpdateOperationsInput | string
|
id?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
cwCatalogId?: Prisma.IntFieldUpdateOperationsInput | number
|
cwCatalogId?: Prisma.IntFieldUpdateOperationsInput | number
|
||||||
|
identifier?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
customerDescription?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
customerDescription?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
internalNotes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
internalNotes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
category?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
categoryCwId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
|
subcategory?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
subcategoryCwId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
manufacturer?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
manufacturer?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
manufactureCwId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
manufactureCwId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
partNumber?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
partNumber?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
@@ -570,10 +658,15 @@ export type CatalogItemUncheckedUpdateInput = {
|
|||||||
export type CatalogItemCreateManyInput = {
|
export type CatalogItemCreateManyInput = {
|
||||||
id?: string
|
id?: string
|
||||||
cwCatalogId: number
|
cwCatalogId: number
|
||||||
|
identifier?: string | null
|
||||||
name: string
|
name: string
|
||||||
description?: string | null
|
description?: string | null
|
||||||
customerDescription?: string | null
|
customerDescription?: string | null
|
||||||
internalNotes?: string | null
|
internalNotes?: string | null
|
||||||
|
category?: string | null
|
||||||
|
categoryCwId?: number | null
|
||||||
|
subcategory?: string | null
|
||||||
|
subcategoryCwId?: number | null
|
||||||
manufacturer?: string | null
|
manufacturer?: string | null
|
||||||
manufactureCwId?: number | null
|
manufactureCwId?: number | null
|
||||||
partNumber?: string | null
|
partNumber?: string | null
|
||||||
@@ -593,10 +686,15 @@ export type CatalogItemCreateManyInput = {
|
|||||||
export type CatalogItemUpdateManyMutationInput = {
|
export type CatalogItemUpdateManyMutationInput = {
|
||||||
id?: Prisma.StringFieldUpdateOperationsInput | string
|
id?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
cwCatalogId?: Prisma.IntFieldUpdateOperationsInput | number
|
cwCatalogId?: Prisma.IntFieldUpdateOperationsInput | number
|
||||||
|
identifier?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
customerDescription?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
customerDescription?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
internalNotes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
internalNotes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
category?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
categoryCwId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
|
subcategory?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
subcategoryCwId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
manufacturer?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
manufacturer?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
manufactureCwId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
manufactureCwId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
partNumber?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
partNumber?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
@@ -616,10 +714,15 @@ export type CatalogItemUpdateManyMutationInput = {
|
|||||||
export type CatalogItemUncheckedUpdateManyInput = {
|
export type CatalogItemUncheckedUpdateManyInput = {
|
||||||
id?: Prisma.StringFieldUpdateOperationsInput | string
|
id?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
cwCatalogId?: Prisma.IntFieldUpdateOperationsInput | number
|
cwCatalogId?: Prisma.IntFieldUpdateOperationsInput | number
|
||||||
|
identifier?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
customerDescription?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
customerDescription?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
internalNotes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
internalNotes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
category?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
categoryCwId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
|
subcategory?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
subcategoryCwId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
manufacturer?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
manufacturer?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
manufactureCwId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
manufactureCwId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
partNumber?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
partNumber?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
@@ -649,10 +752,15 @@ export type CatalogItemOrderByRelationAggregateInput = {
|
|||||||
export type CatalogItemCountOrderByAggregateInput = {
|
export type CatalogItemCountOrderByAggregateInput = {
|
||||||
id?: Prisma.SortOrder
|
id?: Prisma.SortOrder
|
||||||
cwCatalogId?: Prisma.SortOrder
|
cwCatalogId?: Prisma.SortOrder
|
||||||
|
identifier?: Prisma.SortOrder
|
||||||
name?: Prisma.SortOrder
|
name?: Prisma.SortOrder
|
||||||
description?: Prisma.SortOrder
|
description?: Prisma.SortOrder
|
||||||
customerDescription?: Prisma.SortOrder
|
customerDescription?: Prisma.SortOrder
|
||||||
internalNotes?: Prisma.SortOrder
|
internalNotes?: Prisma.SortOrder
|
||||||
|
category?: Prisma.SortOrder
|
||||||
|
categoryCwId?: Prisma.SortOrder
|
||||||
|
subcategory?: Prisma.SortOrder
|
||||||
|
subcategoryCwId?: Prisma.SortOrder
|
||||||
manufacturer?: Prisma.SortOrder
|
manufacturer?: Prisma.SortOrder
|
||||||
manufactureCwId?: Prisma.SortOrder
|
manufactureCwId?: Prisma.SortOrder
|
||||||
partNumber?: Prisma.SortOrder
|
partNumber?: Prisma.SortOrder
|
||||||
@@ -671,6 +779,8 @@ export type CatalogItemCountOrderByAggregateInput = {
|
|||||||
|
|
||||||
export type CatalogItemAvgOrderByAggregateInput = {
|
export type CatalogItemAvgOrderByAggregateInput = {
|
||||||
cwCatalogId?: Prisma.SortOrder
|
cwCatalogId?: Prisma.SortOrder
|
||||||
|
categoryCwId?: Prisma.SortOrder
|
||||||
|
subcategoryCwId?: Prisma.SortOrder
|
||||||
manufactureCwId?: Prisma.SortOrder
|
manufactureCwId?: Prisma.SortOrder
|
||||||
vendorCwId?: Prisma.SortOrder
|
vendorCwId?: Prisma.SortOrder
|
||||||
price?: Prisma.SortOrder
|
price?: Prisma.SortOrder
|
||||||
@@ -681,10 +791,15 @@ export type CatalogItemAvgOrderByAggregateInput = {
|
|||||||
export type CatalogItemMaxOrderByAggregateInput = {
|
export type CatalogItemMaxOrderByAggregateInput = {
|
||||||
id?: Prisma.SortOrder
|
id?: Prisma.SortOrder
|
||||||
cwCatalogId?: Prisma.SortOrder
|
cwCatalogId?: Prisma.SortOrder
|
||||||
|
identifier?: Prisma.SortOrder
|
||||||
name?: Prisma.SortOrder
|
name?: Prisma.SortOrder
|
||||||
description?: Prisma.SortOrder
|
description?: Prisma.SortOrder
|
||||||
customerDescription?: Prisma.SortOrder
|
customerDescription?: Prisma.SortOrder
|
||||||
internalNotes?: Prisma.SortOrder
|
internalNotes?: Prisma.SortOrder
|
||||||
|
category?: Prisma.SortOrder
|
||||||
|
categoryCwId?: Prisma.SortOrder
|
||||||
|
subcategory?: Prisma.SortOrder
|
||||||
|
subcategoryCwId?: Prisma.SortOrder
|
||||||
manufacturer?: Prisma.SortOrder
|
manufacturer?: Prisma.SortOrder
|
||||||
manufactureCwId?: Prisma.SortOrder
|
manufactureCwId?: Prisma.SortOrder
|
||||||
partNumber?: Prisma.SortOrder
|
partNumber?: Prisma.SortOrder
|
||||||
@@ -704,10 +819,15 @@ export type CatalogItemMaxOrderByAggregateInput = {
|
|||||||
export type CatalogItemMinOrderByAggregateInput = {
|
export type CatalogItemMinOrderByAggregateInput = {
|
||||||
id?: Prisma.SortOrder
|
id?: Prisma.SortOrder
|
||||||
cwCatalogId?: Prisma.SortOrder
|
cwCatalogId?: Prisma.SortOrder
|
||||||
|
identifier?: Prisma.SortOrder
|
||||||
name?: Prisma.SortOrder
|
name?: Prisma.SortOrder
|
||||||
description?: Prisma.SortOrder
|
description?: Prisma.SortOrder
|
||||||
customerDescription?: Prisma.SortOrder
|
customerDescription?: Prisma.SortOrder
|
||||||
internalNotes?: Prisma.SortOrder
|
internalNotes?: Prisma.SortOrder
|
||||||
|
category?: Prisma.SortOrder
|
||||||
|
categoryCwId?: Prisma.SortOrder
|
||||||
|
subcategory?: Prisma.SortOrder
|
||||||
|
subcategoryCwId?: Prisma.SortOrder
|
||||||
manufacturer?: Prisma.SortOrder
|
manufacturer?: Prisma.SortOrder
|
||||||
manufactureCwId?: Prisma.SortOrder
|
manufactureCwId?: Prisma.SortOrder
|
||||||
partNumber?: Prisma.SortOrder
|
partNumber?: Prisma.SortOrder
|
||||||
@@ -726,6 +846,8 @@ export type CatalogItemMinOrderByAggregateInput = {
|
|||||||
|
|
||||||
export type CatalogItemSumOrderByAggregateInput = {
|
export type CatalogItemSumOrderByAggregateInput = {
|
||||||
cwCatalogId?: Prisma.SortOrder
|
cwCatalogId?: Prisma.SortOrder
|
||||||
|
categoryCwId?: Prisma.SortOrder
|
||||||
|
subcategoryCwId?: Prisma.SortOrder
|
||||||
manufactureCwId?: Prisma.SortOrder
|
manufactureCwId?: Prisma.SortOrder
|
||||||
vendorCwId?: Prisma.SortOrder
|
vendorCwId?: Prisma.SortOrder
|
||||||
price?: Prisma.SortOrder
|
price?: Prisma.SortOrder
|
||||||
@@ -828,10 +950,15 @@ export type CatalogItemUncheckedUpdateManyWithoutLinkedItemsNestedInput = {
|
|||||||
export type CatalogItemCreateWithoutLinkedToInput = {
|
export type CatalogItemCreateWithoutLinkedToInput = {
|
||||||
id?: string
|
id?: string
|
||||||
cwCatalogId: number
|
cwCatalogId: number
|
||||||
|
identifier?: string | null
|
||||||
name: string
|
name: string
|
||||||
description?: string | null
|
description?: string | null
|
||||||
customerDescription?: string | null
|
customerDescription?: string | null
|
||||||
internalNotes?: string | null
|
internalNotes?: string | null
|
||||||
|
category?: string | null
|
||||||
|
categoryCwId?: number | null
|
||||||
|
subcategory?: string | null
|
||||||
|
subcategoryCwId?: number | null
|
||||||
manufacturer?: string | null
|
manufacturer?: string | null
|
||||||
manufactureCwId?: number | null
|
manufactureCwId?: number | null
|
||||||
partNumber?: string | null
|
partNumber?: string | null
|
||||||
@@ -852,10 +979,15 @@ export type CatalogItemCreateWithoutLinkedToInput = {
|
|||||||
export type CatalogItemUncheckedCreateWithoutLinkedToInput = {
|
export type CatalogItemUncheckedCreateWithoutLinkedToInput = {
|
||||||
id?: string
|
id?: string
|
||||||
cwCatalogId: number
|
cwCatalogId: number
|
||||||
|
identifier?: string | null
|
||||||
name: string
|
name: string
|
||||||
description?: string | null
|
description?: string | null
|
||||||
customerDescription?: string | null
|
customerDescription?: string | null
|
||||||
internalNotes?: string | null
|
internalNotes?: string | null
|
||||||
|
category?: string | null
|
||||||
|
categoryCwId?: number | null
|
||||||
|
subcategory?: string | null
|
||||||
|
subcategoryCwId?: number | null
|
||||||
manufacturer?: string | null
|
manufacturer?: string | null
|
||||||
manufactureCwId?: number | null
|
manufactureCwId?: number | null
|
||||||
partNumber?: string | null
|
partNumber?: string | null
|
||||||
@@ -881,10 +1013,15 @@ export type CatalogItemCreateOrConnectWithoutLinkedToInput = {
|
|||||||
export type CatalogItemCreateWithoutLinkedItemsInput = {
|
export type CatalogItemCreateWithoutLinkedItemsInput = {
|
||||||
id?: string
|
id?: string
|
||||||
cwCatalogId: number
|
cwCatalogId: number
|
||||||
|
identifier?: string | null
|
||||||
name: string
|
name: string
|
||||||
description?: string | null
|
description?: string | null
|
||||||
customerDescription?: string | null
|
customerDescription?: string | null
|
||||||
internalNotes?: string | null
|
internalNotes?: string | null
|
||||||
|
category?: string | null
|
||||||
|
categoryCwId?: number | null
|
||||||
|
subcategory?: string | null
|
||||||
|
subcategoryCwId?: number | null
|
||||||
manufacturer?: string | null
|
manufacturer?: string | null
|
||||||
manufactureCwId?: number | null
|
manufactureCwId?: number | null
|
||||||
partNumber?: string | null
|
partNumber?: string | null
|
||||||
@@ -905,10 +1042,15 @@ export type CatalogItemCreateWithoutLinkedItemsInput = {
|
|||||||
export type CatalogItemUncheckedCreateWithoutLinkedItemsInput = {
|
export type CatalogItemUncheckedCreateWithoutLinkedItemsInput = {
|
||||||
id?: string
|
id?: string
|
||||||
cwCatalogId: number
|
cwCatalogId: number
|
||||||
|
identifier?: string | null
|
||||||
name: string
|
name: string
|
||||||
description?: string | null
|
description?: string | null
|
||||||
customerDescription?: string | null
|
customerDescription?: string | null
|
||||||
internalNotes?: string | null
|
internalNotes?: string | null
|
||||||
|
category?: string | null
|
||||||
|
categoryCwId?: number | null
|
||||||
|
subcategory?: string | null
|
||||||
|
subcategoryCwId?: number | null
|
||||||
manufacturer?: string | null
|
manufacturer?: string | null
|
||||||
manufactureCwId?: number | null
|
manufactureCwId?: number | null
|
||||||
partNumber?: string | null
|
partNumber?: string | null
|
||||||
@@ -953,10 +1095,15 @@ export type CatalogItemScalarWhereInput = {
|
|||||||
NOT?: Prisma.CatalogItemScalarWhereInput | Prisma.CatalogItemScalarWhereInput[]
|
NOT?: Prisma.CatalogItemScalarWhereInput | Prisma.CatalogItemScalarWhereInput[]
|
||||||
id?: Prisma.StringFilter<"CatalogItem"> | string
|
id?: Prisma.StringFilter<"CatalogItem"> | string
|
||||||
cwCatalogId?: Prisma.IntFilter<"CatalogItem"> | number
|
cwCatalogId?: Prisma.IntFilter<"CatalogItem"> | number
|
||||||
|
identifier?: Prisma.StringNullableFilter<"CatalogItem"> | string | null
|
||||||
name?: Prisma.StringFilter<"CatalogItem"> | string
|
name?: Prisma.StringFilter<"CatalogItem"> | string
|
||||||
description?: Prisma.StringNullableFilter<"CatalogItem"> | string | null
|
description?: Prisma.StringNullableFilter<"CatalogItem"> | string | null
|
||||||
customerDescription?: Prisma.StringNullableFilter<"CatalogItem"> | string | null
|
customerDescription?: Prisma.StringNullableFilter<"CatalogItem"> | string | null
|
||||||
internalNotes?: Prisma.StringNullableFilter<"CatalogItem"> | string | null
|
internalNotes?: Prisma.StringNullableFilter<"CatalogItem"> | string | null
|
||||||
|
category?: Prisma.StringNullableFilter<"CatalogItem"> | string | null
|
||||||
|
categoryCwId?: Prisma.IntNullableFilter<"CatalogItem"> | number | null
|
||||||
|
subcategory?: Prisma.StringNullableFilter<"CatalogItem"> | string | null
|
||||||
|
subcategoryCwId?: Prisma.IntNullableFilter<"CatalogItem"> | number | null
|
||||||
manufacturer?: Prisma.StringNullableFilter<"CatalogItem"> | string | null
|
manufacturer?: Prisma.StringNullableFilter<"CatalogItem"> | string | null
|
||||||
manufactureCwId?: Prisma.IntNullableFilter<"CatalogItem"> | number | null
|
manufactureCwId?: Prisma.IntNullableFilter<"CatalogItem"> | number | null
|
||||||
partNumber?: Prisma.StringNullableFilter<"CatalogItem"> | string | null
|
partNumber?: Prisma.StringNullableFilter<"CatalogItem"> | string | null
|
||||||
@@ -992,10 +1139,15 @@ export type CatalogItemUpdateManyWithWhereWithoutLinkedItemsInput = {
|
|||||||
export type CatalogItemUpdateWithoutLinkedToInput = {
|
export type CatalogItemUpdateWithoutLinkedToInput = {
|
||||||
id?: Prisma.StringFieldUpdateOperationsInput | string
|
id?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
cwCatalogId?: Prisma.IntFieldUpdateOperationsInput | number
|
cwCatalogId?: Prisma.IntFieldUpdateOperationsInput | number
|
||||||
|
identifier?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
customerDescription?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
customerDescription?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
internalNotes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
internalNotes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
category?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
categoryCwId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
|
subcategory?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
subcategoryCwId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
manufacturer?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
manufacturer?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
manufactureCwId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
manufactureCwId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
partNumber?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
partNumber?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
@@ -1016,10 +1168,15 @@ export type CatalogItemUpdateWithoutLinkedToInput = {
|
|||||||
export type CatalogItemUncheckedUpdateWithoutLinkedToInput = {
|
export type CatalogItemUncheckedUpdateWithoutLinkedToInput = {
|
||||||
id?: Prisma.StringFieldUpdateOperationsInput | string
|
id?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
cwCatalogId?: Prisma.IntFieldUpdateOperationsInput | number
|
cwCatalogId?: Prisma.IntFieldUpdateOperationsInput | number
|
||||||
|
identifier?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
customerDescription?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
customerDescription?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
internalNotes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
internalNotes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
category?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
categoryCwId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
|
subcategory?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
subcategoryCwId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
manufacturer?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
manufacturer?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
manufactureCwId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
manufactureCwId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
partNumber?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
partNumber?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
@@ -1040,10 +1197,15 @@ export type CatalogItemUncheckedUpdateWithoutLinkedToInput = {
|
|||||||
export type CatalogItemUncheckedUpdateManyWithoutLinkedToInput = {
|
export type CatalogItemUncheckedUpdateManyWithoutLinkedToInput = {
|
||||||
id?: Prisma.StringFieldUpdateOperationsInput | string
|
id?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
cwCatalogId?: Prisma.IntFieldUpdateOperationsInput | number
|
cwCatalogId?: Prisma.IntFieldUpdateOperationsInput | number
|
||||||
|
identifier?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
customerDescription?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
customerDescription?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
internalNotes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
internalNotes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
category?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
categoryCwId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
|
subcategory?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
subcategoryCwId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
manufacturer?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
manufacturer?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
manufactureCwId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
manufactureCwId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
partNumber?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
partNumber?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
@@ -1063,10 +1225,15 @@ export type CatalogItemUncheckedUpdateManyWithoutLinkedToInput = {
|
|||||||
export type CatalogItemUpdateWithoutLinkedItemsInput = {
|
export type CatalogItemUpdateWithoutLinkedItemsInput = {
|
||||||
id?: Prisma.StringFieldUpdateOperationsInput | string
|
id?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
cwCatalogId?: Prisma.IntFieldUpdateOperationsInput | number
|
cwCatalogId?: Prisma.IntFieldUpdateOperationsInput | number
|
||||||
|
identifier?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
customerDescription?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
customerDescription?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
internalNotes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
internalNotes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
category?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
categoryCwId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
|
subcategory?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
subcategoryCwId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
manufacturer?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
manufacturer?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
manufactureCwId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
manufactureCwId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
partNumber?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
partNumber?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
@@ -1087,10 +1254,15 @@ export type CatalogItemUpdateWithoutLinkedItemsInput = {
|
|||||||
export type CatalogItemUncheckedUpdateWithoutLinkedItemsInput = {
|
export type CatalogItemUncheckedUpdateWithoutLinkedItemsInput = {
|
||||||
id?: Prisma.StringFieldUpdateOperationsInput | string
|
id?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
cwCatalogId?: Prisma.IntFieldUpdateOperationsInput | number
|
cwCatalogId?: Prisma.IntFieldUpdateOperationsInput | number
|
||||||
|
identifier?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
customerDescription?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
customerDescription?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
internalNotes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
internalNotes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
category?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
categoryCwId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
|
subcategory?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
subcategoryCwId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
manufacturer?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
manufacturer?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
manufactureCwId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
manufactureCwId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
partNumber?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
partNumber?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
@@ -1111,10 +1283,15 @@ export type CatalogItemUncheckedUpdateWithoutLinkedItemsInput = {
|
|||||||
export type CatalogItemUncheckedUpdateManyWithoutLinkedItemsInput = {
|
export type CatalogItemUncheckedUpdateManyWithoutLinkedItemsInput = {
|
||||||
id?: Prisma.StringFieldUpdateOperationsInput | string
|
id?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
cwCatalogId?: Prisma.IntFieldUpdateOperationsInput | number
|
cwCatalogId?: Prisma.IntFieldUpdateOperationsInput | number
|
||||||
|
identifier?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
customerDescription?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
customerDescription?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
internalNotes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
internalNotes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
category?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
categoryCwId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
|
subcategory?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
subcategoryCwId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
manufacturer?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
manufacturer?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
manufactureCwId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
manufactureCwId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||||
partNumber?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
partNumber?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
@@ -1174,10 +1351,15 @@ export type CatalogItemCountOutputTypeCountLinkedToArgs<ExtArgs extends runtime.
|
|||||||
export type CatalogItemSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
|
export type CatalogItemSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
|
||||||
id?: boolean
|
id?: boolean
|
||||||
cwCatalogId?: boolean
|
cwCatalogId?: boolean
|
||||||
|
identifier?: boolean
|
||||||
name?: boolean
|
name?: boolean
|
||||||
description?: boolean
|
description?: boolean
|
||||||
customerDescription?: boolean
|
customerDescription?: boolean
|
||||||
internalNotes?: boolean
|
internalNotes?: boolean
|
||||||
|
category?: boolean
|
||||||
|
categoryCwId?: boolean
|
||||||
|
subcategory?: boolean
|
||||||
|
subcategoryCwId?: boolean
|
||||||
manufacturer?: boolean
|
manufacturer?: boolean
|
||||||
manufactureCwId?: boolean
|
manufactureCwId?: boolean
|
||||||
partNumber?: boolean
|
partNumber?: boolean
|
||||||
@@ -1200,10 +1382,15 @@ export type CatalogItemSelect<ExtArgs extends runtime.Types.Extensions.InternalA
|
|||||||
export type CatalogItemSelectCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
|
export type CatalogItemSelectCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
|
||||||
id?: boolean
|
id?: boolean
|
||||||
cwCatalogId?: boolean
|
cwCatalogId?: boolean
|
||||||
|
identifier?: boolean
|
||||||
name?: boolean
|
name?: boolean
|
||||||
description?: boolean
|
description?: boolean
|
||||||
customerDescription?: boolean
|
customerDescription?: boolean
|
||||||
internalNotes?: boolean
|
internalNotes?: boolean
|
||||||
|
category?: boolean
|
||||||
|
categoryCwId?: boolean
|
||||||
|
subcategory?: boolean
|
||||||
|
subcategoryCwId?: boolean
|
||||||
manufacturer?: boolean
|
manufacturer?: boolean
|
||||||
manufactureCwId?: boolean
|
manufactureCwId?: boolean
|
||||||
partNumber?: boolean
|
partNumber?: boolean
|
||||||
@@ -1223,10 +1410,15 @@ export type CatalogItemSelectCreateManyAndReturn<ExtArgs extends runtime.Types.E
|
|||||||
export type CatalogItemSelectUpdateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
|
export type CatalogItemSelectUpdateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
|
||||||
id?: boolean
|
id?: boolean
|
||||||
cwCatalogId?: boolean
|
cwCatalogId?: boolean
|
||||||
|
identifier?: boolean
|
||||||
name?: boolean
|
name?: boolean
|
||||||
description?: boolean
|
description?: boolean
|
||||||
customerDescription?: boolean
|
customerDescription?: boolean
|
||||||
internalNotes?: boolean
|
internalNotes?: boolean
|
||||||
|
category?: boolean
|
||||||
|
categoryCwId?: boolean
|
||||||
|
subcategory?: boolean
|
||||||
|
subcategoryCwId?: boolean
|
||||||
manufacturer?: boolean
|
manufacturer?: boolean
|
||||||
manufactureCwId?: boolean
|
manufactureCwId?: boolean
|
||||||
partNumber?: boolean
|
partNumber?: boolean
|
||||||
@@ -1246,10 +1438,15 @@ export type CatalogItemSelectUpdateManyAndReturn<ExtArgs extends runtime.Types.E
|
|||||||
export type CatalogItemSelectScalar = {
|
export type CatalogItemSelectScalar = {
|
||||||
id?: boolean
|
id?: boolean
|
||||||
cwCatalogId?: boolean
|
cwCatalogId?: boolean
|
||||||
|
identifier?: boolean
|
||||||
name?: boolean
|
name?: boolean
|
||||||
description?: boolean
|
description?: boolean
|
||||||
customerDescription?: boolean
|
customerDescription?: boolean
|
||||||
internalNotes?: boolean
|
internalNotes?: boolean
|
||||||
|
category?: boolean
|
||||||
|
categoryCwId?: boolean
|
||||||
|
subcategory?: boolean
|
||||||
|
subcategoryCwId?: boolean
|
||||||
manufacturer?: boolean
|
manufacturer?: boolean
|
||||||
manufactureCwId?: boolean
|
manufactureCwId?: boolean
|
||||||
partNumber?: boolean
|
partNumber?: boolean
|
||||||
@@ -1266,7 +1463,7 @@ export type CatalogItemSelectScalar = {
|
|||||||
updatedAt?: boolean
|
updatedAt?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CatalogItemOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "cwCatalogId" | "name" | "description" | "customerDescription" | "internalNotes" | "manufacturer" | "manufactureCwId" | "partNumber" | "vendorName" | "vendorSku" | "vendorCwId" | "price" | "cost" | "inactive" | "salesTaxable" | "onHand" | "cwLastUpdated" | "createdAt" | "updatedAt", ExtArgs["result"]["catalogItem"]>
|
export type CatalogItemOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "cwCatalogId" | "identifier" | "name" | "description" | "customerDescription" | "internalNotes" | "category" | "categoryCwId" | "subcategory" | "subcategoryCwId" | "manufacturer" | "manufactureCwId" | "partNumber" | "vendorName" | "vendorSku" | "vendorCwId" | "price" | "cost" | "inactive" | "salesTaxable" | "onHand" | "cwLastUpdated" | "createdAt" | "updatedAt", ExtArgs["result"]["catalogItem"]>
|
||||||
export type CatalogItemInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
export type CatalogItemInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||||
linkedItems?: boolean | Prisma.CatalogItem$linkedItemsArgs<ExtArgs>
|
linkedItems?: boolean | Prisma.CatalogItem$linkedItemsArgs<ExtArgs>
|
||||||
linkedTo?: boolean | Prisma.CatalogItem$linkedToArgs<ExtArgs>
|
linkedTo?: boolean | Prisma.CatalogItem$linkedToArgs<ExtArgs>
|
||||||
@@ -1284,10 +1481,15 @@ export type $CatalogItemPayload<ExtArgs extends runtime.Types.Extensions.Interna
|
|||||||
scalars: runtime.Types.Extensions.GetPayloadResult<{
|
scalars: runtime.Types.Extensions.GetPayloadResult<{
|
||||||
id: string
|
id: string
|
||||||
cwCatalogId: number
|
cwCatalogId: number
|
||||||
|
identifier: string | null
|
||||||
name: string
|
name: string
|
||||||
description: string | null
|
description: string | null
|
||||||
customerDescription: string | null
|
customerDescription: string | null
|
||||||
internalNotes: string | null
|
internalNotes: string | null
|
||||||
|
category: string | null
|
||||||
|
categoryCwId: number | null
|
||||||
|
subcategory: string | null
|
||||||
|
subcategoryCwId: number | null
|
||||||
manufacturer: string | null
|
manufacturer: string | null
|
||||||
manufactureCwId: number | null
|
manufactureCwId: number | null
|
||||||
partNumber: string | null
|
partNumber: string | null
|
||||||
@@ -1729,10 +1931,15 @@ export interface Prisma__CatalogItemClient<T, Null = never, ExtArgs extends runt
|
|||||||
export interface CatalogItemFieldRefs {
|
export interface CatalogItemFieldRefs {
|
||||||
readonly id: Prisma.FieldRef<"CatalogItem", 'String'>
|
readonly id: Prisma.FieldRef<"CatalogItem", 'String'>
|
||||||
readonly cwCatalogId: Prisma.FieldRef<"CatalogItem", 'Int'>
|
readonly cwCatalogId: Prisma.FieldRef<"CatalogItem", 'Int'>
|
||||||
|
readonly identifier: Prisma.FieldRef<"CatalogItem", 'String'>
|
||||||
readonly name: Prisma.FieldRef<"CatalogItem", 'String'>
|
readonly name: Prisma.FieldRef<"CatalogItem", 'String'>
|
||||||
readonly description: Prisma.FieldRef<"CatalogItem", 'String'>
|
readonly description: Prisma.FieldRef<"CatalogItem", 'String'>
|
||||||
readonly customerDescription: Prisma.FieldRef<"CatalogItem", 'String'>
|
readonly customerDescription: Prisma.FieldRef<"CatalogItem", 'String'>
|
||||||
readonly internalNotes: Prisma.FieldRef<"CatalogItem", 'String'>
|
readonly internalNotes: Prisma.FieldRef<"CatalogItem", 'String'>
|
||||||
|
readonly category: Prisma.FieldRef<"CatalogItem", 'String'>
|
||||||
|
readonly categoryCwId: Prisma.FieldRef<"CatalogItem", 'Int'>
|
||||||
|
readonly subcategory: Prisma.FieldRef<"CatalogItem", 'String'>
|
||||||
|
readonly subcategoryCwId: Prisma.FieldRef<"CatalogItem", 'Int'>
|
||||||
readonly manufacturer: Prisma.FieldRef<"CatalogItem", 'String'>
|
readonly manufacturer: Prisma.FieldRef<"CatalogItem", 'String'>
|
||||||
readonly manufactureCwId: Prisma.FieldRef<"CatalogItem", 'Int'>
|
readonly manufactureCwId: Prisma.FieldRef<"CatalogItem", 'Int'>
|
||||||
readonly partNumber: Prisma.FieldRef<"CatalogItem", 'String'>
|
readonly partNumber: Prisma.FieldRef<"CatalogItem", 'String'>
|
||||||
|
|||||||
@@ -226,6 +226,7 @@ export type CompanyWhereInput = {
|
|||||||
updatedAt?: Prisma.DateTimeFilter<"Company"> | Date | string
|
updatedAt?: Prisma.DateTimeFilter<"Company"> | Date | string
|
||||||
credentials?: Prisma.CredentialListRelationFilter
|
credentials?: Prisma.CredentialListRelationFilter
|
||||||
unifiSites?: Prisma.UnifiSiteListRelationFilter
|
unifiSites?: Prisma.UnifiSiteListRelationFilter
|
||||||
|
opportunities?: Prisma.OpportunityListRelationFilter
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CompanyOrderByWithRelationInput = {
|
export type CompanyOrderByWithRelationInput = {
|
||||||
@@ -237,6 +238,7 @@ export type CompanyOrderByWithRelationInput = {
|
|||||||
updatedAt?: Prisma.SortOrder
|
updatedAt?: Prisma.SortOrder
|
||||||
credentials?: Prisma.CredentialOrderByRelationAggregateInput
|
credentials?: Prisma.CredentialOrderByRelationAggregateInput
|
||||||
unifiSites?: Prisma.UnifiSiteOrderByRelationAggregateInput
|
unifiSites?: Prisma.UnifiSiteOrderByRelationAggregateInput
|
||||||
|
opportunities?: Prisma.OpportunityOrderByRelationAggregateInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CompanyWhereUniqueInput = Prisma.AtLeast<{
|
export type CompanyWhereUniqueInput = Prisma.AtLeast<{
|
||||||
@@ -251,6 +253,7 @@ export type CompanyWhereUniqueInput = Prisma.AtLeast<{
|
|||||||
updatedAt?: Prisma.DateTimeFilter<"Company"> | Date | string
|
updatedAt?: Prisma.DateTimeFilter<"Company"> | Date | string
|
||||||
credentials?: Prisma.CredentialListRelationFilter
|
credentials?: Prisma.CredentialListRelationFilter
|
||||||
unifiSites?: Prisma.UnifiSiteListRelationFilter
|
unifiSites?: Prisma.UnifiSiteListRelationFilter
|
||||||
|
opportunities?: Prisma.OpportunityListRelationFilter
|
||||||
}, "id" | "cw_CompanyId" | "cw_Identifier">
|
}, "id" | "cw_CompanyId" | "cw_Identifier">
|
||||||
|
|
||||||
export type CompanyOrderByWithAggregationInput = {
|
export type CompanyOrderByWithAggregationInput = {
|
||||||
@@ -288,6 +291,7 @@ export type CompanyCreateInput = {
|
|||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
credentials?: Prisma.CredentialCreateNestedManyWithoutCompanyInput
|
credentials?: Prisma.CredentialCreateNestedManyWithoutCompanyInput
|
||||||
unifiSites?: Prisma.UnifiSiteCreateNestedManyWithoutCompanyInput
|
unifiSites?: Prisma.UnifiSiteCreateNestedManyWithoutCompanyInput
|
||||||
|
opportunities?: Prisma.OpportunityCreateNestedManyWithoutCompanyInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CompanyUncheckedCreateInput = {
|
export type CompanyUncheckedCreateInput = {
|
||||||
@@ -299,6 +303,7 @@ export type CompanyUncheckedCreateInput = {
|
|||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
credentials?: Prisma.CredentialUncheckedCreateNestedManyWithoutCompanyInput
|
credentials?: Prisma.CredentialUncheckedCreateNestedManyWithoutCompanyInput
|
||||||
unifiSites?: Prisma.UnifiSiteUncheckedCreateNestedManyWithoutCompanyInput
|
unifiSites?: Prisma.UnifiSiteUncheckedCreateNestedManyWithoutCompanyInput
|
||||||
|
opportunities?: Prisma.OpportunityUncheckedCreateNestedManyWithoutCompanyInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CompanyUpdateInput = {
|
export type CompanyUpdateInput = {
|
||||||
@@ -310,6 +315,7 @@ export type CompanyUpdateInput = {
|
|||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
credentials?: Prisma.CredentialUpdateManyWithoutCompanyNestedInput
|
credentials?: Prisma.CredentialUpdateManyWithoutCompanyNestedInput
|
||||||
unifiSites?: Prisma.UnifiSiteUpdateManyWithoutCompanyNestedInput
|
unifiSites?: Prisma.UnifiSiteUpdateManyWithoutCompanyNestedInput
|
||||||
|
opportunities?: Prisma.OpportunityUpdateManyWithoutCompanyNestedInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CompanyUncheckedUpdateInput = {
|
export type CompanyUncheckedUpdateInput = {
|
||||||
@@ -321,6 +327,7 @@ export type CompanyUncheckedUpdateInput = {
|
|||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
credentials?: Prisma.CredentialUncheckedUpdateManyWithoutCompanyNestedInput
|
credentials?: Prisma.CredentialUncheckedUpdateManyWithoutCompanyNestedInput
|
||||||
unifiSites?: Prisma.UnifiSiteUncheckedUpdateManyWithoutCompanyNestedInput
|
unifiSites?: Prisma.UnifiSiteUncheckedUpdateManyWithoutCompanyNestedInput
|
||||||
|
opportunities?: Prisma.OpportunityUncheckedUpdateManyWithoutCompanyNestedInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CompanyCreateManyInput = {
|
export type CompanyCreateManyInput = {
|
||||||
@@ -419,6 +426,22 @@ export type IntFieldUpdateOperationsInput = {
|
|||||||
divide?: number
|
divide?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type CompanyCreateNestedOneWithoutOpportunitiesInput = {
|
||||||
|
create?: Prisma.XOR<Prisma.CompanyCreateWithoutOpportunitiesInput, Prisma.CompanyUncheckedCreateWithoutOpportunitiesInput>
|
||||||
|
connectOrCreate?: Prisma.CompanyCreateOrConnectWithoutOpportunitiesInput
|
||||||
|
connect?: Prisma.CompanyWhereUniqueInput
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CompanyUpdateOneWithoutOpportunitiesNestedInput = {
|
||||||
|
create?: Prisma.XOR<Prisma.CompanyCreateWithoutOpportunitiesInput, Prisma.CompanyUncheckedCreateWithoutOpportunitiesInput>
|
||||||
|
connectOrCreate?: Prisma.CompanyCreateOrConnectWithoutOpportunitiesInput
|
||||||
|
upsert?: Prisma.CompanyUpsertWithoutOpportunitiesInput
|
||||||
|
disconnect?: Prisma.CompanyWhereInput | boolean
|
||||||
|
delete?: Prisma.CompanyWhereInput | boolean
|
||||||
|
connect?: Prisma.CompanyWhereUniqueInput
|
||||||
|
update?: Prisma.XOR<Prisma.XOR<Prisma.CompanyUpdateToOneWithWhereWithoutOpportunitiesInput, Prisma.CompanyUpdateWithoutOpportunitiesInput>, Prisma.CompanyUncheckedUpdateWithoutOpportunitiesInput>
|
||||||
|
}
|
||||||
|
|
||||||
export type CompanyCreateNestedOneWithoutCredentialsInput = {
|
export type CompanyCreateNestedOneWithoutCredentialsInput = {
|
||||||
create?: Prisma.XOR<Prisma.CompanyCreateWithoutCredentialsInput, Prisma.CompanyUncheckedCreateWithoutCredentialsInput>
|
create?: Prisma.XOR<Prisma.CompanyCreateWithoutCredentialsInput, Prisma.CompanyUncheckedCreateWithoutCredentialsInput>
|
||||||
connectOrCreate?: Prisma.CompanyCreateOrConnectWithoutCredentialsInput
|
connectOrCreate?: Prisma.CompanyCreateOrConnectWithoutCredentialsInput
|
||||||
@@ -441,6 +464,7 @@ export type CompanyCreateWithoutUnifiSitesInput = {
|
|||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
credentials?: Prisma.CredentialCreateNestedManyWithoutCompanyInput
|
credentials?: Prisma.CredentialCreateNestedManyWithoutCompanyInput
|
||||||
|
opportunities?: Prisma.OpportunityCreateNestedManyWithoutCompanyInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CompanyUncheckedCreateWithoutUnifiSitesInput = {
|
export type CompanyUncheckedCreateWithoutUnifiSitesInput = {
|
||||||
@@ -451,6 +475,7 @@ export type CompanyUncheckedCreateWithoutUnifiSitesInput = {
|
|||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
credentials?: Prisma.CredentialUncheckedCreateNestedManyWithoutCompanyInput
|
credentials?: Prisma.CredentialUncheckedCreateNestedManyWithoutCompanyInput
|
||||||
|
opportunities?: Prisma.OpportunityUncheckedCreateNestedManyWithoutCompanyInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CompanyCreateOrConnectWithoutUnifiSitesInput = {
|
export type CompanyCreateOrConnectWithoutUnifiSitesInput = {
|
||||||
@@ -477,6 +502,7 @@ export type CompanyUpdateWithoutUnifiSitesInput = {
|
|||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
credentials?: Prisma.CredentialUpdateManyWithoutCompanyNestedInput
|
credentials?: Prisma.CredentialUpdateManyWithoutCompanyNestedInput
|
||||||
|
opportunities?: Prisma.OpportunityUpdateManyWithoutCompanyNestedInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CompanyUncheckedUpdateWithoutUnifiSitesInput = {
|
export type CompanyUncheckedUpdateWithoutUnifiSitesInput = {
|
||||||
@@ -487,6 +513,67 @@ export type CompanyUncheckedUpdateWithoutUnifiSitesInput = {
|
|||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
credentials?: Prisma.CredentialUncheckedUpdateManyWithoutCompanyNestedInput
|
credentials?: Prisma.CredentialUncheckedUpdateManyWithoutCompanyNestedInput
|
||||||
|
opportunities?: Prisma.OpportunityUncheckedUpdateManyWithoutCompanyNestedInput
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CompanyCreateWithoutOpportunitiesInput = {
|
||||||
|
id?: string
|
||||||
|
name: string
|
||||||
|
cw_CompanyId: number
|
||||||
|
cw_Identifier: string
|
||||||
|
createdAt?: Date | string
|
||||||
|
updatedAt?: Date | string
|
||||||
|
credentials?: Prisma.CredentialCreateNestedManyWithoutCompanyInput
|
||||||
|
unifiSites?: Prisma.UnifiSiteCreateNestedManyWithoutCompanyInput
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CompanyUncheckedCreateWithoutOpportunitiesInput = {
|
||||||
|
id?: string
|
||||||
|
name: string
|
||||||
|
cw_CompanyId: number
|
||||||
|
cw_Identifier: string
|
||||||
|
createdAt?: Date | string
|
||||||
|
updatedAt?: Date | string
|
||||||
|
credentials?: Prisma.CredentialUncheckedCreateNestedManyWithoutCompanyInput
|
||||||
|
unifiSites?: Prisma.UnifiSiteUncheckedCreateNestedManyWithoutCompanyInput
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CompanyCreateOrConnectWithoutOpportunitiesInput = {
|
||||||
|
where: Prisma.CompanyWhereUniqueInput
|
||||||
|
create: Prisma.XOR<Prisma.CompanyCreateWithoutOpportunitiesInput, Prisma.CompanyUncheckedCreateWithoutOpportunitiesInput>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CompanyUpsertWithoutOpportunitiesInput = {
|
||||||
|
update: Prisma.XOR<Prisma.CompanyUpdateWithoutOpportunitiesInput, Prisma.CompanyUncheckedUpdateWithoutOpportunitiesInput>
|
||||||
|
create: Prisma.XOR<Prisma.CompanyCreateWithoutOpportunitiesInput, Prisma.CompanyUncheckedCreateWithoutOpportunitiesInput>
|
||||||
|
where?: Prisma.CompanyWhereInput
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CompanyUpdateToOneWithWhereWithoutOpportunitiesInput = {
|
||||||
|
where?: Prisma.CompanyWhereInput
|
||||||
|
data: Prisma.XOR<Prisma.CompanyUpdateWithoutOpportunitiesInput, Prisma.CompanyUncheckedUpdateWithoutOpportunitiesInput>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CompanyUpdateWithoutOpportunitiesInput = {
|
||||||
|
id?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
cw_CompanyId?: Prisma.IntFieldUpdateOperationsInput | number
|
||||||
|
cw_Identifier?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
|
credentials?: Prisma.CredentialUpdateManyWithoutCompanyNestedInput
|
||||||
|
unifiSites?: Prisma.UnifiSiteUpdateManyWithoutCompanyNestedInput
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CompanyUncheckedUpdateWithoutOpportunitiesInput = {
|
||||||
|
id?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
cw_CompanyId?: Prisma.IntFieldUpdateOperationsInput | number
|
||||||
|
cw_Identifier?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
|
credentials?: Prisma.CredentialUncheckedUpdateManyWithoutCompanyNestedInput
|
||||||
|
unifiSites?: Prisma.UnifiSiteUncheckedUpdateManyWithoutCompanyNestedInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CompanyCreateWithoutCredentialsInput = {
|
export type CompanyCreateWithoutCredentialsInput = {
|
||||||
@@ -497,6 +584,7 @@ export type CompanyCreateWithoutCredentialsInput = {
|
|||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
unifiSites?: Prisma.UnifiSiteCreateNestedManyWithoutCompanyInput
|
unifiSites?: Prisma.UnifiSiteCreateNestedManyWithoutCompanyInput
|
||||||
|
opportunities?: Prisma.OpportunityCreateNestedManyWithoutCompanyInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CompanyUncheckedCreateWithoutCredentialsInput = {
|
export type CompanyUncheckedCreateWithoutCredentialsInput = {
|
||||||
@@ -507,6 +595,7 @@ export type CompanyUncheckedCreateWithoutCredentialsInput = {
|
|||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
unifiSites?: Prisma.UnifiSiteUncheckedCreateNestedManyWithoutCompanyInput
|
unifiSites?: Prisma.UnifiSiteUncheckedCreateNestedManyWithoutCompanyInput
|
||||||
|
opportunities?: Prisma.OpportunityUncheckedCreateNestedManyWithoutCompanyInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CompanyCreateOrConnectWithoutCredentialsInput = {
|
export type CompanyCreateOrConnectWithoutCredentialsInput = {
|
||||||
@@ -533,6 +622,7 @@ export type CompanyUpdateWithoutCredentialsInput = {
|
|||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
unifiSites?: Prisma.UnifiSiteUpdateManyWithoutCompanyNestedInput
|
unifiSites?: Prisma.UnifiSiteUpdateManyWithoutCompanyNestedInput
|
||||||
|
opportunities?: Prisma.OpportunityUpdateManyWithoutCompanyNestedInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CompanyUncheckedUpdateWithoutCredentialsInput = {
|
export type CompanyUncheckedUpdateWithoutCredentialsInput = {
|
||||||
@@ -543,6 +633,7 @@ export type CompanyUncheckedUpdateWithoutCredentialsInput = {
|
|||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
unifiSites?: Prisma.UnifiSiteUncheckedUpdateManyWithoutCompanyNestedInput
|
unifiSites?: Prisma.UnifiSiteUncheckedUpdateManyWithoutCompanyNestedInput
|
||||||
|
opportunities?: Prisma.OpportunityUncheckedUpdateManyWithoutCompanyNestedInput
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -553,11 +644,13 @@ export type CompanyUncheckedUpdateWithoutCredentialsInput = {
|
|||||||
export type CompanyCountOutputType = {
|
export type CompanyCountOutputType = {
|
||||||
credentials: number
|
credentials: number
|
||||||
unifiSites: number
|
unifiSites: number
|
||||||
|
opportunities: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CompanyCountOutputTypeSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
export type CompanyCountOutputTypeSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||||
credentials?: boolean | CompanyCountOutputTypeCountCredentialsArgs
|
credentials?: boolean | CompanyCountOutputTypeCountCredentialsArgs
|
||||||
unifiSites?: boolean | CompanyCountOutputTypeCountUnifiSitesArgs
|
unifiSites?: boolean | CompanyCountOutputTypeCountUnifiSitesArgs
|
||||||
|
opportunities?: boolean | CompanyCountOutputTypeCountOpportunitiesArgs
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -584,6 +677,13 @@ export type CompanyCountOutputTypeCountUnifiSitesArgs<ExtArgs extends runtime.Ty
|
|||||||
where?: Prisma.UnifiSiteWhereInput
|
where?: Prisma.UnifiSiteWhereInput
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CompanyCountOutputType without action
|
||||||
|
*/
|
||||||
|
export type CompanyCountOutputTypeCountOpportunitiesArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||||
|
where?: Prisma.OpportunityWhereInput
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
export type CompanySelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
|
export type CompanySelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
|
||||||
id?: boolean
|
id?: boolean
|
||||||
@@ -594,6 +694,7 @@ export type CompanySelect<ExtArgs extends runtime.Types.Extensions.InternalArgs
|
|||||||
updatedAt?: boolean
|
updatedAt?: boolean
|
||||||
credentials?: boolean | Prisma.Company$credentialsArgs<ExtArgs>
|
credentials?: boolean | Prisma.Company$credentialsArgs<ExtArgs>
|
||||||
unifiSites?: boolean | Prisma.Company$unifiSitesArgs<ExtArgs>
|
unifiSites?: boolean | Prisma.Company$unifiSitesArgs<ExtArgs>
|
||||||
|
opportunities?: boolean | Prisma.Company$opportunitiesArgs<ExtArgs>
|
||||||
_count?: boolean | Prisma.CompanyCountOutputTypeDefaultArgs<ExtArgs>
|
_count?: boolean | Prisma.CompanyCountOutputTypeDefaultArgs<ExtArgs>
|
||||||
}, ExtArgs["result"]["company"]>
|
}, ExtArgs["result"]["company"]>
|
||||||
|
|
||||||
@@ -628,6 +729,7 @@ export type CompanyOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs =
|
|||||||
export type CompanyInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
export type CompanyInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||||
credentials?: boolean | Prisma.Company$credentialsArgs<ExtArgs>
|
credentials?: boolean | Prisma.Company$credentialsArgs<ExtArgs>
|
||||||
unifiSites?: boolean | Prisma.Company$unifiSitesArgs<ExtArgs>
|
unifiSites?: boolean | Prisma.Company$unifiSitesArgs<ExtArgs>
|
||||||
|
opportunities?: boolean | Prisma.Company$opportunitiesArgs<ExtArgs>
|
||||||
_count?: boolean | Prisma.CompanyCountOutputTypeDefaultArgs<ExtArgs>
|
_count?: boolean | Prisma.CompanyCountOutputTypeDefaultArgs<ExtArgs>
|
||||||
}
|
}
|
||||||
export type CompanyIncludeCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {}
|
export type CompanyIncludeCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {}
|
||||||
@@ -638,6 +740,7 @@ export type $CompanyPayload<ExtArgs extends runtime.Types.Extensions.InternalArg
|
|||||||
objects: {
|
objects: {
|
||||||
credentials: Prisma.$CredentialPayload<ExtArgs>[]
|
credentials: Prisma.$CredentialPayload<ExtArgs>[]
|
||||||
unifiSites: Prisma.$UnifiSitePayload<ExtArgs>[]
|
unifiSites: Prisma.$UnifiSitePayload<ExtArgs>[]
|
||||||
|
opportunities: Prisma.$OpportunityPayload<ExtArgs>[]
|
||||||
}
|
}
|
||||||
scalars: runtime.Types.Extensions.GetPayloadResult<{
|
scalars: runtime.Types.Extensions.GetPayloadResult<{
|
||||||
id: string
|
id: string
|
||||||
@@ -1042,6 +1145,7 @@ export interface Prisma__CompanyClient<T, Null = never, ExtArgs extends runtime.
|
|||||||
readonly [Symbol.toStringTag]: "PrismaPromise"
|
readonly [Symbol.toStringTag]: "PrismaPromise"
|
||||||
credentials<T extends Prisma.Company$credentialsArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Company$credentialsArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$CredentialPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
|
credentials<T extends Prisma.Company$credentialsArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Company$credentialsArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$CredentialPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
|
||||||
unifiSites<T extends Prisma.Company$unifiSitesArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Company$unifiSitesArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$UnifiSitePayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
|
unifiSites<T extends Prisma.Company$unifiSitesArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Company$unifiSitesArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$UnifiSitePayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
|
||||||
|
opportunities<T extends Prisma.Company$opportunitiesArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Company$opportunitiesArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$OpportunityPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
|
||||||
/**
|
/**
|
||||||
* Attaches callbacks for the resolution and/or rejection of the Promise.
|
* Attaches callbacks for the resolution and/or rejection of the Promise.
|
||||||
* @param onfulfilled The callback to execute when the Promise is resolved.
|
* @param onfulfilled The callback to execute when the Promise is resolved.
|
||||||
@@ -1512,6 +1616,30 @@ export type Company$unifiSitesArgs<ExtArgs extends runtime.Types.Extensions.Inte
|
|||||||
distinct?: Prisma.UnifiSiteScalarFieldEnum | Prisma.UnifiSiteScalarFieldEnum[]
|
distinct?: Prisma.UnifiSiteScalarFieldEnum | Prisma.UnifiSiteScalarFieldEnum[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Company.opportunities
|
||||||
|
*/
|
||||||
|
export type Company$opportunitiesArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||||
|
/**
|
||||||
|
* Select specific fields to fetch from the Opportunity
|
||||||
|
*/
|
||||||
|
select?: Prisma.OpportunitySelect<ExtArgs> | null
|
||||||
|
/**
|
||||||
|
* Omit specific fields from the Opportunity
|
||||||
|
*/
|
||||||
|
omit?: Prisma.OpportunityOmit<ExtArgs> | null
|
||||||
|
/**
|
||||||
|
* Choose, which related nodes to fetch as well
|
||||||
|
*/
|
||||||
|
include?: Prisma.OpportunityInclude<ExtArgs> | null
|
||||||
|
where?: Prisma.OpportunityWhereInput
|
||||||
|
orderBy?: Prisma.OpportunityOrderByWithRelationInput | Prisma.OpportunityOrderByWithRelationInput[]
|
||||||
|
cursor?: Prisma.OpportunityWhereUniqueInput
|
||||||
|
take?: number
|
||||||
|
skip?: number
|
||||||
|
distinct?: Prisma.OpportunityScalarFieldEnum | Prisma.OpportunityScalarFieldEnum[]
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Company without action
|
* Company without action
|
||||||
*/
|
*/
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -32,6 +32,7 @@ export type UserMinAggregateOutputType = {
|
|||||||
email: string | null
|
email: string | null
|
||||||
emailVerified: Date | null
|
emailVerified: Date | null
|
||||||
image: string | null
|
image: string | null
|
||||||
|
cwIdentifier: string | null
|
||||||
userId: string | null
|
userId: string | null
|
||||||
token: string | null
|
token: string | null
|
||||||
createdAt: Date | null
|
createdAt: Date | null
|
||||||
@@ -46,6 +47,7 @@ export type UserMaxAggregateOutputType = {
|
|||||||
email: string | null
|
email: string | null
|
||||||
emailVerified: Date | null
|
emailVerified: Date | null
|
||||||
image: string | null
|
image: string | null
|
||||||
|
cwIdentifier: string | null
|
||||||
userId: string | null
|
userId: string | null
|
||||||
token: string | null
|
token: string | null
|
||||||
createdAt: Date | null
|
createdAt: Date | null
|
||||||
@@ -60,6 +62,7 @@ export type UserCountAggregateOutputType = {
|
|||||||
email: number
|
email: number
|
||||||
emailVerified: number
|
emailVerified: number
|
||||||
image: number
|
image: number
|
||||||
|
cwIdentifier: number
|
||||||
userId: number
|
userId: number
|
||||||
token: number
|
token: number
|
||||||
createdAt: number
|
createdAt: number
|
||||||
@@ -76,6 +79,7 @@ export type UserMinAggregateInputType = {
|
|||||||
email?: true
|
email?: true
|
||||||
emailVerified?: true
|
emailVerified?: true
|
||||||
image?: true
|
image?: true
|
||||||
|
cwIdentifier?: true
|
||||||
userId?: true
|
userId?: true
|
||||||
token?: true
|
token?: true
|
||||||
createdAt?: true
|
createdAt?: true
|
||||||
@@ -90,6 +94,7 @@ export type UserMaxAggregateInputType = {
|
|||||||
email?: true
|
email?: true
|
||||||
emailVerified?: true
|
emailVerified?: true
|
||||||
image?: true
|
image?: true
|
||||||
|
cwIdentifier?: true
|
||||||
userId?: true
|
userId?: true
|
||||||
token?: true
|
token?: true
|
||||||
createdAt?: true
|
createdAt?: true
|
||||||
@@ -104,6 +109,7 @@ export type UserCountAggregateInputType = {
|
|||||||
email?: true
|
email?: true
|
||||||
emailVerified?: true
|
emailVerified?: true
|
||||||
image?: true
|
image?: true
|
||||||
|
cwIdentifier?: true
|
||||||
userId?: true
|
userId?: true
|
||||||
token?: true
|
token?: true
|
||||||
createdAt?: true
|
createdAt?: true
|
||||||
@@ -191,6 +197,7 @@ export type UserGroupByOutputType = {
|
|||||||
email: string
|
email: string
|
||||||
emailVerified: Date | null
|
emailVerified: Date | null
|
||||||
image: string | null
|
image: string | null
|
||||||
|
cwIdentifier: string | null
|
||||||
userId: string
|
userId: string
|
||||||
token: string | null
|
token: string | null
|
||||||
createdAt: Date
|
createdAt: Date
|
||||||
@@ -226,12 +233,14 @@ export type UserWhereInput = {
|
|||||||
email?: Prisma.StringFilter<"User"> | string
|
email?: Prisma.StringFilter<"User"> | string
|
||||||
emailVerified?: Prisma.DateTimeNullableFilter<"User"> | Date | string | null
|
emailVerified?: Prisma.DateTimeNullableFilter<"User"> | Date | string | null
|
||||||
image?: Prisma.StringNullableFilter<"User"> | string | null
|
image?: Prisma.StringNullableFilter<"User"> | string | null
|
||||||
|
cwIdentifier?: Prisma.StringNullableFilter<"User"> | string | null
|
||||||
userId?: Prisma.StringFilter<"User"> | string
|
userId?: Prisma.StringFilter<"User"> | string
|
||||||
token?: Prisma.StringNullableFilter<"User"> | string | null
|
token?: Prisma.StringNullableFilter<"User"> | string | null
|
||||||
createdAt?: Prisma.DateTimeFilter<"User"> | Date | string
|
createdAt?: Prisma.DateTimeFilter<"User"> | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFilter<"User"> | Date | string
|
updatedAt?: Prisma.DateTimeFilter<"User"> | Date | string
|
||||||
roles?: Prisma.RoleListRelationFilter
|
roles?: Prisma.RoleListRelationFilter
|
||||||
sessions?: Prisma.SessionListRelationFilter
|
sessions?: Prisma.SessionListRelationFilter
|
||||||
|
generatedQuotes?: Prisma.GeneratedQuotesListRelationFilter
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UserOrderByWithRelationInput = {
|
export type UserOrderByWithRelationInput = {
|
||||||
@@ -242,12 +251,14 @@ export type UserOrderByWithRelationInput = {
|
|||||||
email?: Prisma.SortOrder
|
email?: Prisma.SortOrder
|
||||||
emailVerified?: Prisma.SortOrderInput | Prisma.SortOrder
|
emailVerified?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||||
image?: Prisma.SortOrderInput | Prisma.SortOrder
|
image?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||||
|
cwIdentifier?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||||
userId?: Prisma.SortOrder
|
userId?: Prisma.SortOrder
|
||||||
token?: Prisma.SortOrderInput | Prisma.SortOrder
|
token?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||||
createdAt?: Prisma.SortOrder
|
createdAt?: Prisma.SortOrder
|
||||||
updatedAt?: Prisma.SortOrder
|
updatedAt?: Prisma.SortOrder
|
||||||
roles?: Prisma.RoleOrderByRelationAggregateInput
|
roles?: Prisma.RoleOrderByRelationAggregateInput
|
||||||
sessions?: Prisma.SessionOrderByRelationAggregateInput
|
sessions?: Prisma.SessionOrderByRelationAggregateInput
|
||||||
|
generatedQuotes?: Prisma.GeneratedQuotesOrderByRelationAggregateInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UserWhereUniqueInput = Prisma.AtLeast<{
|
export type UserWhereUniqueInput = Prisma.AtLeast<{
|
||||||
@@ -262,11 +273,13 @@ export type UserWhereUniqueInput = Prisma.AtLeast<{
|
|||||||
name?: Prisma.StringNullableFilter<"User"> | string | null
|
name?: Prisma.StringNullableFilter<"User"> | string | null
|
||||||
emailVerified?: Prisma.DateTimeNullableFilter<"User"> | Date | string | null
|
emailVerified?: Prisma.DateTimeNullableFilter<"User"> | Date | string | null
|
||||||
image?: Prisma.StringNullableFilter<"User"> | string | null
|
image?: Prisma.StringNullableFilter<"User"> | string | null
|
||||||
|
cwIdentifier?: Prisma.StringNullableFilter<"User"> | string | null
|
||||||
token?: Prisma.StringNullableFilter<"User"> | string | null
|
token?: Prisma.StringNullableFilter<"User"> | string | null
|
||||||
createdAt?: Prisma.DateTimeFilter<"User"> | Date | string
|
createdAt?: Prisma.DateTimeFilter<"User"> | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFilter<"User"> | Date | string
|
updatedAt?: Prisma.DateTimeFilter<"User"> | Date | string
|
||||||
roles?: Prisma.RoleListRelationFilter
|
roles?: Prisma.RoleListRelationFilter
|
||||||
sessions?: Prisma.SessionListRelationFilter
|
sessions?: Prisma.SessionListRelationFilter
|
||||||
|
generatedQuotes?: Prisma.GeneratedQuotesListRelationFilter
|
||||||
}, "id" | "login" | "email" | "userId">
|
}, "id" | "login" | "email" | "userId">
|
||||||
|
|
||||||
export type UserOrderByWithAggregationInput = {
|
export type UserOrderByWithAggregationInput = {
|
||||||
@@ -277,6 +290,7 @@ export type UserOrderByWithAggregationInput = {
|
|||||||
email?: Prisma.SortOrder
|
email?: Prisma.SortOrder
|
||||||
emailVerified?: Prisma.SortOrderInput | Prisma.SortOrder
|
emailVerified?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||||
image?: Prisma.SortOrderInput | Prisma.SortOrder
|
image?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||||
|
cwIdentifier?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||||
userId?: Prisma.SortOrder
|
userId?: Prisma.SortOrder
|
||||||
token?: Prisma.SortOrderInput | Prisma.SortOrder
|
token?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||||
createdAt?: Prisma.SortOrder
|
createdAt?: Prisma.SortOrder
|
||||||
@@ -297,6 +311,7 @@ export type UserScalarWhereWithAggregatesInput = {
|
|||||||
email?: Prisma.StringWithAggregatesFilter<"User"> | string
|
email?: Prisma.StringWithAggregatesFilter<"User"> | string
|
||||||
emailVerified?: Prisma.DateTimeNullableWithAggregatesFilter<"User"> | Date | string | null
|
emailVerified?: Prisma.DateTimeNullableWithAggregatesFilter<"User"> | Date | string | null
|
||||||
image?: Prisma.StringNullableWithAggregatesFilter<"User"> | string | null
|
image?: Prisma.StringNullableWithAggregatesFilter<"User"> | string | null
|
||||||
|
cwIdentifier?: Prisma.StringNullableWithAggregatesFilter<"User"> | string | null
|
||||||
userId?: Prisma.StringWithAggregatesFilter<"User"> | string
|
userId?: Prisma.StringWithAggregatesFilter<"User"> | string
|
||||||
token?: Prisma.StringNullableWithAggregatesFilter<"User"> | string | null
|
token?: Prisma.StringNullableWithAggregatesFilter<"User"> | string | null
|
||||||
createdAt?: Prisma.DateTimeWithAggregatesFilter<"User"> | Date | string
|
createdAt?: Prisma.DateTimeWithAggregatesFilter<"User"> | Date | string
|
||||||
@@ -311,12 +326,14 @@ export type UserCreateInput = {
|
|||||||
email: string
|
email: string
|
||||||
emailVerified?: Date | string | null
|
emailVerified?: Date | string | null
|
||||||
image?: string | null
|
image?: string | null
|
||||||
|
cwIdentifier?: string | null
|
||||||
userId: string
|
userId: string
|
||||||
token?: string | null
|
token?: string | null
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
roles?: Prisma.RoleCreateNestedManyWithoutUsersInput
|
roles?: Prisma.RoleCreateNestedManyWithoutUsersInput
|
||||||
sessions?: Prisma.SessionCreateNestedManyWithoutUserInput
|
sessions?: Prisma.SessionCreateNestedManyWithoutUserInput
|
||||||
|
generatedQuotes?: Prisma.GeneratedQuotesCreateNestedManyWithoutCreatedByInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UserUncheckedCreateInput = {
|
export type UserUncheckedCreateInput = {
|
||||||
@@ -327,12 +344,14 @@ export type UserUncheckedCreateInput = {
|
|||||||
email: string
|
email: string
|
||||||
emailVerified?: Date | string | null
|
emailVerified?: Date | string | null
|
||||||
image?: string | null
|
image?: string | null
|
||||||
|
cwIdentifier?: string | null
|
||||||
userId: string
|
userId: string
|
||||||
token?: string | null
|
token?: string | null
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
roles?: Prisma.RoleUncheckedCreateNestedManyWithoutUsersInput
|
roles?: Prisma.RoleUncheckedCreateNestedManyWithoutUsersInput
|
||||||
sessions?: Prisma.SessionUncheckedCreateNestedManyWithoutUserInput
|
sessions?: Prisma.SessionUncheckedCreateNestedManyWithoutUserInput
|
||||||
|
generatedQuotes?: Prisma.GeneratedQuotesUncheckedCreateNestedManyWithoutCreatedByInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UserUpdateInput = {
|
export type UserUpdateInput = {
|
||||||
@@ -343,12 +362,14 @@ export type UserUpdateInput = {
|
|||||||
email?: Prisma.StringFieldUpdateOperationsInput | string
|
email?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
emailVerified?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
emailVerified?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
cwIdentifier?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
userId?: Prisma.StringFieldUpdateOperationsInput | string
|
userId?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
token?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
token?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
roles?: Prisma.RoleUpdateManyWithoutUsersNestedInput
|
roles?: Prisma.RoleUpdateManyWithoutUsersNestedInput
|
||||||
sessions?: Prisma.SessionUpdateManyWithoutUserNestedInput
|
sessions?: Prisma.SessionUpdateManyWithoutUserNestedInput
|
||||||
|
generatedQuotes?: Prisma.GeneratedQuotesUpdateManyWithoutCreatedByNestedInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UserUncheckedUpdateInput = {
|
export type UserUncheckedUpdateInput = {
|
||||||
@@ -359,12 +380,14 @@ export type UserUncheckedUpdateInput = {
|
|||||||
email?: Prisma.StringFieldUpdateOperationsInput | string
|
email?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
emailVerified?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
emailVerified?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
cwIdentifier?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
userId?: Prisma.StringFieldUpdateOperationsInput | string
|
userId?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
token?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
token?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
roles?: Prisma.RoleUncheckedUpdateManyWithoutUsersNestedInput
|
roles?: Prisma.RoleUncheckedUpdateManyWithoutUsersNestedInput
|
||||||
sessions?: Prisma.SessionUncheckedUpdateManyWithoutUserNestedInput
|
sessions?: Prisma.SessionUncheckedUpdateManyWithoutUserNestedInput
|
||||||
|
generatedQuotes?: Prisma.GeneratedQuotesUncheckedUpdateManyWithoutCreatedByNestedInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UserCreateManyInput = {
|
export type UserCreateManyInput = {
|
||||||
@@ -375,6 +398,7 @@ export type UserCreateManyInput = {
|
|||||||
email: string
|
email: string
|
||||||
emailVerified?: Date | string | null
|
emailVerified?: Date | string | null
|
||||||
image?: string | null
|
image?: string | null
|
||||||
|
cwIdentifier?: string | null
|
||||||
userId: string
|
userId: string
|
||||||
token?: string | null
|
token?: string | null
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
@@ -389,6 +413,7 @@ export type UserUpdateManyMutationInput = {
|
|||||||
email?: Prisma.StringFieldUpdateOperationsInput | string
|
email?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
emailVerified?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
emailVerified?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
cwIdentifier?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
userId?: Prisma.StringFieldUpdateOperationsInput | string
|
userId?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
token?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
token?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
@@ -403,6 +428,7 @@ export type UserUncheckedUpdateManyInput = {
|
|||||||
email?: Prisma.StringFieldUpdateOperationsInput | string
|
email?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
emailVerified?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
emailVerified?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
cwIdentifier?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
userId?: Prisma.StringFieldUpdateOperationsInput | string
|
userId?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
token?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
token?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
@@ -422,6 +448,7 @@ export type UserCountOrderByAggregateInput = {
|
|||||||
email?: Prisma.SortOrder
|
email?: Prisma.SortOrder
|
||||||
emailVerified?: Prisma.SortOrder
|
emailVerified?: Prisma.SortOrder
|
||||||
image?: Prisma.SortOrder
|
image?: Prisma.SortOrder
|
||||||
|
cwIdentifier?: Prisma.SortOrder
|
||||||
userId?: Prisma.SortOrder
|
userId?: Prisma.SortOrder
|
||||||
token?: Prisma.SortOrder
|
token?: Prisma.SortOrder
|
||||||
createdAt?: Prisma.SortOrder
|
createdAt?: Prisma.SortOrder
|
||||||
@@ -436,6 +463,7 @@ export type UserMaxOrderByAggregateInput = {
|
|||||||
email?: Prisma.SortOrder
|
email?: Prisma.SortOrder
|
||||||
emailVerified?: Prisma.SortOrder
|
emailVerified?: Prisma.SortOrder
|
||||||
image?: Prisma.SortOrder
|
image?: Prisma.SortOrder
|
||||||
|
cwIdentifier?: Prisma.SortOrder
|
||||||
userId?: Prisma.SortOrder
|
userId?: Prisma.SortOrder
|
||||||
token?: Prisma.SortOrder
|
token?: Prisma.SortOrder
|
||||||
createdAt?: Prisma.SortOrder
|
createdAt?: Prisma.SortOrder
|
||||||
@@ -450,6 +478,7 @@ export type UserMinOrderByAggregateInput = {
|
|||||||
email?: Prisma.SortOrder
|
email?: Prisma.SortOrder
|
||||||
emailVerified?: Prisma.SortOrder
|
emailVerified?: Prisma.SortOrder
|
||||||
image?: Prisma.SortOrder
|
image?: Prisma.SortOrder
|
||||||
|
cwIdentifier?: Prisma.SortOrder
|
||||||
userId?: Prisma.SortOrder
|
userId?: Prisma.SortOrder
|
||||||
token?: Prisma.SortOrder
|
token?: Prisma.SortOrder
|
||||||
createdAt?: Prisma.SortOrder
|
createdAt?: Prisma.SortOrder
|
||||||
@@ -466,6 +495,11 @@ export type UserOrderByRelationAggregateInput = {
|
|||||||
_count?: Prisma.SortOrder
|
_count?: Prisma.SortOrder
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type UserNullableScalarRelationFilter = {
|
||||||
|
is?: Prisma.UserWhereInput | null
|
||||||
|
isNot?: Prisma.UserWhereInput | null
|
||||||
|
}
|
||||||
|
|
||||||
export type UserCreateNestedOneWithoutSessionsInput = {
|
export type UserCreateNestedOneWithoutSessionsInput = {
|
||||||
create?: Prisma.XOR<Prisma.UserCreateWithoutSessionsInput, Prisma.UserUncheckedCreateWithoutSessionsInput>
|
create?: Prisma.XOR<Prisma.UserCreateWithoutSessionsInput, Prisma.UserUncheckedCreateWithoutSessionsInput>
|
||||||
connectOrCreate?: Prisma.UserCreateOrConnectWithoutSessionsInput
|
connectOrCreate?: Prisma.UserCreateOrConnectWithoutSessionsInput
|
||||||
@@ -522,6 +556,22 @@ export type UserUncheckedUpdateManyWithoutRolesNestedInput = {
|
|||||||
deleteMany?: Prisma.UserScalarWhereInput | Prisma.UserScalarWhereInput[]
|
deleteMany?: Prisma.UserScalarWhereInput | Prisma.UserScalarWhereInput[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type UserCreateNestedOneWithoutGeneratedQuotesInput = {
|
||||||
|
create?: Prisma.XOR<Prisma.UserCreateWithoutGeneratedQuotesInput, Prisma.UserUncheckedCreateWithoutGeneratedQuotesInput>
|
||||||
|
connectOrCreate?: Prisma.UserCreateOrConnectWithoutGeneratedQuotesInput
|
||||||
|
connect?: Prisma.UserWhereUniqueInput
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UserUpdateOneWithoutGeneratedQuotesNestedInput = {
|
||||||
|
create?: Prisma.XOR<Prisma.UserCreateWithoutGeneratedQuotesInput, Prisma.UserUncheckedCreateWithoutGeneratedQuotesInput>
|
||||||
|
connectOrCreate?: Prisma.UserCreateOrConnectWithoutGeneratedQuotesInput
|
||||||
|
upsert?: Prisma.UserUpsertWithoutGeneratedQuotesInput
|
||||||
|
disconnect?: Prisma.UserWhereInput | boolean
|
||||||
|
delete?: Prisma.UserWhereInput | boolean
|
||||||
|
connect?: Prisma.UserWhereUniqueInput
|
||||||
|
update?: Prisma.XOR<Prisma.XOR<Prisma.UserUpdateToOneWithWhereWithoutGeneratedQuotesInput, Prisma.UserUpdateWithoutGeneratedQuotesInput>, Prisma.UserUncheckedUpdateWithoutGeneratedQuotesInput>
|
||||||
|
}
|
||||||
|
|
||||||
export type UserCreateWithoutSessionsInput = {
|
export type UserCreateWithoutSessionsInput = {
|
||||||
id?: string
|
id?: string
|
||||||
permissions?: string | null
|
permissions?: string | null
|
||||||
@@ -530,11 +580,13 @@ export type UserCreateWithoutSessionsInput = {
|
|||||||
email: string
|
email: string
|
||||||
emailVerified?: Date | string | null
|
emailVerified?: Date | string | null
|
||||||
image?: string | null
|
image?: string | null
|
||||||
|
cwIdentifier?: string | null
|
||||||
userId: string
|
userId: string
|
||||||
token?: string | null
|
token?: string | null
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
roles?: Prisma.RoleCreateNestedManyWithoutUsersInput
|
roles?: Prisma.RoleCreateNestedManyWithoutUsersInput
|
||||||
|
generatedQuotes?: Prisma.GeneratedQuotesCreateNestedManyWithoutCreatedByInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UserUncheckedCreateWithoutSessionsInput = {
|
export type UserUncheckedCreateWithoutSessionsInput = {
|
||||||
@@ -545,11 +597,13 @@ export type UserUncheckedCreateWithoutSessionsInput = {
|
|||||||
email: string
|
email: string
|
||||||
emailVerified?: Date | string | null
|
emailVerified?: Date | string | null
|
||||||
image?: string | null
|
image?: string | null
|
||||||
|
cwIdentifier?: string | null
|
||||||
userId: string
|
userId: string
|
||||||
token?: string | null
|
token?: string | null
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
roles?: Prisma.RoleUncheckedCreateNestedManyWithoutUsersInput
|
roles?: Prisma.RoleUncheckedCreateNestedManyWithoutUsersInput
|
||||||
|
generatedQuotes?: Prisma.GeneratedQuotesUncheckedCreateNestedManyWithoutCreatedByInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UserCreateOrConnectWithoutSessionsInput = {
|
export type UserCreateOrConnectWithoutSessionsInput = {
|
||||||
@@ -576,11 +630,13 @@ export type UserUpdateWithoutSessionsInput = {
|
|||||||
email?: Prisma.StringFieldUpdateOperationsInput | string
|
email?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
emailVerified?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
emailVerified?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
cwIdentifier?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
userId?: Prisma.StringFieldUpdateOperationsInput | string
|
userId?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
token?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
token?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
roles?: Prisma.RoleUpdateManyWithoutUsersNestedInput
|
roles?: Prisma.RoleUpdateManyWithoutUsersNestedInput
|
||||||
|
generatedQuotes?: Prisma.GeneratedQuotesUpdateManyWithoutCreatedByNestedInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UserUncheckedUpdateWithoutSessionsInput = {
|
export type UserUncheckedUpdateWithoutSessionsInput = {
|
||||||
@@ -591,11 +647,13 @@ export type UserUncheckedUpdateWithoutSessionsInput = {
|
|||||||
email?: Prisma.StringFieldUpdateOperationsInput | string
|
email?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
emailVerified?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
emailVerified?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
cwIdentifier?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
userId?: Prisma.StringFieldUpdateOperationsInput | string
|
userId?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
token?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
token?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
roles?: Prisma.RoleUncheckedUpdateManyWithoutUsersNestedInput
|
roles?: Prisma.RoleUncheckedUpdateManyWithoutUsersNestedInput
|
||||||
|
generatedQuotes?: Prisma.GeneratedQuotesUncheckedUpdateManyWithoutCreatedByNestedInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UserCreateWithoutRolesInput = {
|
export type UserCreateWithoutRolesInput = {
|
||||||
@@ -606,11 +664,13 @@ export type UserCreateWithoutRolesInput = {
|
|||||||
email: string
|
email: string
|
||||||
emailVerified?: Date | string | null
|
emailVerified?: Date | string | null
|
||||||
image?: string | null
|
image?: string | null
|
||||||
|
cwIdentifier?: string | null
|
||||||
userId: string
|
userId: string
|
||||||
token?: string | null
|
token?: string | null
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
sessions?: Prisma.SessionCreateNestedManyWithoutUserInput
|
sessions?: Prisma.SessionCreateNestedManyWithoutUserInput
|
||||||
|
generatedQuotes?: Prisma.GeneratedQuotesCreateNestedManyWithoutCreatedByInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UserUncheckedCreateWithoutRolesInput = {
|
export type UserUncheckedCreateWithoutRolesInput = {
|
||||||
@@ -621,11 +681,13 @@ export type UserUncheckedCreateWithoutRolesInput = {
|
|||||||
email: string
|
email: string
|
||||||
emailVerified?: Date | string | null
|
emailVerified?: Date | string | null
|
||||||
image?: string | null
|
image?: string | null
|
||||||
|
cwIdentifier?: string | null
|
||||||
userId: string
|
userId: string
|
||||||
token?: string | null
|
token?: string | null
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
sessions?: Prisma.SessionUncheckedCreateNestedManyWithoutUserInput
|
sessions?: Prisma.SessionUncheckedCreateNestedManyWithoutUserInput
|
||||||
|
generatedQuotes?: Prisma.GeneratedQuotesUncheckedCreateNestedManyWithoutCreatedByInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UserCreateOrConnectWithoutRolesInput = {
|
export type UserCreateOrConnectWithoutRolesInput = {
|
||||||
@@ -660,12 +722,97 @@ export type UserScalarWhereInput = {
|
|||||||
email?: Prisma.StringFilter<"User"> | string
|
email?: Prisma.StringFilter<"User"> | string
|
||||||
emailVerified?: Prisma.DateTimeNullableFilter<"User"> | Date | string | null
|
emailVerified?: Prisma.DateTimeNullableFilter<"User"> | Date | string | null
|
||||||
image?: Prisma.StringNullableFilter<"User"> | string | null
|
image?: Prisma.StringNullableFilter<"User"> | string | null
|
||||||
|
cwIdentifier?: Prisma.StringNullableFilter<"User"> | string | null
|
||||||
userId?: Prisma.StringFilter<"User"> | string
|
userId?: Prisma.StringFilter<"User"> | string
|
||||||
token?: Prisma.StringNullableFilter<"User"> | string | null
|
token?: Prisma.StringNullableFilter<"User"> | string | null
|
||||||
createdAt?: Prisma.DateTimeFilter<"User"> | Date | string
|
createdAt?: Prisma.DateTimeFilter<"User"> | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFilter<"User"> | Date | string
|
updatedAt?: Prisma.DateTimeFilter<"User"> | Date | string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type UserCreateWithoutGeneratedQuotesInput = {
|
||||||
|
id?: string
|
||||||
|
permissions?: string | null
|
||||||
|
login: string
|
||||||
|
name?: string | null
|
||||||
|
email: string
|
||||||
|
emailVerified?: Date | string | null
|
||||||
|
image?: string | null
|
||||||
|
cwIdentifier?: string | null
|
||||||
|
userId: string
|
||||||
|
token?: string | null
|
||||||
|
createdAt?: Date | string
|
||||||
|
updatedAt?: Date | string
|
||||||
|
roles?: Prisma.RoleCreateNestedManyWithoutUsersInput
|
||||||
|
sessions?: Prisma.SessionCreateNestedManyWithoutUserInput
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UserUncheckedCreateWithoutGeneratedQuotesInput = {
|
||||||
|
id?: string
|
||||||
|
permissions?: string | null
|
||||||
|
login: string
|
||||||
|
name?: string | null
|
||||||
|
email: string
|
||||||
|
emailVerified?: Date | string | null
|
||||||
|
image?: string | null
|
||||||
|
cwIdentifier?: string | null
|
||||||
|
userId: string
|
||||||
|
token?: string | null
|
||||||
|
createdAt?: Date | string
|
||||||
|
updatedAt?: Date | string
|
||||||
|
roles?: Prisma.RoleUncheckedCreateNestedManyWithoutUsersInput
|
||||||
|
sessions?: Prisma.SessionUncheckedCreateNestedManyWithoutUserInput
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UserCreateOrConnectWithoutGeneratedQuotesInput = {
|
||||||
|
where: Prisma.UserWhereUniqueInput
|
||||||
|
create: Prisma.XOR<Prisma.UserCreateWithoutGeneratedQuotesInput, Prisma.UserUncheckedCreateWithoutGeneratedQuotesInput>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UserUpsertWithoutGeneratedQuotesInput = {
|
||||||
|
update: Prisma.XOR<Prisma.UserUpdateWithoutGeneratedQuotesInput, Prisma.UserUncheckedUpdateWithoutGeneratedQuotesInput>
|
||||||
|
create: Prisma.XOR<Prisma.UserCreateWithoutGeneratedQuotesInput, Prisma.UserUncheckedCreateWithoutGeneratedQuotesInput>
|
||||||
|
where?: Prisma.UserWhereInput
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UserUpdateToOneWithWhereWithoutGeneratedQuotesInput = {
|
||||||
|
where?: Prisma.UserWhereInput
|
||||||
|
data: Prisma.XOR<Prisma.UserUpdateWithoutGeneratedQuotesInput, Prisma.UserUncheckedUpdateWithoutGeneratedQuotesInput>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UserUpdateWithoutGeneratedQuotesInput = {
|
||||||
|
id?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
permissions?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
login?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
name?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
email?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
emailVerified?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
|
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
cwIdentifier?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
userId?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
token?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
|
roles?: Prisma.RoleUpdateManyWithoutUsersNestedInput
|
||||||
|
sessions?: Prisma.SessionUpdateManyWithoutUserNestedInput
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UserUncheckedUpdateWithoutGeneratedQuotesInput = {
|
||||||
|
id?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
permissions?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
login?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
name?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
email?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
emailVerified?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
|
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
cwIdentifier?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
userId?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
token?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
|
roles?: Prisma.RoleUncheckedUpdateManyWithoutUsersNestedInput
|
||||||
|
sessions?: Prisma.SessionUncheckedUpdateManyWithoutUserNestedInput
|
||||||
|
}
|
||||||
|
|
||||||
export type UserUpdateWithoutRolesInput = {
|
export type UserUpdateWithoutRolesInput = {
|
||||||
id?: Prisma.StringFieldUpdateOperationsInput | string
|
id?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
permissions?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
permissions?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
@@ -674,11 +821,13 @@ export type UserUpdateWithoutRolesInput = {
|
|||||||
email?: Prisma.StringFieldUpdateOperationsInput | string
|
email?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
emailVerified?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
emailVerified?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
cwIdentifier?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
userId?: Prisma.StringFieldUpdateOperationsInput | string
|
userId?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
token?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
token?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
sessions?: Prisma.SessionUpdateManyWithoutUserNestedInput
|
sessions?: Prisma.SessionUpdateManyWithoutUserNestedInput
|
||||||
|
generatedQuotes?: Prisma.GeneratedQuotesUpdateManyWithoutCreatedByNestedInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UserUncheckedUpdateWithoutRolesInput = {
|
export type UserUncheckedUpdateWithoutRolesInput = {
|
||||||
@@ -689,11 +838,13 @@ export type UserUncheckedUpdateWithoutRolesInput = {
|
|||||||
email?: Prisma.StringFieldUpdateOperationsInput | string
|
email?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
emailVerified?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
emailVerified?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
cwIdentifier?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
userId?: Prisma.StringFieldUpdateOperationsInput | string
|
userId?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
token?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
token?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
sessions?: Prisma.SessionUncheckedUpdateManyWithoutUserNestedInput
|
sessions?: Prisma.SessionUncheckedUpdateManyWithoutUserNestedInput
|
||||||
|
generatedQuotes?: Prisma.GeneratedQuotesUncheckedUpdateManyWithoutCreatedByNestedInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UserUncheckedUpdateManyWithoutRolesInput = {
|
export type UserUncheckedUpdateManyWithoutRolesInput = {
|
||||||
@@ -704,6 +855,7 @@ export type UserUncheckedUpdateManyWithoutRolesInput = {
|
|||||||
email?: Prisma.StringFieldUpdateOperationsInput | string
|
email?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
emailVerified?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
emailVerified?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
cwIdentifier?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
userId?: Prisma.StringFieldUpdateOperationsInput | string
|
userId?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
token?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
token?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
@@ -718,11 +870,13 @@ export type UserUncheckedUpdateManyWithoutRolesInput = {
|
|||||||
export type UserCountOutputType = {
|
export type UserCountOutputType = {
|
||||||
roles: number
|
roles: number
|
||||||
sessions: number
|
sessions: number
|
||||||
|
generatedQuotes: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UserCountOutputTypeSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
export type UserCountOutputTypeSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||||
roles?: boolean | UserCountOutputTypeCountRolesArgs
|
roles?: boolean | UserCountOutputTypeCountRolesArgs
|
||||||
sessions?: boolean | UserCountOutputTypeCountSessionsArgs
|
sessions?: boolean | UserCountOutputTypeCountSessionsArgs
|
||||||
|
generatedQuotes?: boolean | UserCountOutputTypeCountGeneratedQuotesArgs
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -749,6 +903,13 @@ export type UserCountOutputTypeCountSessionsArgs<ExtArgs extends runtime.Types.E
|
|||||||
where?: Prisma.SessionWhereInput
|
where?: Prisma.SessionWhereInput
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UserCountOutputType without action
|
||||||
|
*/
|
||||||
|
export type UserCountOutputTypeCountGeneratedQuotesArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||||
|
where?: Prisma.GeneratedQuotesWhereInput
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
export type UserSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
|
export type UserSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
|
||||||
id?: boolean
|
id?: boolean
|
||||||
@@ -758,12 +919,14 @@ export type UserSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = r
|
|||||||
email?: boolean
|
email?: boolean
|
||||||
emailVerified?: boolean
|
emailVerified?: boolean
|
||||||
image?: boolean
|
image?: boolean
|
||||||
|
cwIdentifier?: boolean
|
||||||
userId?: boolean
|
userId?: boolean
|
||||||
token?: boolean
|
token?: boolean
|
||||||
createdAt?: boolean
|
createdAt?: boolean
|
||||||
updatedAt?: boolean
|
updatedAt?: boolean
|
||||||
roles?: boolean | Prisma.User$rolesArgs<ExtArgs>
|
roles?: boolean | Prisma.User$rolesArgs<ExtArgs>
|
||||||
sessions?: boolean | Prisma.User$sessionsArgs<ExtArgs>
|
sessions?: boolean | Prisma.User$sessionsArgs<ExtArgs>
|
||||||
|
generatedQuotes?: boolean | Prisma.User$generatedQuotesArgs<ExtArgs>
|
||||||
_count?: boolean | Prisma.UserCountOutputTypeDefaultArgs<ExtArgs>
|
_count?: boolean | Prisma.UserCountOutputTypeDefaultArgs<ExtArgs>
|
||||||
}, ExtArgs["result"]["user"]>
|
}, ExtArgs["result"]["user"]>
|
||||||
|
|
||||||
@@ -775,6 +938,7 @@ export type UserSelectCreateManyAndReturn<ExtArgs extends runtime.Types.Extensio
|
|||||||
email?: boolean
|
email?: boolean
|
||||||
emailVerified?: boolean
|
emailVerified?: boolean
|
||||||
image?: boolean
|
image?: boolean
|
||||||
|
cwIdentifier?: boolean
|
||||||
userId?: boolean
|
userId?: boolean
|
||||||
token?: boolean
|
token?: boolean
|
||||||
createdAt?: boolean
|
createdAt?: boolean
|
||||||
@@ -789,6 +953,7 @@ export type UserSelectUpdateManyAndReturn<ExtArgs extends runtime.Types.Extensio
|
|||||||
email?: boolean
|
email?: boolean
|
||||||
emailVerified?: boolean
|
emailVerified?: boolean
|
||||||
image?: boolean
|
image?: boolean
|
||||||
|
cwIdentifier?: boolean
|
||||||
userId?: boolean
|
userId?: boolean
|
||||||
token?: boolean
|
token?: boolean
|
||||||
createdAt?: boolean
|
createdAt?: boolean
|
||||||
@@ -803,16 +968,18 @@ export type UserSelectScalar = {
|
|||||||
email?: boolean
|
email?: boolean
|
||||||
emailVerified?: boolean
|
emailVerified?: boolean
|
||||||
image?: boolean
|
image?: boolean
|
||||||
|
cwIdentifier?: boolean
|
||||||
userId?: boolean
|
userId?: boolean
|
||||||
token?: boolean
|
token?: boolean
|
||||||
createdAt?: boolean
|
createdAt?: boolean
|
||||||
updatedAt?: boolean
|
updatedAt?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UserOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "permissions" | "login" | "name" | "email" | "emailVerified" | "image" | "userId" | "token" | "createdAt" | "updatedAt", ExtArgs["result"]["user"]>
|
export type UserOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "permissions" | "login" | "name" | "email" | "emailVerified" | "image" | "cwIdentifier" | "userId" | "token" | "createdAt" | "updatedAt", ExtArgs["result"]["user"]>
|
||||||
export type UserInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
export type UserInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||||
roles?: boolean | Prisma.User$rolesArgs<ExtArgs>
|
roles?: boolean | Prisma.User$rolesArgs<ExtArgs>
|
||||||
sessions?: boolean | Prisma.User$sessionsArgs<ExtArgs>
|
sessions?: boolean | Prisma.User$sessionsArgs<ExtArgs>
|
||||||
|
generatedQuotes?: boolean | Prisma.User$generatedQuotesArgs<ExtArgs>
|
||||||
_count?: boolean | Prisma.UserCountOutputTypeDefaultArgs<ExtArgs>
|
_count?: boolean | Prisma.UserCountOutputTypeDefaultArgs<ExtArgs>
|
||||||
}
|
}
|
||||||
export type UserIncludeCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {}
|
export type UserIncludeCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {}
|
||||||
@@ -823,6 +990,7 @@ export type $UserPayload<ExtArgs extends runtime.Types.Extensions.InternalArgs =
|
|||||||
objects: {
|
objects: {
|
||||||
roles: Prisma.$RolePayload<ExtArgs>[]
|
roles: Prisma.$RolePayload<ExtArgs>[]
|
||||||
sessions: Prisma.$SessionPayload<ExtArgs>[]
|
sessions: Prisma.$SessionPayload<ExtArgs>[]
|
||||||
|
generatedQuotes: Prisma.$GeneratedQuotesPayload<ExtArgs>[]
|
||||||
}
|
}
|
||||||
scalars: runtime.Types.Extensions.GetPayloadResult<{
|
scalars: runtime.Types.Extensions.GetPayloadResult<{
|
||||||
id: string
|
id: string
|
||||||
@@ -832,6 +1000,7 @@ export type $UserPayload<ExtArgs extends runtime.Types.Extensions.InternalArgs =
|
|||||||
email: string
|
email: string
|
||||||
emailVerified: Date | null
|
emailVerified: Date | null
|
||||||
image: string | null
|
image: string | null
|
||||||
|
cwIdentifier: string | null
|
||||||
userId: string
|
userId: string
|
||||||
token: string | null
|
token: string | null
|
||||||
createdAt: Date
|
createdAt: Date
|
||||||
@@ -1232,6 +1401,7 @@ export interface Prisma__UserClient<T, Null = never, ExtArgs extends runtime.Typ
|
|||||||
readonly [Symbol.toStringTag]: "PrismaPromise"
|
readonly [Symbol.toStringTag]: "PrismaPromise"
|
||||||
roles<T extends Prisma.User$rolesArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.User$rolesArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$RolePayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
|
roles<T extends Prisma.User$rolesArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.User$rolesArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$RolePayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
|
||||||
sessions<T extends Prisma.User$sessionsArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.User$sessionsArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$SessionPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
|
sessions<T extends Prisma.User$sessionsArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.User$sessionsArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$SessionPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
|
||||||
|
generatedQuotes<T extends Prisma.User$generatedQuotesArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.User$generatedQuotesArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$GeneratedQuotesPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
|
||||||
/**
|
/**
|
||||||
* Attaches callbacks for the resolution and/or rejection of the Promise.
|
* Attaches callbacks for the resolution and/or rejection of the Promise.
|
||||||
* @param onfulfilled The callback to execute when the Promise is resolved.
|
* @param onfulfilled The callback to execute when the Promise is resolved.
|
||||||
@@ -1268,6 +1438,7 @@ export interface UserFieldRefs {
|
|||||||
readonly email: Prisma.FieldRef<"User", 'String'>
|
readonly email: Prisma.FieldRef<"User", 'String'>
|
||||||
readonly emailVerified: Prisma.FieldRef<"User", 'DateTime'>
|
readonly emailVerified: Prisma.FieldRef<"User", 'DateTime'>
|
||||||
readonly image: Prisma.FieldRef<"User", 'String'>
|
readonly image: Prisma.FieldRef<"User", 'String'>
|
||||||
|
readonly cwIdentifier: Prisma.FieldRef<"User", 'String'>
|
||||||
readonly userId: Prisma.FieldRef<"User", 'String'>
|
readonly userId: Prisma.FieldRef<"User", 'String'>
|
||||||
readonly token: Prisma.FieldRef<"User", 'String'>
|
readonly token: Prisma.FieldRef<"User", 'String'>
|
||||||
readonly createdAt: Prisma.FieldRef<"User", 'DateTime'>
|
readonly createdAt: Prisma.FieldRef<"User", 'DateTime'>
|
||||||
@@ -1707,6 +1878,30 @@ export type User$sessionsArgs<ExtArgs extends runtime.Types.Extensions.InternalA
|
|||||||
distinct?: Prisma.SessionScalarFieldEnum | Prisma.SessionScalarFieldEnum[]
|
distinct?: Prisma.SessionScalarFieldEnum | Prisma.SessionScalarFieldEnum[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User.generatedQuotes
|
||||||
|
*/
|
||||||
|
export type User$generatedQuotesArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||||
|
/**
|
||||||
|
* Select specific fields to fetch from the GeneratedQuotes
|
||||||
|
*/
|
||||||
|
select?: Prisma.GeneratedQuotesSelect<ExtArgs> | null
|
||||||
|
/**
|
||||||
|
* Omit specific fields from the GeneratedQuotes
|
||||||
|
*/
|
||||||
|
omit?: Prisma.GeneratedQuotesOmit<ExtArgs> | null
|
||||||
|
/**
|
||||||
|
* Choose, which related nodes to fetch as well
|
||||||
|
*/
|
||||||
|
include?: Prisma.GeneratedQuotesInclude<ExtArgs> | null
|
||||||
|
where?: Prisma.GeneratedQuotesWhereInput
|
||||||
|
orderBy?: Prisma.GeneratedQuotesOrderByWithRelationInput | Prisma.GeneratedQuotesOrderByWithRelationInput[]
|
||||||
|
cursor?: Prisma.GeneratedQuotesWhereUniqueInput
|
||||||
|
take?: number
|
||||||
|
skip?: number
|
||||||
|
distinct?: Prisma.GeneratedQuotesScalarFieldEnum | Prisma.GeneratedQuotesScalarFieldEnum[]
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* User without action
|
* User without action
|
||||||
*/
|
*/
|
||||||
|
|||||||
+10
-1
@@ -19,13 +19,19 @@
|
|||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "NODE_ENV=development bun --watch src/index.ts",
|
"dev": "NODE_ENV=development bun --watch src/index.ts",
|
||||||
|
"dev:log": "LOG_CW_API=1 NODE_ENV=development bun --watch src/index.ts",
|
||||||
|
"test": "bun test --preload ./tests/setup.ts",
|
||||||
"db:gen": "prisma generate",
|
"db:gen": "prisma generate",
|
||||||
"db:push": "prisma migrate dev --skip-generate",
|
"db:push": "prisma migrate dev --skip-generate",
|
||||||
"db:deploy": "prisma migrate deploy",
|
"db:deploy": "prisma migrate deploy",
|
||||||
"utils:dev": "docker compose -f .docker/docker-compose.yml up --build",
|
"utils:dev": "docker compose -f .docker/docker-compose.yml up --build",
|
||||||
"utils:gen_private_keys": "bun ./utils/genPrivateKeys",
|
"utils:gen_private_keys": "bun ./utils/genPrivateKeys",
|
||||||
"utils:create_admin_role": "bun ./utils/createAdminRole",
|
"utils:create_admin_role": "bun ./utils/createAdminRole",
|
||||||
"utils:assign_user_role": "bun ./utils/assignUserRole"
|
"utils:assign_user_role": "bun ./utils/assignUserRole",
|
||||||
|
"utils:test_webserver": "bun ./utils/testWebserver.ts",
|
||||||
|
"utils:test_adjustments_poll": "bun ./utils/testAdjustmentsPoll.ts",
|
||||||
|
"utils:analyze_cw": "python3 debug-scripts/analyze-cw-calls.py",
|
||||||
|
"db:check": "bunx prisma migrate diff --from-migrations prisma/migrations --to-schema prisma/schema.prisma --shadow-database-url $DATABASE_URL --exit-code"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@azure/msal-node": "^5.0.2",
|
"@azure/msal-node": "^5.0.2",
|
||||||
@@ -39,8 +45,11 @@
|
|||||||
"cors": "^2.8.6",
|
"cors": "^2.8.6",
|
||||||
"cuid": "^3.0.0",
|
"cuid": "^3.0.0",
|
||||||
"hono": "^4.11.5",
|
"hono": "^4.11.5",
|
||||||
|
"ioredis": "^5.10.0",
|
||||||
"jsonwebtoken": "^9.0.3",
|
"jsonwebtoken": "^9.0.3",
|
||||||
"keypair": "^1.0.4",
|
"keypair": "^1.0.4",
|
||||||
|
"pdf-lib": "^1.17.1",
|
||||||
|
"pdfmake": "^0.3.5",
|
||||||
"prisma": "^7.3.0",
|
"prisma": "^7.3.0",
|
||||||
"socket.io": "^4.8.3",
|
"socket.io": "^4.8.3",
|
||||||
"zod": "^4.3.6",
|
"zod": "^4.3.6",
|
||||||
|
|||||||
Executable
+23
@@ -0,0 +1,23 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 1. Resolve any previously failed migrations so deploy can proceed.
|
||||||
|
# Only migrations explicitly marked as "Failed" in the status output are
|
||||||
|
# resolved. We grep for lines containing "Failed" and extract the name.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
echo "[migrate] Checking for failed migrations..."
|
||||||
|
STATUS_OUTPUT=$(bunx prisma migrate status 2>&1 || true)
|
||||||
|
echo "$STATUS_OUTPUT"
|
||||||
|
|
||||||
|
# Only resolve migrations whose status line explicitly says "Failed"
|
||||||
|
echo "$STATUS_OUTPUT" | grep -i "failed" | grep -oE '[0-9]{14}_[a-zA-Z_]+' | while read -r MIGRATION; do
|
||||||
|
echo "[migrate] Resolving failed migration: $MIGRATION"
|
||||||
|
bunx prisma migrate resolve --rolled-back "$MIGRATION" || true
|
||||||
|
done
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 2. Deploy all pending migrations from the migrations directory.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
echo "[migrate] Running prisma migrate deploy..."
|
||||||
|
bunx prisma migrate deploy
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "UnifiSite" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"siteId" TEXT NOT NULL,
|
||||||
|
"companyId" TEXT,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "UnifiSite_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "CatalogItem" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"cwCatalogId" INTEGER NOT NULL,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"description" TEXT,
|
||||||
|
"customerDescription" TEXT,
|
||||||
|
"internalNotes" TEXT,
|
||||||
|
"manufacturer" TEXT,
|
||||||
|
"manufactureCwId" INTEGER,
|
||||||
|
"partNumber" TEXT,
|
||||||
|
"vendorName" TEXT,
|
||||||
|
"vendorSku" TEXT,
|
||||||
|
"vendorCwId" INTEGER,
|
||||||
|
"price" DOUBLE PRECISION NOT NULL,
|
||||||
|
"cost" DOUBLE PRECISION NOT NULL,
|
||||||
|
"inactive" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"salesTaxable" BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
"onHand" INTEGER NOT NULL DEFAULT 0,
|
||||||
|
"cwLastUpdated" TIMESTAMP(3),
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "CatalogItem_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "_LinkedItems" (
|
||||||
|
"A" TEXT NOT NULL,
|
||||||
|
"B" TEXT NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "_LinkedItems_AB_pkey" PRIMARY KEY ("A","B")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "UnifiSite_siteId_key" ON "UnifiSite"("siteId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "CatalogItem_cwCatalogId_key" ON "CatalogItem"("cwCatalogId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "_LinkedItems_B_index" ON "_LinkedItems"("B");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "UnifiSite" ADD CONSTRAINT "UnifiSite_companyId_fkey" FOREIGN KEY ("companyId") REFERENCES "Company"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "_LinkedItems" ADD CONSTRAINT "_LinkedItems_A_fkey" FOREIGN KEY ("A") REFERENCES "CatalogItem"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "_LinkedItems" ADD CONSTRAINT "_LinkedItems_B_fkey" FOREIGN KEY ("B") REFERENCES "CatalogItem"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Opportunity" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"cwOpportunityId" INTEGER NOT NULL,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"notes" TEXT,
|
||||||
|
"typeName" TEXT,
|
||||||
|
"typeCwId" INTEGER,
|
||||||
|
"stageName" TEXT,
|
||||||
|
"stageCwId" INTEGER,
|
||||||
|
"statusName" TEXT,
|
||||||
|
"statusCwId" INTEGER,
|
||||||
|
"priorityName" TEXT,
|
||||||
|
"priorityCwId" INTEGER,
|
||||||
|
"ratingName" TEXT,
|
||||||
|
"ratingCwId" INTEGER,
|
||||||
|
"source" TEXT,
|
||||||
|
"campaignName" TEXT,
|
||||||
|
"campaignCwId" INTEGER,
|
||||||
|
"primarySalesRepName" TEXT,
|
||||||
|
"primarySalesRepIdentifier" TEXT,
|
||||||
|
"primarySalesRepCwId" INTEGER,
|
||||||
|
"secondarySalesRepName" TEXT,
|
||||||
|
"secondarySalesRepIdentifier" TEXT,
|
||||||
|
"secondarySalesRepCwId" INTEGER,
|
||||||
|
"companyCwId" INTEGER,
|
||||||
|
"companyName" TEXT,
|
||||||
|
"contactCwId" INTEGER,
|
||||||
|
"contactName" TEXT,
|
||||||
|
"siteCwId" INTEGER,
|
||||||
|
"siteName" TEXT,
|
||||||
|
"customerPO" TEXT,
|
||||||
|
"totalSalesTax" DOUBLE PRECISION NOT NULL DEFAULT 0,
|
||||||
|
"locationName" TEXT,
|
||||||
|
"locationCwId" INTEGER,
|
||||||
|
"departmentName" TEXT,
|
||||||
|
"departmentCwId" INTEGER,
|
||||||
|
"expectedCloseDate" TIMESTAMP(3),
|
||||||
|
"pipelineChangeDate" TIMESTAMP(3),
|
||||||
|
"dateBecameLead" TIMESTAMP(3),
|
||||||
|
"closedDate" TIMESTAMP(3),
|
||||||
|
"closedFlag" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"closedByName" TEXT,
|
||||||
|
"closedByCwId" INTEGER,
|
||||||
|
"companyId" TEXT,
|
||||||
|
"cwLastUpdated" TIMESTAMP(3),
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "Opportunity_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "Opportunity_cwOpportunityId_key" ON "Opportunity"("cwOpportunityId");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Opportunity" ADD CONSTRAINT "Opportunity_companyId_fkey" FOREIGN KEY ("companyId") REFERENCES "Company"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "CatalogItem" ADD COLUMN "identifier" TEXT;
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "CatalogItem_identifier_key" ON "CatalogItem"("identifier");
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
-- AlterTable: User
|
||||||
|
ALTER TABLE "User" ADD COLUMN "cwIdentifier" TEXT;
|
||||||
|
|
||||||
|
-- AlterTable: CatalogItem
|
||||||
|
ALTER TABLE "CatalogItem" ADD COLUMN "category" TEXT;
|
||||||
|
ALTER TABLE "CatalogItem" ADD COLUMN "categoryCwId" INTEGER;
|
||||||
|
ALTER TABLE "CatalogItem" ADD COLUMN "subcategory" TEXT;
|
||||||
|
ALTER TABLE "CatalogItem" ADD COLUMN "subcategoryCwId" INTEGER;
|
||||||
|
|
||||||
|
-- AlterTable: Opportunity
|
||||||
|
ALTER TABLE "Opportunity" ADD COLUMN "productSequence" INTEGER[] DEFAULT ARRAY[]::INTEGER[];
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "GeneratedQuotes" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"quoteFile" BYTEA NOT NULL,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "GeneratedQuotes_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- Added the required column `opportunityId` to the `GeneratedQuotes` table without a default value. This is not possible if the table is not empty.
|
||||||
|
- Added the required column `quoteFileName` to the `GeneratedQuotes` table without a default value. This is not possible if the table is not empty.
|
||||||
|
- Added the required column `quoteRegenData` to the `GeneratedQuotes` table without a default value. This is not possible if the table is not empty.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "GeneratedQuotes" ADD COLUMN "createdById" TEXT,
|
||||||
|
ADD COLUMN "opportunityId" TEXT NOT NULL,
|
||||||
|
ADD COLUMN "quoteFileName" TEXT NOT NULL,
|
||||||
|
ADD COLUMN "quoteRegenData" JSONB NOT NULL;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "GeneratedQuotes" ADD CONSTRAINT "GeneratedQuotes_opportunityId_fkey" FOREIGN KEY ("opportunityId") REFERENCES "Opportunity"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "GeneratedQuotes" ADD CONSTRAINT "GeneratedQuotes_createdById_fkey" FOREIGN KEY ("createdById") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
-- AlterTable: Opportunity
|
||||||
|
ALTER TABLE "Opportunity" ADD COLUMN "probability" DOUBLE PRECISION NOT NULL DEFAULT 0;
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
-- AlterTable: GeneratedQuotes — add columns missing from prior db push
|
||||||
|
ALTER TABLE "GeneratedQuotes" ADD COLUMN "quoteRegenParams" JSONB NOT NULL DEFAULT '{}';
|
||||||
|
ALTER TABLE "GeneratedQuotes" ADD COLUMN "quoteRegenHash" TEXT NOT NULL DEFAULT '';
|
||||||
|
ALTER TABLE "GeneratedQuotes" ADD COLUMN "downloads" JSONB NOT NULL DEFAULT '[]';
|
||||||
|
|
||||||
|
-- AlterTable: GeneratedQuotes — set default on existing quoteRegenData column
|
||||||
|
ALTER TABLE "GeneratedQuotes" ALTER COLUMN "quoteRegenData" SET DEFAULT '{}';
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "GeneratedQuotes_quoteRegenHash_key" ON "GeneratedQuotes"("quoteRegenHash");
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "CwMember" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"cwMemberId" INTEGER NOT NULL,
|
||||||
|
"identifier" TEXT NOT NULL,
|
||||||
|
"firstName" TEXT NOT NULL,
|
||||||
|
"lastName" TEXT NOT NULL,
|
||||||
|
"officeEmail" TEXT,
|
||||||
|
"inactiveFlag" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"apiKey" TEXT,
|
||||||
|
"cwLastUpdated" TIMESTAMP(3),
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "CwMember_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "CwMember_cwMemberId_key" ON "CwMember"("cwMemberId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "CwMember_identifier_key" ON "CwMember"("identifier");
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Opportunity" ADD COLUMN "cwDateEntered" TIMESTAMP(3);
|
||||||
+128
-4
@@ -34,13 +34,16 @@ model User {
|
|||||||
emailVerified DateTime?
|
emailVerified DateTime?
|
||||||
image String?
|
image String?
|
||||||
|
|
||||||
|
cwIdentifier String?
|
||||||
|
|
||||||
userId String @unique
|
userId String @unique
|
||||||
token String?
|
token String?
|
||||||
|
|
||||||
sessions Session[]
|
sessions Session[]
|
||||||
|
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
generatedQuotes GeneratedQuotes[]
|
||||||
}
|
}
|
||||||
|
|
||||||
model Role {
|
model Role {
|
||||||
@@ -75,8 +78,9 @@ model Company {
|
|||||||
cw_CompanyId Int @unique
|
cw_CompanyId Int @unique
|
||||||
cw_Identifier String @unique
|
cw_Identifier String @unique
|
||||||
|
|
||||||
credentials Credential[]
|
credentials Credential[]
|
||||||
unifiSites UnifiSite[]
|
unifiSites UnifiSite[]
|
||||||
|
opportunities Opportunity[]
|
||||||
|
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
@@ -85,6 +89,7 @@ model Company {
|
|||||||
model CatalogItem {
|
model CatalogItem {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
cwCatalogId Int @unique
|
cwCatalogId Int @unique
|
||||||
|
identifier String? @unique
|
||||||
name String
|
name String
|
||||||
description String?
|
description String?
|
||||||
customerDescription String?
|
customerDescription String?
|
||||||
@@ -93,6 +98,11 @@ model CatalogItem {
|
|||||||
linkedItems CatalogItem[] @relation("LinkedItems")
|
linkedItems CatalogItem[] @relation("LinkedItems")
|
||||||
linkedTo CatalogItem[] @relation("LinkedItems")
|
linkedTo CatalogItem[] @relation("LinkedItems")
|
||||||
|
|
||||||
|
category String?
|
||||||
|
categoryCwId Int?
|
||||||
|
subcategory String?
|
||||||
|
subcategoryCwId Int?
|
||||||
|
|
||||||
manufacturer String?
|
manufacturer String?
|
||||||
manufactureCwId Int?
|
manufactureCwId Int?
|
||||||
|
|
||||||
@@ -115,6 +125,81 @@ model CatalogItem {
|
|||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model Opportunity {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
cwOpportunityId Int @unique
|
||||||
|
name String
|
||||||
|
notes String?
|
||||||
|
|
||||||
|
generatedQuotes GeneratedQuotes[]
|
||||||
|
|
||||||
|
// Stage / status / priority / type / rating stored as JSON references
|
||||||
|
// so we don't need separate lookup tables for CW enums
|
||||||
|
typeName String?
|
||||||
|
typeCwId Int?
|
||||||
|
stageName String?
|
||||||
|
stageCwId Int?
|
||||||
|
statusName String?
|
||||||
|
statusCwId Int?
|
||||||
|
priorityName String?
|
||||||
|
priorityCwId Int?
|
||||||
|
ratingName String?
|
||||||
|
ratingCwId Int?
|
||||||
|
source String?
|
||||||
|
campaignName String?
|
||||||
|
campaignCwId Int?
|
||||||
|
|
||||||
|
// Sales rep references
|
||||||
|
primarySalesRepName String?
|
||||||
|
primarySalesRepIdentifier String?
|
||||||
|
primarySalesRepCwId Int?
|
||||||
|
secondarySalesRepName String?
|
||||||
|
secondarySalesRepIdentifier String?
|
||||||
|
secondarySalesRepCwId Int?
|
||||||
|
|
||||||
|
// Company / contact / site
|
||||||
|
companyCwId Int?
|
||||||
|
companyName String?
|
||||||
|
contactCwId Int?
|
||||||
|
contactName String?
|
||||||
|
siteCwId Int?
|
||||||
|
siteName String?
|
||||||
|
customerPO String?
|
||||||
|
|
||||||
|
// Financials
|
||||||
|
totalSalesTax Float @default(0)
|
||||||
|
probability Float @default(0)
|
||||||
|
|
||||||
|
// Location / department
|
||||||
|
locationName String?
|
||||||
|
locationCwId Int?
|
||||||
|
departmentName String?
|
||||||
|
departmentCwId Int?
|
||||||
|
|
||||||
|
// Dates
|
||||||
|
expectedCloseDate DateTime?
|
||||||
|
pipelineChangeDate DateTime?
|
||||||
|
dateBecameLead DateTime?
|
||||||
|
closedDate DateTime?
|
||||||
|
closedFlag Boolean @default(false)
|
||||||
|
closedByName String?
|
||||||
|
closedByCwId Int?
|
||||||
|
|
||||||
|
// Internal relation to Company (optional, linked by cwCompanyId)
|
||||||
|
companyId String?
|
||||||
|
company Company? @relation(fields: [companyId], references: [id])
|
||||||
|
|
||||||
|
// Local product sequence — array of CW forecast item IDs in display order.
|
||||||
|
// When present, fetchProducts() uses this order instead of CW sequenceNumber.
|
||||||
|
productSequence Int[] @default([])
|
||||||
|
|
||||||
|
cwLastUpdated DateTime?
|
||||||
|
cwDateEntered DateTime?
|
||||||
|
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
}
|
||||||
|
|
||||||
model CredentialType {
|
model CredentialType {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
name String @unique
|
name String @unique
|
||||||
@@ -164,3 +249,42 @@ model Credential {
|
|||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model GeneratedQuotes {
|
||||||
|
id String @id @default(uuid())
|
||||||
|
|
||||||
|
quoteRegenData Json @default("{}") // Store any additional data needed for quote regeneration, such as product details, pricing, etc.
|
||||||
|
quoteRegenParams Json @default("{}") // Store parameters used for quote regeneration, such as template ID, formatting options, etc.
|
||||||
|
quoteRegenHash String @unique @default("")
|
||||||
|
|
||||||
|
downloads Json @default("[]") // Array of download records with timestamp and user info
|
||||||
|
|
||||||
|
quoteFile Bytes
|
||||||
|
quoteFileName String
|
||||||
|
|
||||||
|
opportunityId String
|
||||||
|
opportunity Opportunity @relation(fields: [opportunityId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
|
createdById String?
|
||||||
|
createdBy User? @relation(fields: [createdById], references: [id], onDelete: SetNull)
|
||||||
|
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
}
|
||||||
|
|
||||||
|
model CwMember {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
|
||||||
|
cwMemberId Int @unique
|
||||||
|
identifier String @unique
|
||||||
|
firstName String
|
||||||
|
lastName String
|
||||||
|
officeEmail String?
|
||||||
|
inactiveFlag Boolean @default(false)
|
||||||
|
|
||||||
|
apiKey String?
|
||||||
|
|
||||||
|
cwLastUpdated DateTime?
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Hono } from "hono/tiny";
|
import { Hono } from "hono/tiny";
|
||||||
import { createRoute } from "../../modules/api-utils/createRoute";
|
import { createRoute } from "../../modules/api-utils/createRoute";
|
||||||
import * as msal from "@azure/msal-node";
|
import * as msal from "@azure/msal-node";
|
||||||
import { io, msalClient } from "../../constants";
|
import { API_BASE_URL, io, msalClient } from "../../constants";
|
||||||
import { users } from "../../managers/users";
|
import { users } from "../../managers/users";
|
||||||
|
|
||||||
/* /v1/auth/redirect */
|
/* /v1/auth/redirect */
|
||||||
@@ -11,7 +11,7 @@ export default createRoute("get", ["/redirect"], async (c) => {
|
|||||||
const tokenRequest: msal.AuthorizationCodeRequest = {
|
const tokenRequest: msal.AuthorizationCodeRequest = {
|
||||||
code: c.req.query().code as string,
|
code: c.req.query().code as string,
|
||||||
scopes: ["user.read"],
|
scopes: ["user.read"],
|
||||||
redirectUri: "http://localhost:3000/v1/auth/redirect",
|
redirectUri: `${API_BASE_URL}/v1/auth/redirect`,
|
||||||
};
|
};
|
||||||
|
|
||||||
const authResult = await msalClient.acquireTokenByCode(tokenRequest);
|
const authResult = await msalClient.acquireTokenByCode(tokenRequest);
|
||||||
|
|||||||
+3
-1
@@ -1,5 +1,6 @@
|
|||||||
import { Hono } from "hono/tiny";
|
import { Hono } from "hono/tiny";
|
||||||
import { createRoute } from "../../modules/api-utils/createRoute";
|
import { createRoute } from "../../modules/api-utils/createRoute";
|
||||||
|
import { API_BASE_URL } from "../../constants";
|
||||||
import cuid from "cuid";
|
import cuid from "cuid";
|
||||||
|
|
||||||
/* /v1/auth/uri */
|
/* /v1/auth/uri */
|
||||||
@@ -7,7 +8,8 @@ export default createRoute("get", ["/uri"], (c) => {
|
|||||||
c.status(200);
|
c.status(200);
|
||||||
|
|
||||||
const callbackKey = cuid();
|
const callbackKey = cuid();
|
||||||
const msUri = `https://login.microsoftonline.com/${process.env.MICROSOFT_TENANT_ID}/oauth2/v2.0/authorize?client_id=${process.env.MICROSOFT_CLIENT_ID}&response_type=code&redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fv1%2Fauth%2Fredirect&scope=openid+User.Read&state=${callbackKey}&prompt=login`;
|
const redirectUri = encodeURIComponent(`${API_BASE_URL}/v1/auth/redirect`);
|
||||||
|
const msUri = `https://login.microsoftonline.com/${process.env.MICROSOFT_TENANT_ID}/oauth2/v2.0/authorize?client_id=${process.env.MICROSOFT_CLIENT_ID}&response_type=code&redirect_uri=${redirectUri}&scope=openid+User.Read&state=${callbackKey}&prompt=login`;
|
||||||
|
|
||||||
return c.json({
|
return c.json({
|
||||||
status: 200,
|
status: 200,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { apiResponse } from "../../../modules/api-utils/apiResponse";
|
|||||||
import { ContentfulStatusCode } from "hono/utils/http-status";
|
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||||
import { authMiddleware } from "../../middleware/authorization";
|
import { authMiddleware } from "../../middleware/authorization";
|
||||||
import GenericError from "../../../Errors/GenericError";
|
import GenericError from "../../../Errors/GenericError";
|
||||||
|
import { processObjectValuePerms } from "../../../modules/permission-utils/processObjectPermissions";
|
||||||
|
|
||||||
/* /v1/company/companies/[id] */
|
/* /v1/company/companies/[id] */
|
||||||
export default createRoute(
|
export default createRoute(
|
||||||
@@ -42,13 +43,20 @@ export default createRoute(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const companyData = company.toJson({
|
||||||
|
includeAddress,
|
||||||
|
includePrimaryContact,
|
||||||
|
includeAllContacts,
|
||||||
|
});
|
||||||
|
const gatedData = await processObjectValuePerms(
|
||||||
|
companyData,
|
||||||
|
"obj.company",
|
||||||
|
c.get("user"),
|
||||||
|
);
|
||||||
|
|
||||||
const response = apiResponse.successful(
|
const response = apiResponse.successful(
|
||||||
"Company Fetched Successfully!",
|
"Company Fetched Successfully!",
|
||||||
company.toJson({
|
gatedData,
|
||||||
includeAddress,
|
|
||||||
includePrimaryContact,
|
|
||||||
includeAllContacts,
|
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
return c.json(response, response.status as ContentfulStatusCode);
|
return c.json(response, response.status as ContentfulStatusCode);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { createRoute } from "../../../modules/api-utils/createRoute";
|
||||||
|
import { companies } from "../../../managers/companies";
|
||||||
|
import { apiResponse } from "../../../modules/api-utils/apiResponse";
|
||||||
|
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||||
|
import { authMiddleware } from "../../middleware/authorization";
|
||||||
|
|
||||||
|
/* /v1/company/companies/[id]/sites */
|
||||||
|
export default createRoute(
|
||||||
|
"get",
|
||||||
|
["/companies/:identifier/sites"],
|
||||||
|
|
||||||
|
async (c) => {
|
||||||
|
const company = await companies.fetch(c.req.param("identifier"));
|
||||||
|
const sites = await company.fetchSites();
|
||||||
|
|
||||||
|
const response = apiResponse.successful(
|
||||||
|
"Company Sites Fetched Successfully!",
|
||||||
|
sites,
|
||||||
|
);
|
||||||
|
return c.json(response, response.status as ContentfulStatusCode);
|
||||||
|
},
|
||||||
|
authMiddleware({
|
||||||
|
permissions: ["company.fetch", "company.fetch.sites"],
|
||||||
|
}),
|
||||||
|
);
|
||||||
@@ -4,6 +4,7 @@ import { companies } from "../../../managers/companies";
|
|||||||
import { apiResponse } from "../../../modules/api-utils/apiResponse";
|
import { apiResponse } from "../../../modules/api-utils/apiResponse";
|
||||||
import { ContentfulStatusCode } from "hono/utils/http-status";
|
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||||
import { authMiddleware } from "../../middleware/authorization";
|
import { authMiddleware } from "../../middleware/authorization";
|
||||||
|
import { processObjectValuePerms } from "../../../modules/permission-utils/processObjectPermissions";
|
||||||
|
|
||||||
/* GET /v1/company/companies/:identifier/unifi/sites */
|
/* GET /v1/company/companies/:identifier/unifi/sites */
|
||||||
export default createRoute(
|
export default createRoute(
|
||||||
@@ -12,9 +13,16 @@ export default createRoute(
|
|||||||
async (c) => {
|
async (c) => {
|
||||||
const company = await companies.fetch(c.req.param("identifier"));
|
const company = await companies.fetch(c.req.param("identifier"));
|
||||||
const sites = await unifiSites.fetchByCompany(company.id);
|
const sites = await unifiSites.fetchByCompany(company.id);
|
||||||
|
|
||||||
|
const gatedData = await Promise.all(
|
||||||
|
sites.map((site) =>
|
||||||
|
processObjectValuePerms(site, "obj.unifiSite", c.get("user")),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
const response = apiResponse.successful(
|
const response = apiResponse.successful(
|
||||||
"Company UniFi Sites Fetched Successfully!",
|
"Company UniFi Sites Fetched Successfully!",
|
||||||
sites,
|
gatedData,
|
||||||
);
|
);
|
||||||
return c.json(response, response.status as ContentfulStatusCode);
|
return c.json(response, response.status as ContentfulStatusCode);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { companies } from "../../managers/companies";
|
|||||||
import { apiResponse } from "../../modules/api-utils/apiResponse";
|
import { apiResponse } from "../../modules/api-utils/apiResponse";
|
||||||
import { ContentfulStatusCode } from "hono/utils/http-status";
|
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||||
import { authMiddleware } from "../middleware/authorization";
|
import { authMiddleware } from "../middleware/authorization";
|
||||||
|
import { processObjectValuePerms } from "../../modules/permission-utils/processObjectPermissions";
|
||||||
|
|
||||||
/* /v1/company/companies */
|
/* /v1/company/companies */
|
||||||
export default createRoute(
|
export default createRoute(
|
||||||
@@ -22,9 +23,15 @@ export default createRoute(
|
|||||||
? (await companies.search(search, 1, 999999)).length
|
? (await companies.search(search, 1, 999999)).length
|
||||||
: await companies.count();
|
: await companies.count();
|
||||||
|
|
||||||
|
const gatedData = await Promise.all(
|
||||||
|
data.map((item) =>
|
||||||
|
processObjectValuePerms(item, "obj.company", c.get("user")),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
let response = apiResponse.successful(
|
let response = apiResponse.successful(
|
||||||
"Companies Fetched Successfully!",
|
"Companies Fetched Successfully!",
|
||||||
data,
|
gatedData,
|
||||||
{
|
{
|
||||||
pagination: {
|
pagination: {
|
||||||
previousPage: page == 1 ? null : page - 1, // Previous Page
|
previousPage: page == 1 ? null : page - 1, // Previous Page
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { default as fetchAll } from "./fetchAll";
|
import { default as fetchAll } from "./fetchAll";
|
||||||
import { default as fetch } from "./[id]/fetch";
|
import { default as fetch } from "./[id]/fetch";
|
||||||
import { default as configurations } from "./[id]/configurations";
|
import { default as configurations } from "./[id]/configurations";
|
||||||
|
import { default as sites } from "./[id]/sites";
|
||||||
import { default as unifiSites } from "./[id]/unifiSites";
|
import { default as unifiSites } from "./[id]/unifiSites";
|
||||||
import { default as count } from "./count";
|
import { default as count } from "./count";
|
||||||
|
|
||||||
export { configurations, count, fetch, fetchAll, unifiSites };
|
export { configurations, count, fetch, fetchAll, sites, unifiSites };
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { credentialTypes } from "../../managers/credentialTypes";
|
|||||||
import { apiResponse } from "../../modules/api-utils/apiResponse";
|
import { apiResponse } from "../../modules/api-utils/apiResponse";
|
||||||
import { ContentfulStatusCode } from "hono/utils/http-status";
|
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||||
import { authMiddleware } from "../middleware/authorization";
|
import { authMiddleware } from "../middleware/authorization";
|
||||||
|
import { processObjectValuePerms } from "../../modules/permission-utils/processObjectPermissions";
|
||||||
|
|
||||||
/* /v1/credential-type/:identifier */
|
/* /v1/credential-type/:identifier */
|
||||||
export default createRoute(
|
export default createRoute(
|
||||||
@@ -15,9 +16,15 @@ export default createRoute(
|
|||||||
c.req.param("identifier"),
|
c.req.param("identifier"),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const gatedData = await processObjectValuePerms(
|
||||||
|
credentialType.toJson({ includeCredentialCount: true }),
|
||||||
|
"obj.credentialType",
|
||||||
|
c.get("user"),
|
||||||
|
);
|
||||||
|
|
||||||
const response = apiResponse.successful(
|
const response = apiResponse.successful(
|
||||||
"Credential Type Fetched Successfully!",
|
"Credential Type Fetched Successfully!",
|
||||||
credentialType.toJson({ includeCredentialCount: true }),
|
gatedData,
|
||||||
);
|
);
|
||||||
return c.json(response, response.status as ContentfulStatusCode);
|
return c.json(response, response.status as ContentfulStatusCode);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { credentialTypes } from "../../managers/credentialTypes";
|
|||||||
import { apiResponse } from "../../modules/api-utils/apiResponse";
|
import { apiResponse } from "../../modules/api-utils/apiResponse";
|
||||||
import { ContentfulStatusCode } from "hono/utils/http-status";
|
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||||
import { authMiddleware } from "../middleware/authorization";
|
import { authMiddleware } from "../middleware/authorization";
|
||||||
|
import { processObjectValuePerms } from "../../modules/permission-utils/processObjectPermissions";
|
||||||
|
|
||||||
/* /v1/credential-type */
|
/* /v1/credential-type */
|
||||||
export default createRoute(
|
export default createRoute(
|
||||||
@@ -13,11 +14,19 @@ export default createRoute(
|
|||||||
async (c) => {
|
async (c) => {
|
||||||
const allCredentialTypes = await credentialTypes.fetchAll();
|
const allCredentialTypes = await credentialTypes.fetchAll();
|
||||||
|
|
||||||
|
const gatedData = await Promise.all(
|
||||||
|
allCredentialTypes.map((ct) =>
|
||||||
|
processObjectValuePerms(
|
||||||
|
ct.toJson({ includeCredentialCount: true }),
|
||||||
|
"obj.credentialType",
|
||||||
|
c.get("user"),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
const response = apiResponse.successful(
|
const response = apiResponse.successful(
|
||||||
"Credential Types Fetched Successfully!",
|
"Credential Types Fetched Successfully!",
|
||||||
allCredentialTypes.map((ct) =>
|
gatedData,
|
||||||
ct.toJson({ includeCredentialCount: true }),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
return c.json(response, response.status as ContentfulStatusCode);
|
return c.json(response, response.status as ContentfulStatusCode);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { credentialTypes } from "../../managers/credentialTypes";
|
|||||||
import { apiResponse } from "../../modules/api-utils/apiResponse";
|
import { apiResponse } from "../../modules/api-utils/apiResponse";
|
||||||
import { ContentfulStatusCode } from "hono/utils/http-status";
|
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||||
import { authMiddleware } from "../middleware/authorization";
|
import { authMiddleware } from "../middleware/authorization";
|
||||||
|
import { processObjectValuePerms } from "../../modules/permission-utils/processObjectPermissions";
|
||||||
|
|
||||||
/* /v1/credential-type/:id/credentials */
|
/* /v1/credential-type/:id/credentials */
|
||||||
export default createRoute(
|
export default createRoute(
|
||||||
@@ -14,9 +15,15 @@ export default createRoute(
|
|||||||
const credentialType = await credentialTypes.fetch(c.req.param("id"));
|
const credentialType = await credentialTypes.fetch(c.req.param("id"));
|
||||||
const credentials = await credentialType.fetchCredentials();
|
const credentials = await credentialType.fetchCredentials();
|
||||||
|
|
||||||
|
const gatedData = await Promise.all(
|
||||||
|
credentials.map((cred) =>
|
||||||
|
processObjectValuePerms(cred.toJson(), "obj.credential", c.get("user")),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
const response = apiResponse.successful(
|
const response = apiResponse.successful(
|
||||||
"Credentials Fetched Successfully!",
|
"Credentials Fetched Successfully!",
|
||||||
credentials.map((cred) => cred.toJson()),
|
gatedData,
|
||||||
);
|
);
|
||||||
return c.json(response, response.status as ContentfulStatusCode);
|
return c.json(response, response.status as ContentfulStatusCode);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { credentials } from "../../managers/credentials";
|
|||||||
import { apiResponse } from "../../modules/api-utils/apiResponse";
|
import { apiResponse } from "../../modules/api-utils/apiResponse";
|
||||||
import { ContentfulStatusCode } from "hono/utils/http-status";
|
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||||
import { authMiddleware } from "../middleware/authorization";
|
import { authMiddleware } from "../middleware/authorization";
|
||||||
|
import { processObjectValuePerms } from "../../modules/permission-utils/processObjectPermissions";
|
||||||
|
|
||||||
/* /v1/credential/:id */
|
/* /v1/credential/:id */
|
||||||
export default createRoute(
|
export default createRoute(
|
||||||
@@ -12,10 +13,15 @@ export default createRoute(
|
|||||||
|
|
||||||
async (c) => {
|
async (c) => {
|
||||||
const credential = await credentials.fetch(c.req.param("id"));
|
const credential = await credentials.fetch(c.req.param("id"));
|
||||||
|
const gatedData = await processObjectValuePerms(
|
||||||
|
credential.toJson(),
|
||||||
|
"obj.credential",
|
||||||
|
c.get("user"),
|
||||||
|
);
|
||||||
|
|
||||||
const response = apiResponse.successful(
|
const response = apiResponse.successful(
|
||||||
"Credential Fetched Successfully!",
|
"Credential Fetched Successfully!",
|
||||||
credential.toJson(),
|
gatedData,
|
||||||
);
|
);
|
||||||
return c.json(response, response.status as ContentfulStatusCode);
|
return c.json(response, response.status as ContentfulStatusCode);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { credentials } from "../../managers/credentials";
|
|||||||
import { apiResponse } from "../../modules/api-utils/apiResponse";
|
import { apiResponse } from "../../modules/api-utils/apiResponse";
|
||||||
import { ContentfulStatusCode } from "hono/utils/http-status";
|
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||||
import { authMiddleware } from "../middleware/authorization";
|
import { authMiddleware } from "../middleware/authorization";
|
||||||
|
import { processObjectValuePerms } from "../../modules/permission-utils/processObjectPermissions";
|
||||||
|
|
||||||
/* /v1/credential/company/:companyId */
|
/* /v1/credential/company/:companyId */
|
||||||
export default createRoute(
|
export default createRoute(
|
||||||
@@ -15,9 +16,15 @@ export default createRoute(
|
|||||||
c.req.param("companyId"),
|
c.req.param("companyId"),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const gatedData = await Promise.all(
|
||||||
|
companyCredentials.map((cred) =>
|
||||||
|
processObjectValuePerms(cred.toJson(), "obj.credential", c.get("user")),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
const response = apiResponse.successful(
|
const response = apiResponse.successful(
|
||||||
"Company Credentials Fetched Successfully!",
|
"Company Credentials Fetched Successfully!",
|
||||||
companyCredentials.map((cred) => cred.toJson()),
|
gatedData,
|
||||||
);
|
);
|
||||||
return c.json(response, response.status as ContentfulStatusCode);
|
return c.json(response, response.status as ContentfulStatusCode);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { credentials } from "../../managers/credentials";
|
|||||||
import { apiResponse } from "../../modules/api-utils/apiResponse";
|
import { apiResponse } from "../../modules/api-utils/apiResponse";
|
||||||
import { ContentfulStatusCode } from "hono/utils/http-status";
|
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||||
import { authMiddleware } from "../middleware/authorization";
|
import { authMiddleware } from "../middleware/authorization";
|
||||||
|
import { processObjectValuePerms } from "../../modules/permission-utils/processObjectPermissions";
|
||||||
|
|
||||||
/* GET /v1/credential/credentials/:id/sub-credentials */
|
/* GET /v1/credential/credentials/:id/sub-credentials */
|
||||||
export default createRoute(
|
export default createRoute(
|
||||||
@@ -17,9 +18,15 @@ export default createRoute(
|
|||||||
|
|
||||||
const subCredentials = await credentials.fetchSubCredentials(parentId);
|
const subCredentials = await credentials.fetchSubCredentials(parentId);
|
||||||
|
|
||||||
|
const gatedData = await Promise.all(
|
||||||
|
subCredentials.map((sc) =>
|
||||||
|
processObjectValuePerms(sc.toJson(), "obj.credential", c.get("user")),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
const response = apiResponse.successful(
|
const response = apiResponse.successful(
|
||||||
"Sub-Credentials Fetched Successfully!",
|
"Sub-Credentials Fetched Successfully!",
|
||||||
subCredentials.map((sc) => sc.toJson()),
|
gatedData,
|
||||||
);
|
);
|
||||||
return c.json(response, response.status as ContentfulStatusCode);
|
return c.json(response, response.status as ContentfulStatusCode);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,164 @@
|
|||||||
|
import { createRoute } from "../../modules/api-utils/createRoute";
|
||||||
|
import { apiResponse } from "../../modules/api-utils/apiResponse";
|
||||||
|
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||||
|
import { z } from "zod";
|
||||||
|
import GenericError from "../../Errors/GenericError";
|
||||||
|
|
||||||
|
type ParsedJson = Record<string, unknown> | unknown[];
|
||||||
|
|
||||||
|
const callbackResource = z.enum([
|
||||||
|
"opportunity",
|
||||||
|
"ticket",
|
||||||
|
"company",
|
||||||
|
"activity",
|
||||||
|
]);
|
||||||
|
|
||||||
|
const safeParseJson = (value: string): ParsedJson | null => {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(value);
|
||||||
|
const isObject = typeof parsed === "object" && parsed !== null;
|
||||||
|
|
||||||
|
return isObject ? (parsed as ParsedJson) : null;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const asObject = (value: ParsedJson | null): Record<string, unknown> | null => {
|
||||||
|
if (!value) return null;
|
||||||
|
if (Array.isArray(value)) return null;
|
||||||
|
|
||||||
|
return value;
|
||||||
|
};
|
||||||
|
|
||||||
|
const parseJsonStringFields = (
|
||||||
|
value: Record<string, unknown> | null,
|
||||||
|
): Record<string, unknown> | null => {
|
||||||
|
if (!value) return null;
|
||||||
|
|
||||||
|
return Object.entries(value).reduce<Record<string, unknown>>(
|
||||||
|
(acc, [key, current]) => {
|
||||||
|
if (typeof current !== "string") {
|
||||||
|
acc[key] = current;
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}
|
||||||
|
|
||||||
|
const looksLikeJson = current.startsWith("{") || current.startsWith("[");
|
||||||
|
if (!looksLikeJson) {
|
||||||
|
acc[key] = current;
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsed = safeParseJson(current);
|
||||||
|
acc[key] = parsed ?? current;
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const parseEntity = (value: unknown): ParsedJson | null => {
|
||||||
|
if (typeof value === "string") return safeParseJson(value);
|
||||||
|
if (typeof value !== "object" || value === null) return null;
|
||||||
|
|
||||||
|
return value as ParsedJson;
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildSummary = (
|
||||||
|
resource: z.infer<typeof callbackResource>,
|
||||||
|
parsedBody: Record<string, unknown> | null,
|
||||||
|
parsedEntity: Record<string, unknown> | null,
|
||||||
|
) => {
|
||||||
|
if (!parsedBody) return null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
resource,
|
||||||
|
messageId: parsedBody.MessageId ?? null,
|
||||||
|
action: parsedBody.Action ?? null,
|
||||||
|
type: parsedBody.Type ?? null,
|
||||||
|
id: parsedBody.ID ?? null,
|
||||||
|
memberId: parsedBody.MemberId ?? null,
|
||||||
|
entityStatus:
|
||||||
|
parsedEntity?.StatusName ??
|
||||||
|
parsedEntity?.TicketStatus ??
|
||||||
|
parsedEntity?.Status ??
|
||||||
|
null,
|
||||||
|
entitySummary: parsedEntity?.Summary ?? parsedEntity?.CompanyName ?? null,
|
||||||
|
entityUpdatedBy: parsedEntity?.UpdatedBy ?? null,
|
||||||
|
entityLastUpdated:
|
||||||
|
parsedEntity?.LastUpdatedUTC ?? parsedEntity?.LastUpdated ?? null,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const parseHeaders = (headers: Headers): Record<string, string> =>
|
||||||
|
Object.fromEntries(headers.entries());
|
||||||
|
|
||||||
|
const callbackHeaderSummary = (headers: Record<string, string>) => ({
|
||||||
|
contentType: headers["content-type"] ?? null,
|
||||||
|
userAgent: headers["user-agent"] ?? null,
|
||||||
|
host: headers.host ?? null,
|
||||||
|
forwardedFor: headers["x-forwarded-for"] ?? null,
|
||||||
|
callbackId:
|
||||||
|
headers["x-cw-request-id"] ??
|
||||||
|
headers["x-request-id"] ??
|
||||||
|
headers["x-correlation-id"] ??
|
||||||
|
null,
|
||||||
|
});
|
||||||
|
|
||||||
|
/* /v1/cw/callback/:resource */
|
||||||
|
export default createRoute("post", ["/callback/:secret/:resource"], async (c) => {
|
||||||
|
const suppliedSecret = c.req.param("secret");
|
||||||
|
const expectedSecret = process.env.CW_CALLBACK_SECRET;
|
||||||
|
|
||||||
|
if (expectedSecret && suppliedSecret !== expectedSecret) {
|
||||||
|
throw new GenericError({
|
||||||
|
name: "Unauthorized",
|
||||||
|
message: "Invalid callback secret.",
|
||||||
|
cause: "Path secret mismatch",
|
||||||
|
status: 401,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!expectedSecret) {
|
||||||
|
console.warn(
|
||||||
|
"[cw-callback] CW_CALLBACK_SECRET is not configured; accepting path secret without verification",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const resource = callbackResource.parse(c.req.param("resource"));
|
||||||
|
const headers = parseHeaders(c.req.raw.headers);
|
||||||
|
const headerSummary = callbackHeaderSummary(headers);
|
||||||
|
const rawBody = await c.req.text();
|
||||||
|
const parsedJson = safeParseJson(rawBody);
|
||||||
|
const parsedBody = asObject(parsedJson);
|
||||||
|
const parsedBodyExpanded = parseJsonStringFields(parsedBody);
|
||||||
|
const parsedEntity = asObject(parseEntity(parsedBodyExpanded?.Entity));
|
||||||
|
const summary = buildSummary(resource, parsedBodyExpanded, parsedEntity);
|
||||||
|
|
||||||
|
const line = [
|
||||||
|
`[cw-callback] resource=${resource}`,
|
||||||
|
`action=${String(summary?.action ?? "-")}`,
|
||||||
|
`type=${String(summary?.type ?? "-")}`,
|
||||||
|
`id=${String(summary?.id ?? "-")}`,
|
||||||
|
`by=${String(summary?.entityUpdatedBy ?? summary?.memberId ?? "-")}`,
|
||||||
|
`requestId=${String(headerSummary.callbackId ?? "-")}`,
|
||||||
|
`status=${String(summary?.entityStatus ?? "-")}`,
|
||||||
|
`summary=${String(summary?.entitySummary ?? "-")}`,
|
||||||
|
].join(" ");
|
||||||
|
console.log(line);
|
||||||
|
|
||||||
|
const response = apiResponse.successful("CW callback received.", {
|
||||||
|
resource,
|
||||||
|
secretValidated: Boolean(expectedSecret),
|
||||||
|
summary,
|
||||||
|
headers,
|
||||||
|
headerSummary,
|
||||||
|
bodyParsed: parsedBodyExpanded,
|
||||||
|
receivedAt: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
return c.json(response, response.status as ContentfulStatusCode);
|
||||||
|
});
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import { createRoute } from "../../modules/api-utils/createRoute";
|
||||||
|
import { apiResponse } from "../../modules/api-utils/apiResponse";
|
||||||
|
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||||
|
import { authMiddleware } from "../middleware/authorization";
|
||||||
|
import { getMemberCache } from "../../modules/cw-utils/members/memberCache";
|
||||||
|
|
||||||
|
/* GET /v1/cw/members */
|
||||||
|
export default createRoute(
|
||||||
|
"get",
|
||||||
|
["/members"],
|
||||||
|
async (c) => {
|
||||||
|
const cache = await getMemberCache();
|
||||||
|
|
||||||
|
const activeOnly = c.req.query("active") !== "false";
|
||||||
|
|
||||||
|
const members = cache
|
||||||
|
.filter((m) => (activeOnly ? !m.inactiveFlag : true))
|
||||||
|
.map((m) => ({
|
||||||
|
id: m.id,
|
||||||
|
identifier: m.identifier,
|
||||||
|
firstName: m.firstName,
|
||||||
|
lastName: m.lastName,
|
||||||
|
name: `${m.firstName} ${m.lastName}`.trim(),
|
||||||
|
officeEmail: m.officeEmail,
|
||||||
|
inactive: m.inactiveFlag,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const sorted = members.sort((a, b) => a.name.localeCompare(b.name));
|
||||||
|
|
||||||
|
const response = apiResponse.successful(
|
||||||
|
"CW members fetched successfully!",
|
||||||
|
sorted,
|
||||||
|
);
|
||||||
|
return c.json(response, response.status as ContentfulStatusCode);
|
||||||
|
},
|
||||||
|
authMiddleware(),
|
||||||
|
);
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
import { default as callback } from "./callback";
|
||||||
|
import { default as fetchMembers } from "./fetchMembers";
|
||||||
|
|
||||||
|
export { callback, fetchMembers };
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import { createRoute } from "../../../modules/api-utils/createRoute";
|
||||||
|
import { procurement } from "../../../managers/procurement";
|
||||||
|
import { apiResponse } from "../../../modules/api-utils/apiResponse";
|
||||||
|
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||||
|
import { authMiddleware } from "../../middleware/authorization";
|
||||||
|
import { processObjectValuePerms } from "../../../modules/permission-utils/processObjectPermissions";
|
||||||
|
|
||||||
|
/* /v1/procurement/items/:identifier */
|
||||||
|
export default createRoute(
|
||||||
|
"get",
|
||||||
|
["/items/:identifier"],
|
||||||
|
async (c) => {
|
||||||
|
const identifier = c.req.param("identifier");
|
||||||
|
const includeLinkedItems = c.req.query("includeLinkedItems") === "true";
|
||||||
|
|
||||||
|
const item = await procurement.fetchItem(identifier);
|
||||||
|
|
||||||
|
const gatedData = await processObjectValuePerms(
|
||||||
|
item.toJson({ includeLinkedItems }),
|
||||||
|
"obj.catalogItem",
|
||||||
|
c.get("user"),
|
||||||
|
);
|
||||||
|
|
||||||
|
const response = apiResponse.successful(
|
||||||
|
"Catalog item fetched successfully!",
|
||||||
|
gatedData,
|
||||||
|
);
|
||||||
|
|
||||||
|
return c.json(response, response.status as ContentfulStatusCode);
|
||||||
|
},
|
||||||
|
authMiddleware({ permissions: ["procurement.catalog.fetch"] }),
|
||||||
|
);
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import { createRoute } from "../../../modules/api-utils/createRoute";
|
||||||
|
import { procurement } from "../../../managers/procurement";
|
||||||
|
import { apiResponse } from "../../../modules/api-utils/apiResponse";
|
||||||
|
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||||
|
import { authMiddleware } from "../../middleware/authorization";
|
||||||
|
import { processObjectValuePerms } from "../../../modules/permission-utils/processObjectPermissions";
|
||||||
|
|
||||||
|
/* GET /v1/procurement/items/:identifier/linked */
|
||||||
|
export default createRoute(
|
||||||
|
"get",
|
||||||
|
["/items/:identifier/linked"],
|
||||||
|
async (c) => {
|
||||||
|
const identifier = c.req.param("identifier");
|
||||||
|
const item = await procurement.fetchItem(identifier);
|
||||||
|
|
||||||
|
const linkedItems = item.getLinkedItems().map((linked) => linked.toJson());
|
||||||
|
|
||||||
|
const gatedData = await Promise.all(
|
||||||
|
linkedItems.map((linked) =>
|
||||||
|
processObjectValuePerms(linked, "obj.catalogItem", c.get("user")),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const response = apiResponse.successful(
|
||||||
|
"Linked catalog items fetched successfully!",
|
||||||
|
gatedData,
|
||||||
|
);
|
||||||
|
|
||||||
|
return c.json(response, response.status as ContentfulStatusCode);
|
||||||
|
},
|
||||||
|
authMiddleware({ permissions: ["procurement.catalog.fetch"] }),
|
||||||
|
);
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { createRoute } from "../../../modules/api-utils/createRoute";
|
||||||
|
import { procurement } from "../../../managers/procurement";
|
||||||
|
import { apiResponse } from "../../../modules/api-utils/apiResponse";
|
||||||
|
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||||
|
import { authMiddleware } from "../../middleware/authorization";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
/* POST /v1/procurement/items/:identifier/link */
|
||||||
|
export default createRoute(
|
||||||
|
"post",
|
||||||
|
["/items/:identifier/link"],
|
||||||
|
async (c) => {
|
||||||
|
const identifier = c.req.param("identifier");
|
||||||
|
const body = await c.req.json();
|
||||||
|
const schema = z.object({ targetId: z.string() }).strict();
|
||||||
|
const { targetId } = schema.parse(body);
|
||||||
|
|
||||||
|
const item = await procurement.linkItems(identifier, targetId);
|
||||||
|
|
||||||
|
const response = apiResponse.successful(
|
||||||
|
"Catalog item linked successfully!",
|
||||||
|
item.toJson({ includeLinkedItems: true }),
|
||||||
|
);
|
||||||
|
|
||||||
|
return c.json(response, response.status as ContentfulStatusCode);
|
||||||
|
},
|
||||||
|
authMiddleware({ permissions: ["procurement.catalog.link"] }),
|
||||||
|
);
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { createRoute } from "../../../modules/api-utils/createRoute";
|
||||||
|
import { procurement } from "../../../managers/procurement";
|
||||||
|
import { apiResponse } from "../../../modules/api-utils/apiResponse";
|
||||||
|
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||||
|
import { authMiddleware } from "../../middleware/authorization";
|
||||||
|
|
||||||
|
/* /v1/procurement/items/:identifier/refresh-inventory */
|
||||||
|
export default createRoute(
|
||||||
|
"post",
|
||||||
|
["/items/:identifier/refresh-inventory"],
|
||||||
|
async (c) => {
|
||||||
|
const identifier = c.req.param("identifier");
|
||||||
|
const item = await procurement.fetchItem(identifier);
|
||||||
|
|
||||||
|
await item.refreshInventory();
|
||||||
|
|
||||||
|
const response = apiResponse.successful(
|
||||||
|
"Inventory refreshed successfully!",
|
||||||
|
item.toJson(),
|
||||||
|
);
|
||||||
|
|
||||||
|
return c.json(response, response.status as ContentfulStatusCode);
|
||||||
|
},
|
||||||
|
authMiddleware({ permissions: ["procurement.catalog.inventory.refresh"] }),
|
||||||
|
);
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { createRoute } from "../../../modules/api-utils/createRoute";
|
||||||
|
import { procurement } from "../../../managers/procurement";
|
||||||
|
import { apiResponse } from "../../../modules/api-utils/apiResponse";
|
||||||
|
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||||
|
import { authMiddleware } from "../../middleware/authorization";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
/* POST /v1/procurement/items/:identifier/unlink */
|
||||||
|
export default createRoute(
|
||||||
|
"post",
|
||||||
|
["/items/:identifier/unlink"],
|
||||||
|
async (c) => {
|
||||||
|
const identifier = c.req.param("identifier");
|
||||||
|
const body = await c.req.json();
|
||||||
|
const schema = z.object({ targetId: z.string() }).strict();
|
||||||
|
const { targetId } = schema.parse(body);
|
||||||
|
|
||||||
|
const item = await procurement.unlinkItems(identifier, targetId);
|
||||||
|
|
||||||
|
const response = apiResponse.successful(
|
||||||
|
"Catalog item unlinked successfully!",
|
||||||
|
item.toJson({ includeLinkedItems: true }),
|
||||||
|
);
|
||||||
|
|
||||||
|
return c.json(response, response.status as ContentfulStatusCode);
|
||||||
|
},
|
||||||
|
authMiddleware({ permissions: ["procurement.catalog.link"] }),
|
||||||
|
);
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { createRoute } from "../../modules/api-utils/createRoute";
|
||||||
|
import { apiResponse } from "../../modules/api-utils/apiResponse";
|
||||||
|
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||||
|
import { authMiddleware } from "../middleware/authorization";
|
||||||
|
import {
|
||||||
|
serializeCategoryTree,
|
||||||
|
serializeEcosystemTree,
|
||||||
|
} from "../../modules/catalog-categories/catalogCategories";
|
||||||
|
|
||||||
|
/* /v1/procurement/categories */
|
||||||
|
export default createRoute(
|
||||||
|
"get",
|
||||||
|
["/categories"],
|
||||||
|
async (c) => {
|
||||||
|
const categories = serializeCategoryTree();
|
||||||
|
const ecosystems = serializeEcosystemTree();
|
||||||
|
|
||||||
|
const response = apiResponse.successful(
|
||||||
|
"Category and ecosystem data fetched successfully!",
|
||||||
|
{ categories, ecosystems },
|
||||||
|
);
|
||||||
|
|
||||||
|
return c.json(response, response.status as ContentfulStatusCode);
|
||||||
|
},
|
||||||
|
authMiddleware({ permissions: ["procurement.catalog.fetch.many"] }),
|
||||||
|
);
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { createRoute } from "../../modules/api-utils/createRoute";
|
||||||
|
import { procurement } from "../../managers/procurement";
|
||||||
|
import { apiResponse } from "../../modules/api-utils/apiResponse";
|
||||||
|
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||||
|
import { authMiddleware } from "../middleware/authorization";
|
||||||
|
|
||||||
|
/* /v1/procurement/count */
|
||||||
|
export default createRoute(
|
||||||
|
"get",
|
||||||
|
["/count"],
|
||||||
|
async (c) => {
|
||||||
|
const activeOnly = c.req.query("activeOnly") === "true";
|
||||||
|
|
||||||
|
const count = await procurement.count({ activeOnly });
|
||||||
|
|
||||||
|
const response = apiResponse.successful(
|
||||||
|
"Catalog item count fetched successfully!",
|
||||||
|
{ count },
|
||||||
|
);
|
||||||
|
|
||||||
|
return c.json(response, response.status as ContentfulStatusCode);
|
||||||
|
},
|
||||||
|
authMiddleware({ permissions: ["procurement.catalog.fetch.many"] }),
|
||||||
|
);
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
import { createRoute } from "../../modules/api-utils/createRoute";
|
||||||
|
import { procurement, CatalogFilterOpts } from "../../managers/procurement";
|
||||||
|
import { apiResponse } from "../../modules/api-utils/apiResponse";
|
||||||
|
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||||
|
import { authMiddleware } from "../middleware/authorization";
|
||||||
|
import { processObjectValuePerms } from "../../modules/permission-utils/processObjectPermissions";
|
||||||
|
|
||||||
|
/* /v1/procurement/items */
|
||||||
|
export default createRoute(
|
||||||
|
"get",
|
||||||
|
["/items"],
|
||||||
|
async (c) => {
|
||||||
|
const page = Number(c.req.query("page") ?? 1);
|
||||||
|
const rpp = Number(c.req.query("rpp") ?? 30);
|
||||||
|
const search = c.req.query("search") as string;
|
||||||
|
const includeInactive = c.req.query("includeInactive") === "true";
|
||||||
|
|
||||||
|
// Category / filter params
|
||||||
|
const category = c.req.query("category") as string | undefined;
|
||||||
|
const subcategory = c.req.query("subcategory") as string | undefined;
|
||||||
|
const group = c.req.query("group") as string | undefined;
|
||||||
|
const manufacturer = c.req.query("manufacturer") as string | undefined;
|
||||||
|
const ecosystem = c.req.query("ecosystem") as string | undefined;
|
||||||
|
const inStock = c.req.query("inStock") === "true" ? true : undefined;
|
||||||
|
const minPrice = c.req.query("minPrice")
|
||||||
|
? Number(c.req.query("minPrice"))
|
||||||
|
: undefined;
|
||||||
|
const maxPrice = c.req.query("maxPrice")
|
||||||
|
? Number(c.req.query("maxPrice"))
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const filterOpts: CatalogFilterOpts = {
|
||||||
|
includeInactive,
|
||||||
|
category,
|
||||||
|
subcategory,
|
||||||
|
group,
|
||||||
|
manufacturer,
|
||||||
|
ecosystem,
|
||||||
|
inStock,
|
||||||
|
minPrice,
|
||||||
|
maxPrice,
|
||||||
|
};
|
||||||
|
|
||||||
|
const data = search
|
||||||
|
? await procurement.search(search, page, rpp, filterOpts)
|
||||||
|
: await procurement.fetchPages(page, rpp, filterOpts);
|
||||||
|
|
||||||
|
const totalRecords = search
|
||||||
|
? await procurement.countSearch(search, filterOpts)
|
||||||
|
: await procurement.count(filterOpts);
|
||||||
|
|
||||||
|
const gatedData = await Promise.all(
|
||||||
|
data.map((item) =>
|
||||||
|
processObjectValuePerms(
|
||||||
|
item.toJson(),
|
||||||
|
"obj.catalogItem",
|
||||||
|
c.get("user"),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const response = apiResponse.successful(
|
||||||
|
"Catalog items fetched successfully!",
|
||||||
|
gatedData,
|
||||||
|
{
|
||||||
|
pagination: {
|
||||||
|
previousPage: page <= 1 ? null : page - 1,
|
||||||
|
currentPage: page,
|
||||||
|
nextPage: page >= totalRecords / rpp ? null : page + 1,
|
||||||
|
totalPages: Math.ceil(totalRecords / rpp),
|
||||||
|
totalRecords,
|
||||||
|
listedRecords: rpp,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return c.json(response, response.status as ContentfulStatusCode);
|
||||||
|
},
|
||||||
|
authMiddleware({ permissions: ["procurement.catalog.fetch.many"] }),
|
||||||
|
);
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import { createRoute } from "../../modules/api-utils/createRoute";
|
||||||
|
import { procurement } from "../../managers/procurement";
|
||||||
|
import { apiResponse } from "../../modules/api-utils/apiResponse";
|
||||||
|
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||||
|
import { authMiddleware } from "../middleware/authorization";
|
||||||
|
|
||||||
|
/* /v1/procurement/filters */
|
||||||
|
export default createRoute(
|
||||||
|
"get",
|
||||||
|
["/filters"],
|
||||||
|
async (c) => {
|
||||||
|
const category = c.req.query("category") as string | undefined;
|
||||||
|
const subcategory = c.req.query("subcategory") as string | undefined;
|
||||||
|
const includeInactive = c.req.query("includeInactive") === "true";
|
||||||
|
|
||||||
|
const filterOpts = { category, subcategory, includeInactive };
|
||||||
|
|
||||||
|
const [categories, subcategories, manufacturers] = await Promise.all([
|
||||||
|
procurement.fetchDistinctValues("category", filterOpts),
|
||||||
|
procurement.fetchDistinctValues("subcategory", filterOpts),
|
||||||
|
procurement.fetchDistinctValues("manufacturer", filterOpts),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const response = apiResponse.successful(
|
||||||
|
"Available filter values fetched successfully!",
|
||||||
|
{ categories, subcategories, manufacturers },
|
||||||
|
);
|
||||||
|
|
||||||
|
return c.json(response, response.status as ContentfulStatusCode);
|
||||||
|
},
|
||||||
|
authMiddleware({ permissions: ["procurement.catalog.fetch.many"] }),
|
||||||
|
);
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { default as fetchAll } from "./fetchAll";
|
||||||
|
import { default as fetch } from "./[id]/fetch";
|
||||||
|
import { default as refreshInventory } from "./[id]/refreshInventory";
|
||||||
|
import { default as link } from "./[id]/link";
|
||||||
|
import { default as unlink } from "./[id]/unlink";
|
||||||
|
import { default as fetchLinked } from "./[id]/fetchLinked";
|
||||||
|
import { default as count } from "./count";
|
||||||
|
import { default as categories } from "./categories";
|
||||||
|
import { default as filters } from "./filters";
|
||||||
|
|
||||||
|
export {
|
||||||
|
categories,
|
||||||
|
count,
|
||||||
|
fetch,
|
||||||
|
fetchAll,
|
||||||
|
fetchLinked,
|
||||||
|
filters,
|
||||||
|
link,
|
||||||
|
refreshInventory,
|
||||||
|
unlink,
|
||||||
|
};
|
||||||
@@ -4,6 +4,7 @@ import { roles } from "../../managers/roles";
|
|||||||
import { apiResponse } from "../../modules/api-utils/apiResponse";
|
import { apiResponse } from "../../modules/api-utils/apiResponse";
|
||||||
import { ContentfulStatusCode } from "hono/utils/http-status";
|
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||||
import { authMiddleware } from "../middleware/authorization";
|
import { authMiddleware } from "../middleware/authorization";
|
||||||
|
import { processObjectValuePerms } from "../../modules/permission-utils/processObjectPermissions";
|
||||||
|
|
||||||
/* GET /v1/role/:identifier */
|
/* GET /v1/role/:identifier */
|
||||||
export default createRoute(
|
export default createRoute(
|
||||||
@@ -15,9 +16,15 @@ export default createRoute(
|
|||||||
|
|
||||||
const role = await roles.fetch(identifier);
|
const role = await roles.fetch(identifier);
|
||||||
|
|
||||||
|
const gatedData = await processObjectValuePerms(
|
||||||
|
role.toJson({ viewPermissions: true }),
|
||||||
|
"obj.role",
|
||||||
|
c.get("user"),
|
||||||
|
);
|
||||||
|
|
||||||
const response = apiResponse.successful(
|
const response = apiResponse.successful(
|
||||||
"Role Fetched Successfully!",
|
"Role Fetched Successfully!",
|
||||||
role.toJson({ viewPermissions: true }),
|
gatedData,
|
||||||
);
|
);
|
||||||
return c.json(response, response.status as ContentfulStatusCode);
|
return c.json(response, response.status as ContentfulStatusCode);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { roles } from "../../managers/roles";
|
|||||||
import { apiResponse } from "../../modules/api-utils/apiResponse";
|
import { apiResponse } from "../../modules/api-utils/apiResponse";
|
||||||
import { ContentfulStatusCode } from "hono/utils/http-status";
|
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||||
import { authMiddleware } from "../middleware/authorization";
|
import { authMiddleware } from "../middleware/authorization";
|
||||||
|
import { processObjectValuePerms } from "../../modules/permission-utils/processObjectPermissions";
|
||||||
|
|
||||||
/* GET /v1/role */
|
/* GET /v1/role */
|
||||||
export default createRoute(
|
export default createRoute(
|
||||||
@@ -13,13 +14,19 @@ export default createRoute(
|
|||||||
async (c) => {
|
async (c) => {
|
||||||
const allRoles = await roles.fetchAllRoles();
|
const allRoles = await roles.fetchAllRoles();
|
||||||
|
|
||||||
const rolesArray = allRoles.map((role) =>
|
const gatedData = await Promise.all(
|
||||||
role.toJson({ viewPermissions: true }),
|
allRoles.map((role) =>
|
||||||
|
processObjectValuePerms(
|
||||||
|
role.toJson({ viewPermissions: true }),
|
||||||
|
"obj.role",
|
||||||
|
c.get("user"),
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
const response = apiResponse.successful(
|
const response = apiResponse.successful(
|
||||||
"Roles Fetched Successfully!",
|
"Roles Fetched Successfully!",
|
||||||
rolesArray,
|
gatedData,
|
||||||
);
|
);
|
||||||
return c.json(response, response.status as ContentfulStatusCode);
|
return c.json(response, response.status as ContentfulStatusCode);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { roles } from "../../managers/roles";
|
|||||||
import { apiResponse } from "../../modules/api-utils/apiResponse";
|
import { apiResponse } from "../../modules/api-utils/apiResponse";
|
||||||
import { ContentfulStatusCode } from "hono/utils/http-status";
|
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||||
import { authMiddleware } from "../middleware/authorization";
|
import { authMiddleware } from "../middleware/authorization";
|
||||||
|
import { processObjectValuePerms } from "../../modules/permission-utils/processObjectPermissions";
|
||||||
|
|
||||||
/* GET /v1/role/:identifier/users */
|
/* GET /v1/role/:identifier/users */
|
||||||
export default createRoute(
|
export default createRoute(
|
||||||
@@ -16,11 +17,15 @@ export default createRoute(
|
|||||||
const role = await roles.fetch(identifier);
|
const role = await roles.fetch(identifier);
|
||||||
const users = role.getUsers();
|
const users = role.getUsers();
|
||||||
|
|
||||||
const usersArray = users.map((user) => user.toJson());
|
const gatedData = await Promise.all(
|
||||||
|
users.map((user) =>
|
||||||
|
processObjectValuePerms(user.toJson(), "obj.user", c.get("user")),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
const response = apiResponse.successful(
|
const response = apiResponse.successful(
|
||||||
"Users Fetched Successfully!",
|
"Users Fetched Successfully!",
|
||||||
usersArray,
|
gatedData,
|
||||||
);
|
);
|
||||||
return c.json(response, response.status as ContentfulStatusCode);
|
return c.json(response, response.status as ContentfulStatusCode);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { Hono } from "hono";
|
||||||
|
import * as cwRoutes from "../cw";
|
||||||
|
|
||||||
|
const cwRouter = new Hono();
|
||||||
|
Object.values(cwRoutes).map((r) => cwRouter.route("/", r));
|
||||||
|
|
||||||
|
export default cwRouter;
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { Hono } from "hono";
|
||||||
|
import * as procurementRoutes from "../procurement";
|
||||||
|
|
||||||
|
const procurementRouter = new Hono();
|
||||||
|
Object.values(procurementRoutes).map((r) => procurementRouter.route("/", r));
|
||||||
|
|
||||||
|
export default procurementRouter;
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { Hono } from "hono";
|
||||||
|
import * as salesRoutes from "../sales";
|
||||||
|
|
||||||
|
const salesRouter = new Hono();
|
||||||
|
Object.values(salesRoutes).map((r) => salesRouter.route("/", r));
|
||||||
|
|
||||||
|
export default salesRouter;
|
||||||
@@ -0,0 +1,183 @@
|
|||||||
|
import { createRoute } from "../../../modules/api-utils/createRoute";
|
||||||
|
import { opportunities } from "../../../managers/opportunities";
|
||||||
|
import { apiResponse } from "../../../modules/api-utils/apiResponse";
|
||||||
|
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||||
|
import { authMiddleware } from "../../middleware/authorization";
|
||||||
|
import { processObjectValuePerms } from "../../../modules/permission-utils/processObjectPermissions";
|
||||||
|
import GenericError from "../../../Errors/GenericError";
|
||||||
|
import { prisma } from "../../../constants";
|
||||||
|
import { computeSubResourceCacheTTL } from "../../../modules/algorithms/computeSubResourceCacheTTL";
|
||||||
|
import { computeProductsCacheTTL } from "../../../modules/algorithms/computeProductsCacheTTL";
|
||||||
|
import {
|
||||||
|
getCachedSite,
|
||||||
|
getCachedNotes,
|
||||||
|
getCachedContacts,
|
||||||
|
getCachedProducts,
|
||||||
|
fetchAndCacheNotes,
|
||||||
|
fetchAndCacheContacts,
|
||||||
|
fetchAndCacheProducts,
|
||||||
|
fetchAndCacheSite,
|
||||||
|
} from "../../../modules/cache/opportunityCache";
|
||||||
|
import { generatedQuotes } from "../../../managers/generatedQuotes";
|
||||||
|
|
||||||
|
/* GET /v1/sales/opportunities/opportunity/:identifier?include=notes,contacts,products,quotes */
|
||||||
|
export default createRoute(
|
||||||
|
"get",
|
||||||
|
["/opportunities/opportunity/:identifier"],
|
||||||
|
async (c) => {
|
||||||
|
const identifier = c.req.param("identifier");
|
||||||
|
const includeParam = c.req.query("include") ?? "";
|
||||||
|
const includes = new Set(
|
||||||
|
includeParam
|
||||||
|
.split(",")
|
||||||
|
.map((s) => s.trim().toLowerCase())
|
||||||
|
.filter(Boolean),
|
||||||
|
);
|
||||||
|
|
||||||
|
// ── Quick DB lookup (≈3ms) to get cwOpportunityId for pre-warming ──
|
||||||
|
const isNumeric = /^\d+$/.test(identifier);
|
||||||
|
const dbRecord = await prisma.opportunity.findFirst({
|
||||||
|
where: isNumeric
|
||||||
|
? { cwOpportunityId: Number(identifier) }
|
||||||
|
: { id: identifier },
|
||||||
|
select: {
|
||||||
|
cwOpportunityId: true,
|
||||||
|
companyCwId: true,
|
||||||
|
siteCwId: true,
|
||||||
|
closedFlag: true,
|
||||||
|
closedDate: true,
|
||||||
|
expectedCloseDate: true,
|
||||||
|
cwLastUpdated: true,
|
||||||
|
statusCwId: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!dbRecord) {
|
||||||
|
throw new GenericError({
|
||||||
|
message: "Opportunity not found",
|
||||||
|
name: "OpportunityNotFound",
|
||||||
|
cause: `No opportunity exists with identifier '${identifier}'`,
|
||||||
|
status: 404,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute TTLs from DB state
|
||||||
|
const subTtl = computeSubResourceCacheTTL({
|
||||||
|
closedFlag: dbRecord.closedFlag,
|
||||||
|
closedDate: dbRecord.closedDate,
|
||||||
|
expectedCloseDate: dbRecord.expectedCloseDate,
|
||||||
|
lastUpdated: dbRecord.cwLastUpdated,
|
||||||
|
});
|
||||||
|
const prodTtl = computeProductsCacheTTL({
|
||||||
|
closedFlag: dbRecord.closedFlag,
|
||||||
|
closedDate: dbRecord.closedDate,
|
||||||
|
expectedCloseDate: dbRecord.expectedCloseDate,
|
||||||
|
lastUpdated: dbRecord.cwLastUpdated,
|
||||||
|
statusCwId: dbRecord.statusCwId,
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Pre-warm sub-resources only on cache miss ───────────────────────
|
||||||
|
// Check Redis first — if the background refresh has kept the keys warm,
|
||||||
|
// skip the CW calls entirely. Only fetch-and-cache on a miss.
|
||||||
|
const cwOppId = dbRecord.cwOpportunityId;
|
||||||
|
const _ignoreErrors = (p: Promise<any>) => p.catch(() => {});
|
||||||
|
|
||||||
|
const prewarmPromises: Promise<any>[] = [];
|
||||||
|
if (dbRecord.companyCwId && dbRecord.siteCwId) {
|
||||||
|
const compId = dbRecord.companyCwId,
|
||||||
|
siteId = dbRecord.siteCwId;
|
||||||
|
prewarmPromises.push(
|
||||||
|
_ignoreErrors(
|
||||||
|
getCachedSite(compId, siteId).then(
|
||||||
|
(c) => c ?? fetchAndCacheSite(compId, siteId),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (includes.has("notes") && subTtl)
|
||||||
|
prewarmPromises.push(
|
||||||
|
_ignoreErrors(
|
||||||
|
getCachedNotes(cwOppId).then(
|
||||||
|
(c) => c ?? fetchAndCacheNotes(cwOppId, subTtl),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (includes.has("contacts") && subTtl)
|
||||||
|
prewarmPromises.push(
|
||||||
|
_ignoreErrors(
|
||||||
|
getCachedContacts(cwOppId).then(
|
||||||
|
(c) => c ?? fetchAndCacheContacts(cwOppId, subTtl),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (includes.has("products") && prodTtl)
|
||||||
|
prewarmPromises.push(
|
||||||
|
_ignoreErrors(
|
||||||
|
getCachedProducts(cwOppId).then(
|
||||||
|
(c) => c ?? fetchAndCacheProducts(cwOppId, prodTtl),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// fetchItem runs its own CW calls (opp, activities, company) —
|
||||||
|
// these execute concurrently with the sub-resource pre-warming above.
|
||||||
|
const [item] = await Promise.all([
|
||||||
|
opportunities.fetchItem(identifier),
|
||||||
|
...prewarmPromises,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Sub-resources now hit warm Redis cache (near-instant)
|
||||||
|
const subResourcePromises: Record<string, Promise<any>> = {
|
||||||
|
_site: item.fetchSite(),
|
||||||
|
};
|
||||||
|
if (includes.has("notes")) {
|
||||||
|
subResourcePromises.notes = item.fetchNotes();
|
||||||
|
}
|
||||||
|
if (includes.has("contacts")) {
|
||||||
|
subResourcePromises.contacts = item.fetchContacts();
|
||||||
|
}
|
||||||
|
if (includes.has("products")) {
|
||||||
|
subResourcePromises.products = item
|
||||||
|
.fetchProducts()
|
||||||
|
.then((products) => products.map((p) => p.toJson()));
|
||||||
|
}
|
||||||
|
if (includes.has("quotes")) {
|
||||||
|
subResourcePromises.quotes = generatedQuotes
|
||||||
|
.fetchByOpportunity(item.id)
|
||||||
|
.then((quotes) => quotes.map((q) => q.toJson()));
|
||||||
|
}
|
||||||
|
|
||||||
|
const keys = Object.keys(subResourcePromises);
|
||||||
|
const results = await Promise.all(keys.map((k) => subResourcePromises[k]));
|
||||||
|
|
||||||
|
// Apply toJson after site is hydrated (side-effect from fetchSite)
|
||||||
|
const gatedData = await processObjectValuePerms(
|
||||||
|
item.toJson(),
|
||||||
|
"obj.opportunity",
|
||||||
|
c.get("user"),
|
||||||
|
);
|
||||||
|
|
||||||
|
const originalOpportunityNoteText = (gatedData as any).notes;
|
||||||
|
|
||||||
|
// Attach sub-resources (skip the internal _site key)
|
||||||
|
keys.forEach((k, i) => {
|
||||||
|
if (k !== "_site") {
|
||||||
|
(gatedData as any)[k] = results[i];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (includes.has("notes")) {
|
||||||
|
(gatedData as any).opportunityNoteText =
|
||||||
|
typeof originalOpportunityNoteText === "string"
|
||||||
|
? originalOpportunityNoteText
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = apiResponse.successful(
|
||||||
|
"Opportunity fetched successfully!",
|
||||||
|
gatedData,
|
||||||
|
);
|
||||||
|
return c.json(response, response.status as ContentfulStatusCode);
|
||||||
|
},
|
||||||
|
authMiddleware({ permissions: ["sales.opportunity.fetch"] }),
|
||||||
|
);
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { createRoute } from "../../modules/api-utils/createRoute";
|
||||||
|
import { apiResponse } from "../../modules/api-utils/apiResponse";
|
||||||
|
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||||
|
import { authMiddleware } from "../middleware/authorization";
|
||||||
|
import { QUOTE_STATUSES } from "../../types/QuoteStatuses";
|
||||||
|
|
||||||
|
/* GET /v1/sales/opportunity-types */
|
||||||
|
export default createRoute(
|
||||||
|
"get",
|
||||||
|
["/opportunity-types"],
|
||||||
|
|
||||||
|
async (c) => {
|
||||||
|
const response = apiResponse.successful(
|
||||||
|
"Opportunity Types Fetched Successfully!",
|
||||||
|
QUOTE_STATUSES,
|
||||||
|
);
|
||||||
|
return c.json(response, response.status as ContentfulStatusCode);
|
||||||
|
},
|
||||||
|
authMiddleware({ permissions: ["sales.opportunity.fetch.many"] }),
|
||||||
|
);
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
import { default as fetchAll } from "./opportunities/fetchAll";
|
||||||
|
import { default as metrics } from "./opportunities/metrics";
|
||||||
|
import { default as createOpportunity } from "./opportunities/create";
|
||||||
|
import { default as fetchOpportunityTypes } from "./fetchOpportunityTypes";
|
||||||
|
import { default as count } from "./opportunities/count";
|
||||||
|
import { default as fetch } from "./opportunities/[id]/fetch";
|
||||||
|
import { default as refresh } from "./opportunities/[id]/refresh";
|
||||||
|
import { default as updateOpportunity } from "./opportunities/[id]/update";
|
||||||
|
import { default as deleteOpportunity } from "./opportunities/[id]/delete";
|
||||||
|
import { default as products } from "./opportunities/[id]/products/fetchAll";
|
||||||
|
import { default as addProduct } from "./opportunities/[id]/products/add";
|
||||||
|
import { default as addSpecialOrderProduct } from "./opportunities/[id]/products/addSpecialOrder";
|
||||||
|
import { default as addLabor } from "./opportunities/[id]/products/addLabor";
|
||||||
|
import { default as laborOptions } from "./opportunities/[id]/products/laborOptions";
|
||||||
|
import { default as resequenceProducts } from "./opportunities/[id]/products/resequence";
|
||||||
|
import { default as updateProduct } from "./opportunities/[id]/products/update";
|
||||||
|
import { default as cancelProduct } from "./opportunities/[id]/products/cancel";
|
||||||
|
import { default as deleteProduct } from "./opportunities/[id]/products/delete";
|
||||||
|
import { default as notes } from "./opportunities/[id]/notes/fetchAll";
|
||||||
|
import { default as fetchNote } from "./opportunities/[id]/notes/fetch";
|
||||||
|
import { default as createNote } from "./opportunities/[id]/notes/create";
|
||||||
|
import { default as updateNote } from "./opportunities/[id]/notes/update";
|
||||||
|
import { default as deleteNote } from "./opportunities/[id]/notes/delete";
|
||||||
|
import { default as contacts } from "./opportunities/[id]/contacts";
|
||||||
|
import { default as commitQuote } from "./opportunities/[id]/quotes/commit";
|
||||||
|
import { default as fetchQuotes } from "./opportunities/[id]/quotes/fetchAll";
|
||||||
|
import { default as previewQuote } from "./opportunities/[id]/quotes/preview";
|
||||||
|
import { default as downloadQuote } from "./opportunities/[id]/quotes/download";
|
||||||
|
import { default as fetchDownloads } from "./opportunities/[id]/quotes/fetchDownloads";
|
||||||
|
import { default as fetchByUser } from "./opportunities/fetchByUser";
|
||||||
|
import { default as fetchByUserId } from "./opportunities/fetchByUserId";
|
||||||
|
import { default as workflowDispatch } from "./opportunities/[id]/workflow/dispatch";
|
||||||
|
import { default as workflowStatus } from "./opportunities/[id]/workflow/status";
|
||||||
|
import { default as workflowHistory } from "./opportunities/[id]/workflow/history";
|
||||||
|
|
||||||
|
export {
|
||||||
|
addProduct,
|
||||||
|
fetchByUser,
|
||||||
|
fetchByUserId,
|
||||||
|
addLabor,
|
||||||
|
laborOptions,
|
||||||
|
addSpecialOrderProduct,
|
||||||
|
count,
|
||||||
|
createOpportunity,
|
||||||
|
deleteOpportunity,
|
||||||
|
metrics,
|
||||||
|
fetch,
|
||||||
|
fetchAll,
|
||||||
|
fetchOpportunityTypes,
|
||||||
|
products,
|
||||||
|
resequenceProducts,
|
||||||
|
updateProduct,
|
||||||
|
cancelProduct,
|
||||||
|
deleteProduct,
|
||||||
|
notes,
|
||||||
|
fetchNote,
|
||||||
|
createNote,
|
||||||
|
updateNote,
|
||||||
|
deleteNote,
|
||||||
|
contacts,
|
||||||
|
commitQuote,
|
||||||
|
fetchQuotes,
|
||||||
|
previewQuote,
|
||||||
|
downloadQuote,
|
||||||
|
fetchDownloads,
|
||||||
|
refresh,
|
||||||
|
updateOpportunity,
|
||||||
|
workflowDispatch,
|
||||||
|
workflowStatus,
|
||||||
|
workflowHistory,
|
||||||
|
};
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { createRoute } from "../../../../modules/api-utils/createRoute";
|
||||||
|
import { opportunities } from "../../../../managers/opportunities";
|
||||||
|
import { apiResponse } from "../../../../modules/api-utils/apiResponse";
|
||||||
|
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||||
|
import { authMiddleware } from "../../../middleware/authorization";
|
||||||
|
|
||||||
|
/* GET /v1/sales/opportunities/opportunity/:identifier/contacts */
|
||||||
|
export default createRoute(
|
||||||
|
"get",
|
||||||
|
["/opportunities/opportunity/:identifier/contacts"],
|
||||||
|
async (c) => {
|
||||||
|
const identifier = c.req.param("identifier");
|
||||||
|
const item = await opportunities.fetchRecord(identifier);
|
||||||
|
|
||||||
|
const data = await item.fetchContacts();
|
||||||
|
|
||||||
|
const response = apiResponse.successful(
|
||||||
|
"Opportunity contacts fetched successfully!",
|
||||||
|
data,
|
||||||
|
);
|
||||||
|
|
||||||
|
return c.json(response, response.status as ContentfulStatusCode);
|
||||||
|
},
|
||||||
|
authMiddleware({ permissions: ["sales.opportunity.fetch"] }),
|
||||||
|
);
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import { createRoute } from "../../../../modules/api-utils/createRoute";
|
||||||
|
import { opportunities } from "../../../../managers/opportunities";
|
||||||
|
import { apiResponse } from "../../../../modules/api-utils/apiResponse";
|
||||||
|
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||||
|
import { authMiddleware } from "../../../middleware/authorization";
|
||||||
|
import GenericError from "../../../../Errors/GenericError";
|
||||||
|
|
||||||
|
/* DELETE /v1/sales/opportunities/opportunity/:identifier */
|
||||||
|
export default createRoute(
|
||||||
|
"delete",
|
||||||
|
["/opportunities/opportunity/:identifier"],
|
||||||
|
async (c) => {
|
||||||
|
const identifier = c.req.param("identifier");
|
||||||
|
|
||||||
|
try {
|
||||||
|
await opportunities.deleteItem(identifier);
|
||||||
|
|
||||||
|
const response = apiResponse.successful(
|
||||||
|
"Opportunity deleted successfully!",
|
||||||
|
);
|
||||||
|
|
||||||
|
return c.json(response, response.status as ContentfulStatusCode);
|
||||||
|
} catch (err) {
|
||||||
|
const isAxios =
|
||||||
|
err != null && typeof err === "object" && "isAxiosError" in err;
|
||||||
|
|
||||||
|
if (isAxios) {
|
||||||
|
const axiosErr = err as any;
|
||||||
|
const cwStatus: number = axiosErr.response?.status ?? 502;
|
||||||
|
const cwMessage: string =
|
||||||
|
axiosErr.response?.data?.message ??
|
||||||
|
"Failed to delete the opportunity in ConnectWise";
|
||||||
|
|
||||||
|
return c.json(
|
||||||
|
{
|
||||||
|
status: cwStatus,
|
||||||
|
message: cwMessage,
|
||||||
|
error: "ConnectWiseDeleteError",
|
||||||
|
successful: false,
|
||||||
|
meta: { timestamp: Date.now() },
|
||||||
|
},
|
||||||
|
cwStatus as ContentfulStatusCode,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
authMiddleware({ permissions: ["sales.opportunity.delete"] }),
|
||||||
|
);
|
||||||
@@ -0,0 +1,187 @@
|
|||||||
|
import { createRoute } from "../../../../modules/api-utils/createRoute";
|
||||||
|
import { opportunities } from "../../../../managers/opportunities";
|
||||||
|
import { apiResponse } from "../../../../modules/api-utils/apiResponse";
|
||||||
|
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||||
|
import { authMiddleware } from "../../../middleware/authorization";
|
||||||
|
import { processObjectValuePerms } from "../../../../modules/permission-utils/processObjectPermissions";
|
||||||
|
import GenericError from "../../../../Errors/GenericError";
|
||||||
|
import { prisma } from "../../../../constants";
|
||||||
|
import { computeSubResourceCacheTTL } from "../../../../modules/algorithms/computeSubResourceCacheTTL";
|
||||||
|
import { computeProductsCacheTTL } from "../../../../modules/algorithms/computeProductsCacheTTL";
|
||||||
|
import {
|
||||||
|
getCachedSite,
|
||||||
|
getCachedNotes,
|
||||||
|
getCachedContacts,
|
||||||
|
getCachedProducts,
|
||||||
|
fetchAndCacheNotes,
|
||||||
|
fetchAndCacheContacts,
|
||||||
|
fetchAndCacheProducts,
|
||||||
|
fetchAndCacheSite,
|
||||||
|
} from "../../../../modules/cache/opportunityCache";
|
||||||
|
import { generatedQuotes } from "../../../../managers/generatedQuotes";
|
||||||
|
|
||||||
|
/* GET /v1/sales/opportunities/opportunity/:identifier?include=notes,contacts,products,quotes */
|
||||||
|
export default createRoute(
|
||||||
|
"get",
|
||||||
|
["/opportunities/opportunity/:identifier"],
|
||||||
|
async (c) => {
|
||||||
|
const identifier = c.req.param("identifier");
|
||||||
|
const includeParam = c.req.query("include") ?? "";
|
||||||
|
const includes = new Set(
|
||||||
|
includeParam
|
||||||
|
.split(",")
|
||||||
|
.map((s) => s.trim().toLowerCase())
|
||||||
|
.filter(Boolean),
|
||||||
|
);
|
||||||
|
|
||||||
|
// ── Quick DB lookup (≈3ms) to get cwOpportunityId for pre-warming ──
|
||||||
|
const isNumeric = /^\d+$/.test(identifier);
|
||||||
|
const dbRecord = await prisma.opportunity.findFirst({
|
||||||
|
where: isNumeric
|
||||||
|
? { cwOpportunityId: Number(identifier) }
|
||||||
|
: { id: identifier },
|
||||||
|
select: {
|
||||||
|
cwOpportunityId: true,
|
||||||
|
companyCwId: true,
|
||||||
|
siteCwId: true,
|
||||||
|
closedFlag: true,
|
||||||
|
closedDate: true,
|
||||||
|
expectedCloseDate: true,
|
||||||
|
cwLastUpdated: true,
|
||||||
|
statusCwId: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!dbRecord) {
|
||||||
|
throw new GenericError({
|
||||||
|
message: "Opportunity not found",
|
||||||
|
name: "OpportunityNotFound",
|
||||||
|
cause: `No opportunity exists with identifier '${identifier}'`,
|
||||||
|
status: 404,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute TTLs from DB state
|
||||||
|
const subTtl = computeSubResourceCacheTTL({
|
||||||
|
closedFlag: dbRecord.closedFlag,
|
||||||
|
closedDate: dbRecord.closedDate,
|
||||||
|
expectedCloseDate: dbRecord.expectedCloseDate,
|
||||||
|
lastUpdated: dbRecord.cwLastUpdated,
|
||||||
|
});
|
||||||
|
const prodTtl = computeProductsCacheTTL({
|
||||||
|
closedFlag: dbRecord.closedFlag,
|
||||||
|
closedDate: dbRecord.closedDate,
|
||||||
|
expectedCloseDate: dbRecord.expectedCloseDate,
|
||||||
|
lastUpdated: dbRecord.cwLastUpdated,
|
||||||
|
statusCwId: dbRecord.statusCwId,
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Pre-warm sub-resources only on cache miss ───────────────────────
|
||||||
|
// Check Redis first — if the background refresh has kept the keys warm,
|
||||||
|
// skip the CW calls entirely. Only fetch-and-cache on a miss.
|
||||||
|
const cwOppId = dbRecord.cwOpportunityId;
|
||||||
|
const _ignoreErrors = (p: Promise<any>) => p.catch(() => {});
|
||||||
|
|
||||||
|
const prewarmPromises: Promise<any>[] = [];
|
||||||
|
if (dbRecord.companyCwId && dbRecord.siteCwId) {
|
||||||
|
const compId = dbRecord.companyCwId,
|
||||||
|
siteId = dbRecord.siteCwId;
|
||||||
|
prewarmPromises.push(
|
||||||
|
_ignoreErrors(
|
||||||
|
getCachedSite(compId, siteId).then(
|
||||||
|
(c) => c ?? fetchAndCacheSite(compId, siteId),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (includes.has("notes") && subTtl)
|
||||||
|
prewarmPromises.push(
|
||||||
|
_ignoreErrors(
|
||||||
|
getCachedNotes(cwOppId).then(
|
||||||
|
(c) => c ?? fetchAndCacheNotes(cwOppId, subTtl),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (includes.has("contacts") && subTtl)
|
||||||
|
prewarmPromises.push(
|
||||||
|
_ignoreErrors(
|
||||||
|
getCachedContacts(cwOppId).then(
|
||||||
|
(c) => c ?? fetchAndCacheContacts(cwOppId, subTtl),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (includes.has("products") && prodTtl)
|
||||||
|
prewarmPromises.push(
|
||||||
|
_ignoreErrors(
|
||||||
|
getCachedProducts(cwOppId).then(
|
||||||
|
(c) => c ?? fetchAndCacheProducts(cwOppId, prodTtl),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// fetchItem runs its own CW calls (opp, activities, company) —
|
||||||
|
// these execute concurrently with the sub-resource pre-warming above.
|
||||||
|
const [item] = await Promise.all([
|
||||||
|
opportunities.fetchItem(identifier),
|
||||||
|
...prewarmPromises,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Sub-resources now hit warm Redis cache (near-instant)
|
||||||
|
const subResourcePromises: Record<string, Promise<any>> = {
|
||||||
|
_site: item.fetchSite(),
|
||||||
|
};
|
||||||
|
if (includes.has("notes")) {
|
||||||
|
subResourcePromises.notes = item.fetchNotes();
|
||||||
|
}
|
||||||
|
if (includes.has("contacts")) {
|
||||||
|
subResourcePromises.contacts = item.fetchContacts();
|
||||||
|
}
|
||||||
|
if (includes.has("products")) {
|
||||||
|
subResourcePromises.products = item
|
||||||
|
.fetchProducts()
|
||||||
|
.then((products) => products.map((p) => p.toJson()));
|
||||||
|
}
|
||||||
|
if (includes.has("quotes")) {
|
||||||
|
const includeRegenData = c.req.query("includeRegenData") === "true";
|
||||||
|
const includeRegenParams = c.req.query("includeRegenParams") === "true";
|
||||||
|
subResourcePromises.quotes = generatedQuotes
|
||||||
|
.fetchByOpportunity(item.id)
|
||||||
|
.then((quotes) =>
|
||||||
|
quotes.map((q) => q.toJson({ includeRegenData, includeRegenParams })),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const keys = Object.keys(subResourcePromises);
|
||||||
|
const results = await Promise.all(keys.map((k) => subResourcePromises[k]));
|
||||||
|
|
||||||
|
// Apply toJson after site is hydrated (side-effect from fetchSite)
|
||||||
|
const gatedData = await processObjectValuePerms(
|
||||||
|
item.toJson(),
|
||||||
|
"obj.opportunity",
|
||||||
|
c.get("user"),
|
||||||
|
);
|
||||||
|
|
||||||
|
const originalOpportunityNoteText = (gatedData as any).notes;
|
||||||
|
|
||||||
|
// Attach sub-resources (skip the internal _site key)
|
||||||
|
keys.forEach((k, i) => {
|
||||||
|
if (k !== "_site") {
|
||||||
|
(gatedData as any)[k] = results[i];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (includes.has("notes")) {
|
||||||
|
(gatedData as any).opportunityNoteText =
|
||||||
|
typeof originalOpportunityNoteText === "string"
|
||||||
|
? originalOpportunityNoteText
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = apiResponse.successful(
|
||||||
|
"Opportunity fetched successfully!",
|
||||||
|
gatedData,
|
||||||
|
);
|
||||||
|
return c.json(response, response.status as ContentfulStatusCode);
|
||||||
|
},
|
||||||
|
authMiddleware({ permissions: ["sales.opportunity.fetch"] }),
|
||||||
|
);
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import { createRoute } from "../../../../../modules/api-utils/createRoute";
|
||||||
|
import { opportunities } from "../../../../../managers/opportunities";
|
||||||
|
import { apiResponse } from "../../../../../modules/api-utils/apiResponse";
|
||||||
|
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||||
|
import { authMiddleware } from "../../../../middleware/authorization";
|
||||||
|
import { resolveMember } from "../../../../../modules/cw-utils/members/memberCache";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
/* POST /v1/sales/opportunities/opportunity/:identifier/notes */
|
||||||
|
export default createRoute(
|
||||||
|
"post",
|
||||||
|
["/opportunities/opportunity/:identifier/notes"],
|
||||||
|
async (c) => {
|
||||||
|
const identifier = c.req.param("identifier");
|
||||||
|
const body = await c.req.json();
|
||||||
|
|
||||||
|
const schema = z.object({
|
||||||
|
text: z.string().min(1, "Note text is required"),
|
||||||
|
flagged: z.boolean().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = schema.parse(body);
|
||||||
|
|
||||||
|
const item = await opportunities.fetchRecord(identifier);
|
||||||
|
const user = c.get("user");
|
||||||
|
|
||||||
|
const created = await item.addNote(data.text, user.login, {
|
||||||
|
flagged: data.flagged,
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = apiResponse.created(
|
||||||
|
"Opportunity note created successfully!",
|
||||||
|
{
|
||||||
|
id: created.id,
|
||||||
|
text: created.text,
|
||||||
|
type: created.type
|
||||||
|
? { id: created.type.id, name: created.type.name }
|
||||||
|
: null,
|
||||||
|
flagged: created.flagged,
|
||||||
|
enteredBy: await resolveMember(created.enteredBy),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return c.json(response, response.status as ContentfulStatusCode);
|
||||||
|
},
|
||||||
|
authMiddleware({ permissions: ["sales.opportunity.note.create"] }),
|
||||||
|
);
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import { createRoute } from "../../../../../modules/api-utils/createRoute";
|
||||||
|
import { opportunities } from "../../../../../managers/opportunities";
|
||||||
|
import { apiResponse } from "../../../../../modules/api-utils/apiResponse";
|
||||||
|
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||||
|
import { authMiddleware } from "../../../../middleware/authorization";
|
||||||
|
import GenericError from "../../../../../Errors/GenericError";
|
||||||
|
|
||||||
|
/* DELETE /v1/sales/opportunities/opportunity/:identifier/notes/:noteId */
|
||||||
|
export default createRoute(
|
||||||
|
"delete",
|
||||||
|
["/opportunities/opportunity/:identifier/notes/:noteId"],
|
||||||
|
async (c) => {
|
||||||
|
const identifier = c.req.param("identifier");
|
||||||
|
const noteId = Number(c.req.param("noteId"));
|
||||||
|
|
||||||
|
if (isNaN(noteId))
|
||||||
|
throw new GenericError({
|
||||||
|
status: 400,
|
||||||
|
name: "InvalidNoteId",
|
||||||
|
message: "Note ID must be a number",
|
||||||
|
});
|
||||||
|
|
||||||
|
const item = await opportunities.fetchRecord(identifier);
|
||||||
|
await item.deleteNote(noteId);
|
||||||
|
|
||||||
|
const response = apiResponse.successful(
|
||||||
|
"Opportunity note deleted successfully!",
|
||||||
|
);
|
||||||
|
|
||||||
|
return c.json(response, response.status as ContentfulStatusCode);
|
||||||
|
},
|
||||||
|
authMiddleware({ permissions: ["sales.opportunity.note.delete"] }),
|
||||||
|
);
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { createRoute } from "../../../../../modules/api-utils/createRoute";
|
||||||
|
import { opportunities } from "../../../../../managers/opportunities";
|
||||||
|
import { apiResponse } from "../../../../../modules/api-utils/apiResponse";
|
||||||
|
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||||
|
import { authMiddleware } from "../../../../middleware/authorization";
|
||||||
|
import GenericError from "../../../../../Errors/GenericError";
|
||||||
|
|
||||||
|
/* GET /v1/sales/opportunities/opportunity/:identifier/notes/:noteId */
|
||||||
|
export default createRoute(
|
||||||
|
"get",
|
||||||
|
["/opportunities/opportunity/:identifier/notes/:noteId"],
|
||||||
|
async (c) => {
|
||||||
|
const identifier = c.req.param("identifier");
|
||||||
|
const noteId = Number(c.req.param("noteId"));
|
||||||
|
|
||||||
|
if (isNaN(noteId))
|
||||||
|
throw new GenericError({
|
||||||
|
status: 400,
|
||||||
|
name: "InvalidNoteId",
|
||||||
|
message: "Note ID must be a number",
|
||||||
|
});
|
||||||
|
|
||||||
|
const item = await opportunities.fetchRecord(identifier);
|
||||||
|
const data = await item.fetchNote(noteId);
|
||||||
|
|
||||||
|
const response = apiResponse.successful(
|
||||||
|
"Opportunity note fetched successfully!",
|
||||||
|
data,
|
||||||
|
);
|
||||||
|
|
||||||
|
return c.json(response, response.status as ContentfulStatusCode);
|
||||||
|
},
|
||||||
|
authMiddleware({ permissions: ["sales.opportunity.fetch"] }),
|
||||||
|
);
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { createRoute } from "../../../../../modules/api-utils/createRoute";
|
||||||
|
import { opportunities } from "../../../../../managers/opportunities";
|
||||||
|
import { apiResponse } from "../../../../../modules/api-utils/apiResponse";
|
||||||
|
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||||
|
import { authMiddleware } from "../../../../middleware/authorization";
|
||||||
|
|
||||||
|
/* GET /v1/sales/opportunities/opportunity/:identifier/notes */
|
||||||
|
export default createRoute(
|
||||||
|
"get",
|
||||||
|
["/opportunities/opportunity/:identifier/notes"],
|
||||||
|
async (c) => {
|
||||||
|
const identifier = c.req.param("identifier");
|
||||||
|
const item = await opportunities.fetchRecord(identifier);
|
||||||
|
|
||||||
|
const data = await item.fetchNotes();
|
||||||
|
|
||||||
|
const response = apiResponse.successful(
|
||||||
|
"Opportunity notes fetched successfully!",
|
||||||
|
data,
|
||||||
|
);
|
||||||
|
|
||||||
|
return c.json(response, response.status as ContentfulStatusCode);
|
||||||
|
},
|
||||||
|
authMiddleware({ permissions: ["sales.opportunity.fetch"] }),
|
||||||
|
);
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import { createRoute } from "../../../../../modules/api-utils/createRoute";
|
||||||
|
import { opportunities } from "../../../../../managers/opportunities";
|
||||||
|
import { apiResponse } from "../../../../../modules/api-utils/apiResponse";
|
||||||
|
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||||
|
import { authMiddleware } from "../../../../middleware/authorization";
|
||||||
|
import GenericError from "../../../../../Errors/GenericError";
|
||||||
|
import { resolveMember } from "../../../../../modules/cw-utils/members/memberCache";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
/* PATCH /v1/sales/opportunities/opportunity/:identifier/notes/:noteId */
|
||||||
|
export default createRoute(
|
||||||
|
"patch",
|
||||||
|
["/opportunities/opportunity/:identifier/notes/:noteId"],
|
||||||
|
async (c) => {
|
||||||
|
const identifier = c.req.param("identifier");
|
||||||
|
const noteId = Number(c.req.param("noteId"));
|
||||||
|
|
||||||
|
if (isNaN(noteId))
|
||||||
|
throw new GenericError({
|
||||||
|
status: 400,
|
||||||
|
name: "InvalidNoteId",
|
||||||
|
message: "Note ID must be a number",
|
||||||
|
});
|
||||||
|
|
||||||
|
const body = await c.req.json();
|
||||||
|
|
||||||
|
const schema = z
|
||||||
|
.object({
|
||||||
|
text: z.string().min(1).optional(),
|
||||||
|
flagged: z.boolean().optional(),
|
||||||
|
})
|
||||||
|
.refine((d) => d.text !== undefined || d.flagged !== undefined, {
|
||||||
|
message: "At least one of 'text' or 'flagged' must be provided",
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = schema.parse(body);
|
||||||
|
|
||||||
|
const item = await opportunities.fetchRecord(identifier);
|
||||||
|
const updated = await item.updateNote(noteId, data);
|
||||||
|
|
||||||
|
const response = apiResponse.successful(
|
||||||
|
"Opportunity note updated successfully!",
|
||||||
|
{
|
||||||
|
id: updated.id,
|
||||||
|
text: updated.text,
|
||||||
|
type: updated.type
|
||||||
|
? { id: updated.type.id, name: updated.type.name }
|
||||||
|
: null,
|
||||||
|
flagged: updated.flagged,
|
||||||
|
enteredBy: await resolveMember(updated.enteredBy),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return c.json(response, response.status as ContentfulStatusCode);
|
||||||
|
},
|
||||||
|
authMiddleware({ permissions: ["sales.opportunity.note.update"] }),
|
||||||
|
);
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
import { createRoute } from "../../../../../modules/api-utils/createRoute";
|
||||||
|
import { opportunities } from "../../../../../managers/opportunities";
|
||||||
|
import { apiResponse } from "../../../../../modules/api-utils/apiResponse";
|
||||||
|
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||||
|
import { authMiddleware } from "../../../../middleware/authorization";
|
||||||
|
import { processObjectValuePerms } from "../../../../../modules/permission-utils/processObjectPermissions";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
const productItemSchema = z
|
||||||
|
.object({
|
||||||
|
catalogItem: z.object({ id: z.number().int().positive() }).optional(),
|
||||||
|
forecastDescription: z.string().optional(),
|
||||||
|
productDescription: z.string().optional(),
|
||||||
|
customerDescription: z.string().nullable().optional(),
|
||||||
|
quantity: z.number().positive().optional(),
|
||||||
|
status: z.object({ id: z.number().int().positive() }).optional(),
|
||||||
|
productClass: z.string().optional(),
|
||||||
|
forecastType: z.string().optional(),
|
||||||
|
revenue: z.number().optional(),
|
||||||
|
cost: z.number().optional(),
|
||||||
|
includeFlag: z.boolean().optional(),
|
||||||
|
linkFlag: z.boolean().optional(),
|
||||||
|
recurringFlag: z.boolean().optional(),
|
||||||
|
taxableFlag: z.boolean().optional(),
|
||||||
|
recurringRevenue: z.number().optional(),
|
||||||
|
recurringCost: z.number().optional(),
|
||||||
|
cycles: z.number().int().min(0).optional(),
|
||||||
|
sequenceNumber: z.number().int().min(0).optional(),
|
||||||
|
})
|
||||||
|
.strict();
|
||||||
|
|
||||||
|
const addProductSchema = z.union([
|
||||||
|
productItemSchema,
|
||||||
|
z.array(productItemSchema).min(1, "At least one product is required"),
|
||||||
|
]);
|
||||||
|
|
||||||
|
/* POST /v1/sales/opportunities/opportunity/:identifier/products */
|
||||||
|
export default createRoute(
|
||||||
|
"post",
|
||||||
|
["/opportunities/opportunity/:identifier/products"],
|
||||||
|
async (c) => {
|
||||||
|
const identifier = c.req.param("identifier");
|
||||||
|
const body = await c.req.json();
|
||||||
|
|
||||||
|
const validated = addProductSchema.parse(body);
|
||||||
|
const inputItems = Array.isArray(validated) ? validated : [validated];
|
||||||
|
|
||||||
|
// Gate each submitted field against user permissions.
|
||||||
|
// Only fields the user has permission for are forwarded to ConnectWise.
|
||||||
|
const user = c.get("user");
|
||||||
|
const gatedItems = await Promise.all(
|
||||||
|
inputItems.map((item) =>
|
||||||
|
processObjectValuePerms(item, "sales.opportunity.product.field", user),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const item = await opportunities.fetchRecord(identifier);
|
||||||
|
|
||||||
|
// Strip customerDescription from forecast payloads — CW only accepts
|
||||||
|
// it on procurement products, not forecast items.
|
||||||
|
const customerDescriptions = gatedItems.map(
|
||||||
|
(g: any) => g.customerDescription,
|
||||||
|
);
|
||||||
|
const forecastPayloads = gatedItems.map(
|
||||||
|
({ customerDescription, ...rest }: any) => rest,
|
||||||
|
);
|
||||||
|
|
||||||
|
const created = await item.addProducts(forecastPayloads);
|
||||||
|
|
||||||
|
// If any items included customerDescription, patch the linked
|
||||||
|
// procurement products after creation. This is best-effort since
|
||||||
|
// newly created forecast items may not have a linked procurement
|
||||||
|
// product yet.
|
||||||
|
const procurementUpdates = created
|
||||||
|
.map((product, idx) => ({
|
||||||
|
product,
|
||||||
|
customerDescription: customerDescriptions[idx],
|
||||||
|
}))
|
||||||
|
.filter((entry) => entry.customerDescription != null);
|
||||||
|
|
||||||
|
if (procurementUpdates.length > 0) {
|
||||||
|
await Promise.all(
|
||||||
|
procurementUpdates.map(({ product, customerDescription }) =>
|
||||||
|
item
|
||||||
|
.updateProcurementProductByForecastItem(product.cwForecastId, {
|
||||||
|
customerDescription,
|
||||||
|
})
|
||||||
|
.catch(() => null),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const isBatch = Array.isArray(body);
|
||||||
|
const response = apiResponse.created(
|
||||||
|
isBatch
|
||||||
|
? `${created.length} product(s) added to opportunity successfully!`
|
||||||
|
: "Product added to opportunity successfully!",
|
||||||
|
isBatch ? created.map((p) => p.toJson()) : created[0]!.toJson(),
|
||||||
|
);
|
||||||
|
return c.json(response, response.status as ContentfulStatusCode);
|
||||||
|
},
|
||||||
|
authMiddleware({ permissions: ["sales.opportunity.product.add"] }),
|
||||||
|
);
|
||||||
@@ -0,0 +1,147 @@
|
|||||||
|
import { createRoute } from "../../../../../modules/api-utils/createRoute";
|
||||||
|
import { opportunities } from "../../../../../managers/opportunities";
|
||||||
|
import { procurement } from "../../../../../managers/procurement";
|
||||||
|
import { apiResponse } from "../../../../../modules/api-utils/apiResponse";
|
||||||
|
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||||
|
import { authMiddleware } from "../../../../middleware/authorization";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
const LABOR_DEFAULT_RATE = {
|
||||||
|
corporate: 100,
|
||||||
|
residential: 85,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const roundMoney = (value: number) => Math.round(value * 100) / 100;
|
||||||
|
|
||||||
|
const addLaborSchema = z
|
||||||
|
.object({
|
||||||
|
laborStyle: z.enum(["field", "tech"]),
|
||||||
|
customerType: z.enum(["corporate", "residential"]).optional(),
|
||||||
|
hours: z.number().positive().optional(),
|
||||||
|
rate: z.number().min(0).optional(),
|
||||||
|
ppu: z.number().min(0).optional(),
|
||||||
|
cpu: z.number().min(0).optional(),
|
||||||
|
taxable: z.boolean().optional(),
|
||||||
|
taxableFlag: z.boolean().optional(),
|
||||||
|
description: z.string().min(1).optional(),
|
||||||
|
customerDescription: z.string().min(1).optional(),
|
||||||
|
procurementNotes: z.string().optional(),
|
||||||
|
productNarrative: z.string().optional(),
|
||||||
|
})
|
||||||
|
.strict();
|
||||||
|
|
||||||
|
/* POST /v1/sales/opportunities/opportunity/:identifier/products/labor */
|
||||||
|
export default createRoute(
|
||||||
|
"post",
|
||||||
|
["/opportunities/opportunity/:identifier/products/labor"],
|
||||||
|
async (c) => {
|
||||||
|
const identifier = c.req.param("identifier");
|
||||||
|
const body = await c.req.json();
|
||||||
|
const input = addLaborSchema.parse(body);
|
||||||
|
|
||||||
|
const laborCatalog = await procurement.fetchLaborCatalogItems();
|
||||||
|
const selectedCatalog =
|
||||||
|
input.laborStyle === "tech" ? laborCatalog.tech : laborCatalog.field;
|
||||||
|
|
||||||
|
const customerType = input.customerType ?? "corporate";
|
||||||
|
const defaultRate = LABOR_DEFAULT_RATE[customerType];
|
||||||
|
const quantity = input.hours ?? 1;
|
||||||
|
const ppu = input.ppu ?? input.rate ?? defaultRate;
|
||||||
|
const cpu = input.cpu ?? roundMoney(ppu * 0.5);
|
||||||
|
const taxableFlag =
|
||||||
|
input.taxable ?? input.taxableFlag ?? selectedCatalog.salesTaxable;
|
||||||
|
|
||||||
|
const makeCustomField = (
|
||||||
|
caption: string,
|
||||||
|
value: string,
|
||||||
|
fieldId: number,
|
||||||
|
) => ({
|
||||||
|
id: fieldId,
|
||||||
|
caption,
|
||||||
|
type: "Text",
|
||||||
|
entryMethod: "EntryField",
|
||||||
|
value,
|
||||||
|
});
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
...(input.procurementNotes || input.productNarrative
|
||||||
|
? {
|
||||||
|
customFields: [
|
||||||
|
...(input.procurementNotes
|
||||||
|
? [
|
||||||
|
makeCustomField(
|
||||||
|
"Procurement Notes",
|
||||||
|
input.procurementNotes,
|
||||||
|
29,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
...(input.productNarrative
|
||||||
|
? [
|
||||||
|
makeCustomField(
|
||||||
|
"Product Narrative",
|
||||||
|
input.productNarrative,
|
||||||
|
46,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
catalogItem: { id: selectedCatalog.cwCatalogId },
|
||||||
|
description:
|
||||||
|
input.description ??
|
||||||
|
selectedCatalog.name ??
|
||||||
|
selectedCatalog.identifier ??
|
||||||
|
`${input.laborStyle.toUpperCase()} Labor`,
|
||||||
|
customerDescription: input.customerDescription,
|
||||||
|
quantity,
|
||||||
|
price: ppu,
|
||||||
|
cost: cpu,
|
||||||
|
taxableFlag,
|
||||||
|
dropshipFlag: false,
|
||||||
|
billableOption: "Billable",
|
||||||
|
};
|
||||||
|
|
||||||
|
const opportunity = await opportunities.fetchRecord(identifier);
|
||||||
|
const [created] = await opportunity.addProcurementProducts(payload);
|
||||||
|
|
||||||
|
const fields = Array.isArray(created?.customFields)
|
||||||
|
? created.customFields
|
||||||
|
: [];
|
||||||
|
const procurementNotes =
|
||||||
|
fields.find((f: any) => f?.id === 29)?.value ?? null;
|
||||||
|
const productNarrative =
|
||||||
|
fields.find((f: any) => f?.id === 46)?.value ?? null;
|
||||||
|
|
||||||
|
const response = apiResponse.created(
|
||||||
|
"Labor added to opportunity successfully!",
|
||||||
|
{
|
||||||
|
id: created?.id ?? null,
|
||||||
|
forecastDetailId: created?.forecastDetailId ?? null,
|
||||||
|
laborStyle: input.laborStyle,
|
||||||
|
customerType,
|
||||||
|
catalogItem: {
|
||||||
|
id: selectedCatalog.cwCatalogId,
|
||||||
|
identifier: selectedCatalog.identifier,
|
||||||
|
name: selectedCatalog.name,
|
||||||
|
},
|
||||||
|
description: created?.description ?? payload.description,
|
||||||
|
customerDescription:
|
||||||
|
created?.customerDescription ?? input.customerDescription ?? null,
|
||||||
|
quantity: created?.quantity ?? quantity,
|
||||||
|
rate: ppu,
|
||||||
|
ppu,
|
||||||
|
cpu,
|
||||||
|
revenue: roundMoney((created?.quantity ?? quantity) * ppu),
|
||||||
|
cost: roundMoney((created?.quantity ?? quantity) * cpu),
|
||||||
|
taxableFlag: created?.taxableFlag ?? taxableFlag,
|
||||||
|
procurementNotes,
|
||||||
|
productNarrative,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return c.json(response, response.status as ContentfulStatusCode);
|
||||||
|
},
|
||||||
|
authMiddleware({ permissions: ["sales.opportunity.product.add.labor"] }),
|
||||||
|
);
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
import { createRoute } from "../../../../../modules/api-utils/createRoute";
|
||||||
|
import { opportunities } from "../../../../../managers/opportunities";
|
||||||
|
import { procurement } from "../../../../../managers/procurement";
|
||||||
|
import { apiResponse } from "../../../../../modules/api-utils/apiResponse";
|
||||||
|
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||||
|
import { authMiddleware } from "../../../../middleware/authorization";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
const specialOrderItemSchema = z
|
||||||
|
.object({
|
||||||
|
desc: z.string().min(1),
|
||||||
|
customerDesc: z.string().min(1).optional(),
|
||||||
|
qty: z.number().positive().optional(),
|
||||||
|
price: z.number(),
|
||||||
|
cost: z.number().optional(),
|
||||||
|
taxable: z.boolean().optional(),
|
||||||
|
taxableFlag: z.boolean().optional(),
|
||||||
|
procurementNotes: z.string().optional(),
|
||||||
|
productNarrative: z.string().optional(),
|
||||||
|
})
|
||||||
|
.strict();
|
||||||
|
|
||||||
|
const addSpecialOrderSchema = z.union([
|
||||||
|
specialOrderItemSchema,
|
||||||
|
z
|
||||||
|
.array(specialOrderItemSchema)
|
||||||
|
.min(1, "At least one special-order product is required"),
|
||||||
|
]);
|
||||||
|
|
||||||
|
/* POST /v1/sales/opportunities/opportunity/:identifier/products/special-order */
|
||||||
|
export default createRoute(
|
||||||
|
"post",
|
||||||
|
["/opportunities/opportunity/:identifier/products/special-order"],
|
||||||
|
async (c) => {
|
||||||
|
const identifier = c.req.param("identifier");
|
||||||
|
const body = await c.req.json();
|
||||||
|
|
||||||
|
const validated = addSpecialOrderSchema.parse(body);
|
||||||
|
const inputItems = Array.isArray(validated) ? validated : [validated];
|
||||||
|
const specialOrderCatalogItem =
|
||||||
|
await procurement.fetchItem("SPECIAL ORDER");
|
||||||
|
|
||||||
|
const makeCustomField = (
|
||||||
|
caption: string,
|
||||||
|
value: string,
|
||||||
|
fieldId: number,
|
||||||
|
) => ({
|
||||||
|
id: fieldId,
|
||||||
|
caption,
|
||||||
|
type: "Text",
|
||||||
|
entryMethod: "EntryField",
|
||||||
|
value,
|
||||||
|
});
|
||||||
|
|
||||||
|
const normalizedItems = inputItems.map((item) => ({
|
||||||
|
...(item.procurementNotes || item.productNarrative
|
||||||
|
? {
|
||||||
|
customFields: [
|
||||||
|
...(item.procurementNotes
|
||||||
|
? [
|
||||||
|
makeCustomField(
|
||||||
|
"Procurement Notes",
|
||||||
|
item.procurementNotes,
|
||||||
|
29,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
...(item.productNarrative
|
||||||
|
? [
|
||||||
|
makeCustomField(
|
||||||
|
"Product Narrative",
|
||||||
|
item.productNarrative,
|
||||||
|
46,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
catalogItem: { id: specialOrderCatalogItem.cwCatalogId },
|
||||||
|
description: item.desc,
|
||||||
|
customerDescription: item.customerDesc,
|
||||||
|
quantity: item.qty ?? 1,
|
||||||
|
price: item.price,
|
||||||
|
cost: item.cost,
|
||||||
|
taxableFlag:
|
||||||
|
item.taxable ??
|
||||||
|
item.taxableFlag ??
|
||||||
|
specialOrderCatalogItem.salesTaxable,
|
||||||
|
dropshipFlag: false,
|
||||||
|
billableOption: "Billable",
|
||||||
|
}));
|
||||||
|
|
||||||
|
const opportunity = await opportunities.fetchRecord(identifier);
|
||||||
|
const created = await opportunity.addProcurementProducts(normalizedItems);
|
||||||
|
|
||||||
|
const serialized = created.map((item: any) => {
|
||||||
|
const fields = Array.isArray(item?.customFields) ? item.customFields : [];
|
||||||
|
const procurementNotes =
|
||||||
|
fields.find((f: any) => f?.id === 29)?.value ?? null;
|
||||||
|
const productNarrative =
|
||||||
|
fields.find((f: any) => f?.id === 46)?.value ?? null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: item?.id ?? null,
|
||||||
|
forecastDetailId: item?.forecastDetailId ?? null,
|
||||||
|
description: item?.description ?? null,
|
||||||
|
productDescription: item?.description ?? null,
|
||||||
|
customerDescription: item?.customerDescription ?? null,
|
||||||
|
quantity: item?.quantity ?? null,
|
||||||
|
price: item?.price ?? null,
|
||||||
|
revenue: item?.price ?? null,
|
||||||
|
cost: item?.cost ?? null,
|
||||||
|
taxableFlag: item?.taxableFlag ?? null,
|
||||||
|
specialOrderFlag: item?.specialOrderFlag ?? null,
|
||||||
|
procurementNotes,
|
||||||
|
productNarrative,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const isBatch = Array.isArray(body);
|
||||||
|
const response = apiResponse.created(
|
||||||
|
isBatch
|
||||||
|
? `${created.length} special-order product(s) added successfully!`
|
||||||
|
: "Special-order product added successfully!",
|
||||||
|
isBatch ? serialized : serialized[0]!,
|
||||||
|
);
|
||||||
|
|
||||||
|
return c.json(response, response.status as ContentfulStatusCode);
|
||||||
|
},
|
||||||
|
authMiddleware({
|
||||||
|
permissions: ["sales.opportunity.product.add.specialOrder"],
|
||||||
|
}),
|
||||||
|
);
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
import { createRoute } from "../../../../../modules/api-utils/createRoute";
|
||||||
|
import { opportunities } from "../../../../../managers/opportunities";
|
||||||
|
import { apiResponse } from "../../../../../modules/api-utils/apiResponse";
|
||||||
|
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||||
|
import { authMiddleware } from "../../../../middleware/authorization";
|
||||||
|
import GenericError from "../../../../../Errors/GenericError";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
const cancelProductSchema = z
|
||||||
|
.object({
|
||||||
|
quantityCancelled: z.number().int().min(0),
|
||||||
|
cancellationReason: z.string().nullable().optional(),
|
||||||
|
})
|
||||||
|
.strict();
|
||||||
|
|
||||||
|
/* PATCH /v1/sales/opportunities/opportunity/:identifier/products/:productId/cancel */
|
||||||
|
export default createRoute(
|
||||||
|
"patch",
|
||||||
|
["/opportunities/opportunity/:identifier/products/:productId/cancel"],
|
||||||
|
async (c) => {
|
||||||
|
const identifier = c.req.param("identifier");
|
||||||
|
const productId = Number(c.req.param("productId"));
|
||||||
|
const body = await c.req.json();
|
||||||
|
|
||||||
|
if (!Number.isInteger(productId) || productId <= 0) {
|
||||||
|
throw new GenericError({
|
||||||
|
status: 400,
|
||||||
|
name: "InvalidProductId",
|
||||||
|
message: "productId must be a positive integer",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const input = cancelProductSchema.parse(body);
|
||||||
|
const opportunity = await opportunities.fetchRecord(identifier);
|
||||||
|
|
||||||
|
const products = await opportunity.fetchProducts();
|
||||||
|
const product = products.find((item) => item.cwForecastId === productId);
|
||||||
|
|
||||||
|
if (!product) {
|
||||||
|
throw new GenericError({
|
||||||
|
status: 404,
|
||||||
|
name: "ForecastItemNotFound",
|
||||||
|
message: `Forecast item ${productId} not found on opportunity`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const quantity = product.quantity ?? 0;
|
||||||
|
if (input.quantityCancelled > quantity) {
|
||||||
|
throw new GenericError({
|
||||||
|
status: 400,
|
||||||
|
name: "InvalidCancelledQuantity",
|
||||||
|
message: `quantityCancelled cannot exceed product quantity (${quantity})`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await opportunity.setProductCancellation(productId, {
|
||||||
|
quantityCancelled: input.quantityCancelled,
|
||||||
|
cancellationReason: input.cancellationReason,
|
||||||
|
});
|
||||||
|
|
||||||
|
const refreshedProducts = await opportunity.fetchProducts({ fresh: true });
|
||||||
|
const updated = refreshedProducts.find(
|
||||||
|
(item) => item.cwForecastId === productId,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!updated) {
|
||||||
|
throw new GenericError({
|
||||||
|
status: 404,
|
||||||
|
name: "ForecastItemNotFound",
|
||||||
|
message: `Forecast item ${productId} not found on opportunity`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = apiResponse.successful(
|
||||||
|
input.quantityCancelled === 0
|
||||||
|
? "Product uncancelled successfully!"
|
||||||
|
: "Product cancellation updated successfully!",
|
||||||
|
updated.toJson(),
|
||||||
|
);
|
||||||
|
|
||||||
|
return c.json(response, response.status as ContentfulStatusCode);
|
||||||
|
},
|
||||||
|
authMiddleware({ permissions: ["sales.opportunity.product.update"] }),
|
||||||
|
);
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
import { createRoute } from "../../../../../modules/api-utils/createRoute";
|
||||||
|
import { opportunities } from "../../../../../managers/opportunities";
|
||||||
|
import { apiResponse } from "../../../../../modules/api-utils/apiResponse";
|
||||||
|
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||||
|
import { authMiddleware } from "../../../../middleware/authorization";
|
||||||
|
import GenericError from "../../../../../Errors/GenericError";
|
||||||
|
|
||||||
|
/* DELETE /v1/sales/opportunities/opportunity/:identifier/products/:productId */
|
||||||
|
export default createRoute(
|
||||||
|
"delete",
|
||||||
|
["/opportunities/opportunity/:identifier/products/:productId"],
|
||||||
|
async (c) => {
|
||||||
|
const identifier = c.req.param("identifier");
|
||||||
|
const productId = Number(c.req.param("productId"));
|
||||||
|
|
||||||
|
if (!Number.isInteger(productId) || productId <= 0) {
|
||||||
|
throw new GenericError({
|
||||||
|
status: 400,
|
||||||
|
name: "InvalidProductId",
|
||||||
|
message: "productId must be a positive integer",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const opportunity = await opportunities.fetchRecord(identifier);
|
||||||
|
|
||||||
|
// Verify the forecast item exists before attempting deletion
|
||||||
|
const products = await opportunity.fetchProducts();
|
||||||
|
const product = products.find((item) => item.cwForecastId === productId);
|
||||||
|
|
||||||
|
if (!product) {
|
||||||
|
throw new GenericError({
|
||||||
|
status: 404,
|
||||||
|
name: "ForecastItemNotFound",
|
||||||
|
message: `Forecast item ${productId} not found on opportunity`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await opportunity.deleteProduct(productId);
|
||||||
|
|
||||||
|
const response = apiResponse.successful(
|
||||||
|
"Product deleted from opportunity successfully!",
|
||||||
|
);
|
||||||
|
return c.json(response, response.status as ContentfulStatusCode);
|
||||||
|
} catch (err) {
|
||||||
|
const isAxios =
|
||||||
|
err != null && typeof err === "object" && "isAxiosError" in err;
|
||||||
|
|
||||||
|
if (isAxios) {
|
||||||
|
const axiosErr = err as any;
|
||||||
|
const cwStatus: number = axiosErr.response?.status ?? 502;
|
||||||
|
const cwMessage: string =
|
||||||
|
axiosErr.response?.data?.message ??
|
||||||
|
"Failed to delete the product in ConnectWise";
|
||||||
|
|
||||||
|
return c.json(
|
||||||
|
{
|
||||||
|
status: cwStatus,
|
||||||
|
message: cwMessage,
|
||||||
|
error: "ConnectWiseDeleteError",
|
||||||
|
successful: false,
|
||||||
|
meta: { timestamp: Date.now() },
|
||||||
|
},
|
||||||
|
cwStatus as ContentfulStatusCode,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
authMiddleware({ permissions: ["sales.opportunity.product.delete"] }),
|
||||||
|
);
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { createRoute } from "../../../../../modules/api-utils/createRoute";
|
||||||
|
import { opportunities } from "../../../../../managers/opportunities";
|
||||||
|
import { apiResponse } from "../../../../../modules/api-utils/apiResponse";
|
||||||
|
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||||
|
import { authMiddleware } from "../../../../middleware/authorization";
|
||||||
|
|
||||||
|
/* GET /v1/sales/opportunities/opportunity/:identifier/products */
|
||||||
|
export default createRoute(
|
||||||
|
"get",
|
||||||
|
["/opportunities/opportunity/:identifier/products"],
|
||||||
|
async (c) => {
|
||||||
|
const identifier = c.req.param("identifier");
|
||||||
|
const item = await opportunities.fetchRecord(identifier);
|
||||||
|
|
||||||
|
const data = await item.fetchProducts();
|
||||||
|
|
||||||
|
const response = apiResponse.successful(
|
||||||
|
"Opportunity products fetched successfully!",
|
||||||
|
data.map((p) => p.toJson()),
|
||||||
|
);
|
||||||
|
|
||||||
|
return c.json(response, response.status as ContentfulStatusCode);
|
||||||
|
},
|
||||||
|
authMiddleware({ permissions: ["sales.opportunity.fetch"] }),
|
||||||
|
);
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import { createRoute } from "../../../../../modules/api-utils/createRoute";
|
||||||
|
import { opportunities } from "../../../../../managers/opportunities";
|
||||||
|
import { procurement } from "../../../../../managers/procurement";
|
||||||
|
import { apiResponse } from "../../../../../modules/api-utils/apiResponse";
|
||||||
|
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||||
|
import { authMiddleware } from "../../../../middleware/authorization";
|
||||||
|
|
||||||
|
/* GET /v1/sales/opportunities/opportunity/:identifier/products/labor/options */
|
||||||
|
export default createRoute(
|
||||||
|
"get",
|
||||||
|
["/opportunities/opportunity/:identifier/products/labor/options"],
|
||||||
|
async (c) => {
|
||||||
|
const identifier = c.req.param("identifier");
|
||||||
|
|
||||||
|
await opportunities.fetchRecord(identifier);
|
||||||
|
|
||||||
|
const laborCatalog = await procurement.fetchLaborCatalogItems();
|
||||||
|
|
||||||
|
const response = apiResponse.successful(
|
||||||
|
"Labor product options fetched successfully!",
|
||||||
|
{
|
||||||
|
defaults: {
|
||||||
|
customerType: "corporate",
|
||||||
|
rates: {
|
||||||
|
corporate: 100,
|
||||||
|
residential: 85,
|
||||||
|
},
|
||||||
|
cpuMultiplier: 0.5,
|
||||||
|
quantity: 1,
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
field: {
|
||||||
|
cwCatalogId: laborCatalog.field.cwCatalogId,
|
||||||
|
identifier: laborCatalog.field.identifier,
|
||||||
|
name: laborCatalog.field.name,
|
||||||
|
taxableFlag: laborCatalog.field.salesTaxable,
|
||||||
|
},
|
||||||
|
tech: {
|
||||||
|
cwCatalogId: laborCatalog.tech.cwCatalogId,
|
||||||
|
identifier: laborCatalog.tech.identifier,
|
||||||
|
name: laborCatalog.tech.name,
|
||||||
|
taxableFlag: laborCatalog.tech.salesTaxable,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return c.json(response, response.status as ContentfulStatusCode);
|
||||||
|
},
|
||||||
|
authMiddleware({ permissions: ["sales.opportunity.product.add.labor"] }),
|
||||||
|
);
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import { createRoute } from "../../../../../modules/api-utils/createRoute";
|
||||||
|
import { opportunities } from "../../../../../managers/opportunities";
|
||||||
|
import { apiResponse } from "../../../../../modules/api-utils/apiResponse";
|
||||||
|
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||||
|
import { authMiddleware } from "../../../../middleware/authorization";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
/* PATCH /v1/sales/opportunities/opportunity/:identifier/products/sequence */
|
||||||
|
export default createRoute(
|
||||||
|
"patch",
|
||||||
|
["/opportunities/opportunity/:identifier/products/sequence"],
|
||||||
|
async (c) => {
|
||||||
|
const identifier = c.req.param("identifier");
|
||||||
|
const body = await c.req.json();
|
||||||
|
|
||||||
|
const schema = z.object({
|
||||||
|
orderedIds: z
|
||||||
|
.array(z.number().int().positive())
|
||||||
|
.min(1, "At least one forecast item ID is required"),
|
||||||
|
});
|
||||||
|
|
||||||
|
const { orderedIds } = schema.parse(body);
|
||||||
|
|
||||||
|
const item = await opportunities.fetchRecord(identifier);
|
||||||
|
const updated = await item.resequenceProducts(orderedIds);
|
||||||
|
|
||||||
|
const response = apiResponse.successful(
|
||||||
|
"Product sequence updated successfully!",
|
||||||
|
{
|
||||||
|
products: updated.map((p) => p.toJson()),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return c.json(response, response.status as ContentfulStatusCode);
|
||||||
|
},
|
||||||
|
authMiddleware({ permissions: ["sales.opportunity.product.update"] }),
|
||||||
|
);
|
||||||
@@ -0,0 +1,240 @@
|
|||||||
|
import { createRoute } from "../../../../../modules/api-utils/createRoute";
|
||||||
|
import { opportunities } from "../../../../../managers/opportunities";
|
||||||
|
import { apiResponse } from "../../../../../modules/api-utils/apiResponse";
|
||||||
|
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||||
|
import { authMiddleware } from "../../../../middleware/authorization";
|
||||||
|
import GenericError from "../../../../../Errors/GenericError";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
const PRODUCT_NARRATIVE_FIELD_ID = 46;
|
||||||
|
const PROCUREMENT_NOTES_FIELD_ID = 29;
|
||||||
|
|
||||||
|
const updateProductSchema = z
|
||||||
|
.object({
|
||||||
|
productDescription: z.string().min(1).optional(),
|
||||||
|
quantity: z.number().positive().optional(),
|
||||||
|
unitPrice: z.number().min(0).optional(),
|
||||||
|
unitCost: z.number().min(0).optional(),
|
||||||
|
customerDescription: z.string().nullable().optional(),
|
||||||
|
productNarrative: z.string().nullable().optional(),
|
||||||
|
procurementNotes: z.string().nullable().optional(),
|
||||||
|
taxableFlag: z.boolean().optional(),
|
||||||
|
})
|
||||||
|
.strict()
|
||||||
|
.refine(
|
||||||
|
(value) =>
|
||||||
|
Object.values(value).some((item) => item !== undefined && item !== null),
|
||||||
|
"At least one editable field is required",
|
||||||
|
);
|
||||||
|
|
||||||
|
const upsertCustomTextField = (
|
||||||
|
fields: Array<Record<string, unknown>>,
|
||||||
|
fieldId: number,
|
||||||
|
caption: string,
|
||||||
|
value: string,
|
||||||
|
) => {
|
||||||
|
const next = [...fields];
|
||||||
|
const idx = next.findIndex((f) => Number(f.id) === fieldId);
|
||||||
|
|
||||||
|
const field = {
|
||||||
|
id: fieldId,
|
||||||
|
caption,
|
||||||
|
type: "Text",
|
||||||
|
entryMethod: "EntryField",
|
||||||
|
value,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (idx === -1) {
|
||||||
|
next.push(field);
|
||||||
|
return next;
|
||||||
|
}
|
||||||
|
|
||||||
|
next[idx] = {
|
||||||
|
...next[idx],
|
||||||
|
...field,
|
||||||
|
};
|
||||||
|
return next;
|
||||||
|
};
|
||||||
|
|
||||||
|
/* PATCH /v1/sales/opportunities/opportunity/:identifier/products/:productId/edit */
|
||||||
|
export default createRoute(
|
||||||
|
"patch",
|
||||||
|
["/opportunities/opportunity/:identifier/products/:productId/edit"],
|
||||||
|
async (c) => {
|
||||||
|
const identifier = c.req.param("identifier");
|
||||||
|
const productId = Number(c.req.param("productId"));
|
||||||
|
const body = await c.req.json();
|
||||||
|
|
||||||
|
if (!Number.isInteger(productId) || productId <= 0) {
|
||||||
|
throw new GenericError({
|
||||||
|
status: 400,
|
||||||
|
name: "InvalidProductId",
|
||||||
|
message: "productId must be a positive integer",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const input = updateProductSchema.parse(body);
|
||||||
|
const opportunity = await opportunities.fetchRecord(identifier);
|
||||||
|
|
||||||
|
const forecastItems = await opportunity.fetchProducts();
|
||||||
|
const forecastItem = forecastItems.find(
|
||||||
|
(item) => item.cwForecastId === productId,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!forecastItem) {
|
||||||
|
throw new GenericError({
|
||||||
|
status: 404,
|
||||||
|
name: "ForecastItemNotFound",
|
||||||
|
message: `Forecast item ${productId} not found on opportunity`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const forecastJson = forecastItem.toJson();
|
||||||
|
const effectiveQuantity = input.quantity ?? forecastJson.quantity ?? 1;
|
||||||
|
|
||||||
|
const forecastPatch: Record<string, unknown> = {};
|
||||||
|
if (input.productDescription !== undefined) {
|
||||||
|
forecastPatch.productDescription = input.productDescription;
|
||||||
|
}
|
||||||
|
if (input.quantity !== undefined) {
|
||||||
|
forecastPatch.quantity = input.quantity;
|
||||||
|
}
|
||||||
|
if (input.unitPrice !== undefined) {
|
||||||
|
forecastPatch.revenue = Number(
|
||||||
|
(input.unitPrice * effectiveQuantity).toFixed(2),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (input.unitCost !== undefined) {
|
||||||
|
forecastPatch.cost = Number(
|
||||||
|
(input.unitCost * effectiveQuantity).toFixed(2),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (input.taxableFlag !== undefined) {
|
||||||
|
forecastPatch.taxableFlag = input.taxableFlag;
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingProcurement =
|
||||||
|
await opportunity.fetchProcurementProductByForecastItem(productId);
|
||||||
|
|
||||||
|
if (
|
||||||
|
(input.productNarrative !== undefined ||
|
||||||
|
input.procurementNotes !== undefined) &&
|
||||||
|
!existingProcurement
|
||||||
|
) {
|
||||||
|
throw new GenericError({
|
||||||
|
status: 400,
|
||||||
|
name: "ProcurementLinkRequired",
|
||||||
|
message:
|
||||||
|
"Product Narrative and Procurement Notes can only be updated on products linked to a procurement record",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let updatedProcurement = existingProcurement;
|
||||||
|
if (existingProcurement) {
|
||||||
|
const procurementPatch: Record<string, unknown> = {};
|
||||||
|
if (input.productDescription !== undefined) {
|
||||||
|
procurementPatch.description = input.productDescription;
|
||||||
|
}
|
||||||
|
if (input.quantity !== undefined) {
|
||||||
|
procurementPatch.quantity = input.quantity;
|
||||||
|
}
|
||||||
|
if (input.unitPrice !== undefined) {
|
||||||
|
procurementPatch.price = input.unitPrice;
|
||||||
|
}
|
||||||
|
if (input.unitCost !== undefined) {
|
||||||
|
procurementPatch.cost = input.unitCost;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
input.customerDescription !== undefined &&
|
||||||
|
input.customerDescription !== null
|
||||||
|
) {
|
||||||
|
procurementPatch.customerDescription = input.customerDescription;
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingFields = Array.isArray(existingProcurement.customFields)
|
||||||
|
? existingProcurement.customFields.map((field) => ({ ...field }))
|
||||||
|
: [];
|
||||||
|
|
||||||
|
let updatedFields = existingFields as Array<Record<string, unknown>>;
|
||||||
|
if (
|
||||||
|
input.procurementNotes !== undefined &&
|
||||||
|
input.procurementNotes !== null
|
||||||
|
) {
|
||||||
|
updatedFields = upsertCustomTextField(
|
||||||
|
updatedFields,
|
||||||
|
PROCUREMENT_NOTES_FIELD_ID,
|
||||||
|
"Procurement Notes",
|
||||||
|
input.procurementNotes,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
input.productNarrative !== undefined &&
|
||||||
|
input.productNarrative !== null
|
||||||
|
) {
|
||||||
|
updatedFields = upsertCustomTextField(
|
||||||
|
updatedFields,
|
||||||
|
PRODUCT_NARRATIVE_FIELD_ID,
|
||||||
|
"Product Narrative",
|
||||||
|
input.productNarrative,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
(input.procurementNotes !== undefined &&
|
||||||
|
input.procurementNotes !== null) ||
|
||||||
|
(input.productNarrative !== undefined &&
|
||||||
|
input.productNarrative !== null)
|
||||||
|
) {
|
||||||
|
procurementPatch.customFields = updatedFields;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.keys(procurementPatch).length > 0) {
|
||||||
|
updatedProcurement =
|
||||||
|
await opportunity.updateProcurementProductByForecastItem(
|
||||||
|
productId,
|
||||||
|
procurementPatch,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let updatedForecast = forecastJson;
|
||||||
|
if (Object.keys(forecastPatch).length > 0) {
|
||||||
|
const patched = await opportunity.updateProduct(productId, forecastPatch);
|
||||||
|
updatedForecast = patched.toJson();
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedFields = Array.isArray(updatedProcurement?.customFields)
|
||||||
|
? updatedProcurement.customFields
|
||||||
|
: [];
|
||||||
|
const procurementNotes =
|
||||||
|
updatedFields.find(
|
||||||
|
(field: any) => field?.id === PROCUREMENT_NOTES_FIELD_ID,
|
||||||
|
)?.value ?? null;
|
||||||
|
const productNarrative =
|
||||||
|
updatedFields.find(
|
||||||
|
(field: any) => field?.id === PRODUCT_NARRATIVE_FIELD_ID,
|
||||||
|
)?.value ?? null;
|
||||||
|
|
||||||
|
const quantity =
|
||||||
|
updatedProcurement?.quantity ?? updatedForecast.quantity ?? null;
|
||||||
|
const unitPrice = updatedProcurement?.price ?? null;
|
||||||
|
const unitCost = updatedProcurement?.cost ?? null;
|
||||||
|
|
||||||
|
const response = apiResponse.successful("Product updated successfully!", {
|
||||||
|
...updatedForecast,
|
||||||
|
productDescription:
|
||||||
|
updatedProcurement?.description ?? updatedForecast.productDescription,
|
||||||
|
customerDescription:
|
||||||
|
updatedProcurement?.customerDescription ??
|
||||||
|
updatedForecast.customerDescription ??
|
||||||
|
null,
|
||||||
|
quantity,
|
||||||
|
unitPrice,
|
||||||
|
unitCost,
|
||||||
|
procurementNotes,
|
||||||
|
productNarrative,
|
||||||
|
});
|
||||||
|
|
||||||
|
return c.json(response, response.status as ContentfulStatusCode);
|
||||||
|
},
|
||||||
|
authMiddleware({ permissions: ["sales.opportunity.product.update"] }),
|
||||||
|
);
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
import { createRoute } from "../../../../../modules/api-utils/createRoute";
|
||||||
|
import { opportunities } from "../../../../../managers/opportunities";
|
||||||
|
import { apiResponse } from "../../../../../modules/api-utils/apiResponse";
|
||||||
|
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||||
|
import { authMiddleware } from "../../../../middleware/authorization";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { cwMembers } from "../../../../../managers/cwMembers";
|
||||||
|
import {
|
||||||
|
createWorkflowActivity,
|
||||||
|
OptimaType,
|
||||||
|
} from "../../../../../workflows/wf.opportunity";
|
||||||
|
|
||||||
|
const commitQuoteSchema = z
|
||||||
|
.object({
|
||||||
|
lineItemPricing: z.boolean().optional(),
|
||||||
|
includeQuoteNarrative: z.boolean().optional(),
|
||||||
|
includeItemNarratives: z.boolean().optional(),
|
||||||
|
})
|
||||||
|
.strict()
|
||||||
|
.optional();
|
||||||
|
|
||||||
|
/* POST /v1/sales/opportunities/opportunity/:identifier/quote/commit */
|
||||||
|
export default createRoute(
|
||||||
|
"post",
|
||||||
|
["/opportunities/opportunity/:identifier/quote/commit"],
|
||||||
|
async (c) => {
|
||||||
|
const identifier = c.req.param("identifier");
|
||||||
|
const body = await c.req.json().catch(() => undefined);
|
||||||
|
|
||||||
|
const opts = commitQuoteSchema.parse(body);
|
||||||
|
|
||||||
|
const item = await opportunities.fetchRecord(identifier);
|
||||||
|
const user = c.get("user");
|
||||||
|
|
||||||
|
const quote = await item.commitQuote(opts ?? {}, user);
|
||||||
|
|
||||||
|
// Create a workflow activity for the generated quote
|
||||||
|
try {
|
||||||
|
let cwMemberId: number | null = null;
|
||||||
|
|
||||||
|
if (user.cwIdentifier) {
|
||||||
|
const cwMember = await cwMembers.fetch(user.cwIdentifier);
|
||||||
|
cwMemberId = cwMember.cwMemberId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cwMemberId) {
|
||||||
|
await createWorkflowActivity({
|
||||||
|
name: `[Workflow] Quote generated — ${item.name}`,
|
||||||
|
opportunityCwId: item.cwOpportunityId,
|
||||||
|
companyCwId: item.companyCwId,
|
||||||
|
assignToCwMemberId: cwMemberId,
|
||||||
|
notes: `Quote "${quote.quoteFileName}" generated.`,
|
||||||
|
optimaType: OptimaType.QuoteGenerated,
|
||||||
|
quoteId: quote.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (activityErr) {
|
||||||
|
console.error(
|
||||||
|
"[Quote Commit] Failed to create workflow activity:",
|
||||||
|
activityErr,
|
||||||
|
);
|
||||||
|
// Don't fail the quote commit if the activity fails
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = apiResponse.created(
|
||||||
|
"Quote committed successfully!",
|
||||||
|
quote.toJson({ includeRegenData: true, includeRegenParams: true }),
|
||||||
|
);
|
||||||
|
return c.json(response, response.status as ContentfulStatusCode);
|
||||||
|
},
|
||||||
|
authMiddleware({ permissions: ["sales.opportunity.quote.commit"] }),
|
||||||
|
);
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import { createRoute } from "../../../../../modules/api-utils/createRoute";
|
||||||
|
import { generatedQuotes } from "../../../../../managers/generatedQuotes";
|
||||||
|
import { apiResponse } from "../../../../../modules/api-utils/apiResponse";
|
||||||
|
import { injectPdfMetadata } from "../../../../../modules/pdf-utils";
|
||||||
|
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||||
|
import { authMiddleware } from "../../../../middleware/authorization";
|
||||||
|
import GenericError from "../../../../../Errors/GenericError";
|
||||||
|
|
||||||
|
const VALID_FETCH_ACTIONS = ["download", "print"] as const;
|
||||||
|
type FetchAction = (typeof VALID_FETCH_ACTIONS)[number];
|
||||||
|
|
||||||
|
/* GET /v1/sales/opportunities/opportunity/:identifier/quote/:quoteId/download?fetchAction=download|print */
|
||||||
|
export default createRoute(
|
||||||
|
"get",
|
||||||
|
["/opportunities/opportunity/:identifier/quote/:quoteId/download"],
|
||||||
|
async (c) => {
|
||||||
|
const quoteId = c.req.param("quoteId");
|
||||||
|
const user = c.get("user");
|
||||||
|
const fetchAction = c.req.query("fetchAction") as FetchAction | undefined;
|
||||||
|
|
||||||
|
if (!fetchAction || !VALID_FETCH_ACTIONS.includes(fetchAction)) {
|
||||||
|
throw new GenericError({
|
||||||
|
status: 400,
|
||||||
|
name: "InvalidFetchAction",
|
||||||
|
message: `Query parameter 'fetchAction' is required and must be one of: ${VALID_FETCH_ACTIONS.join(", ")}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const downloadedAt = new Date().toISOString();
|
||||||
|
|
||||||
|
const quote = await generatedQuotes.recordDownload(quoteId, {
|
||||||
|
id: user.id,
|
||||||
|
name: user.name,
|
||||||
|
email: user.email,
|
||||||
|
fetchAction,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Inject download-time metadata into the PDF's document properties
|
||||||
|
const pdfWithMetadata = await injectPdfMetadata(quote.quoteFile, {
|
||||||
|
downloadedAt,
|
||||||
|
downloadedById: user.id,
|
||||||
|
downloadedByName: user.name ?? undefined,
|
||||||
|
downloadedByEmail: user.email ?? undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = apiResponse.successful("Quote downloaded successfully!", {
|
||||||
|
id: quote.id,
|
||||||
|
quoteFileName: quote.quoteFileName,
|
||||||
|
mimeType: "application/pdf",
|
||||||
|
contentBase64: Buffer.from(pdfWithMetadata).toString("base64"),
|
||||||
|
});
|
||||||
|
return c.json(response, response.status as ContentfulStatusCode);
|
||||||
|
},
|
||||||
|
authMiddleware({ permissions: ["sales.opportunity.quote.download"] }),
|
||||||
|
);
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { createRoute } from "../../../../../modules/api-utils/createRoute";
|
||||||
|
import { opportunities } from "../../../../../managers/opportunities";
|
||||||
|
import { generatedQuotes } from "../../../../../managers/generatedQuotes";
|
||||||
|
import { apiResponse } from "../../../../../modules/api-utils/apiResponse";
|
||||||
|
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||||
|
import { authMiddleware } from "../../../../middleware/authorization";
|
||||||
|
|
||||||
|
/* GET /v1/sales/opportunities/opportunity/:identifier/quotes */
|
||||||
|
export default createRoute(
|
||||||
|
"get",
|
||||||
|
["/opportunities/opportunity/:identifier/quotes"],
|
||||||
|
async (c) => {
|
||||||
|
const identifier = c.req.param("identifier");
|
||||||
|
const includeRegenData = c.req.query("includeRegenData") === "true";
|
||||||
|
const includeRegenParams = c.req.query("includeRegenParams") === "true";
|
||||||
|
|
||||||
|
const item = await opportunities.fetchRecord(identifier);
|
||||||
|
const quotes = await generatedQuotes.fetchByOpportunity(item.id);
|
||||||
|
|
||||||
|
const response = apiResponse.successful(
|
||||||
|
"Committed quotes fetched successfully!",
|
||||||
|
quotes.map((q) => q.toJson({ includeRegenData, includeRegenParams })),
|
||||||
|
);
|
||||||
|
return c.json(response, response.status as ContentfulStatusCode);
|
||||||
|
},
|
||||||
|
authMiddleware({ permissions: ["sales.opportunity.quote.fetch"] }),
|
||||||
|
);
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import { createRoute } from "../../../../../modules/api-utils/createRoute";
|
||||||
|
import { generatedQuotes } from "../../../../../managers/generatedQuotes";
|
||||||
|
import { opportunities } from "../../../../../managers/opportunities";
|
||||||
|
import { apiResponse } from "../../../../../modules/api-utils/apiResponse";
|
||||||
|
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||||
|
import { authMiddleware } from "../../../../middleware/authorization";
|
||||||
|
|
||||||
|
/* GET /v1/sales/opportunities/opportunity/:identifier/quotes/downloads */
|
||||||
|
export default createRoute(
|
||||||
|
"get",
|
||||||
|
["/opportunities/opportunity/:identifier/quotes/downloads"],
|
||||||
|
async (c) => {
|
||||||
|
const identifier = c.req.param("identifier");
|
||||||
|
|
||||||
|
const opportunity = await opportunities.fetchRecord(identifier);
|
||||||
|
const quotes = await generatedQuotes.fetchByOpportunity(opportunity.id);
|
||||||
|
|
||||||
|
const data = quotes.map((quote) => ({
|
||||||
|
quoteId: quote.id,
|
||||||
|
quoteFileName: quote.quoteFileName,
|
||||||
|
createdById: quote.createdById,
|
||||||
|
createdAt: quote.createdAt,
|
||||||
|
downloads: quote.downloads,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const response = apiResponse.successful(
|
||||||
|
"Quote download history fetched successfully!",
|
||||||
|
data,
|
||||||
|
);
|
||||||
|
return c.json(response, response.status as ContentfulStatusCode);
|
||||||
|
},
|
||||||
|
authMiddleware({ permissions: ["sales.opportunity.quote.fetch_downloads"] }),
|
||||||
|
);
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import { createRoute } from "../../../../../modules/api-utils/createRoute";
|
||||||
|
import { opportunities } from "../../../../../managers/opportunities";
|
||||||
|
import { generatedQuotes } from "../../../../../managers/generatedQuotes";
|
||||||
|
import { apiResponse } from "../../../../../modules/api-utils/apiResponse";
|
||||||
|
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||||
|
import { authMiddleware } from "../../../../middleware/authorization";
|
||||||
|
|
||||||
|
/* GET /v1/sales/opportunities/opportunity/:identifier/quote/:quoteId/preview */
|
||||||
|
export default createRoute(
|
||||||
|
"get",
|
||||||
|
["/opportunities/opportunity/:identifier/quote/:quoteId/preview"],
|
||||||
|
async (c) => {
|
||||||
|
const identifier = c.req.param("identifier");
|
||||||
|
const quoteId = c.req.param("quoteId");
|
||||||
|
|
||||||
|
const item = await opportunities.fetchRecord(identifier);
|
||||||
|
const quote = await generatedQuotes.fetch(quoteId);
|
||||||
|
|
||||||
|
const regenData =
|
||||||
|
quote.quoteRegenData && typeof quote.quoteRegenData === "object"
|
||||||
|
? (quote.quoteRegenData as Record<string, unknown>)
|
||||||
|
: {};
|
||||||
|
|
||||||
|
const options =
|
||||||
|
regenData.options && typeof regenData.options === "object"
|
||||||
|
? (regenData.options as Record<string, unknown>)
|
||||||
|
: {};
|
||||||
|
|
||||||
|
const creator = await quote.fetchCreatedBy();
|
||||||
|
|
||||||
|
const previewBuffer = await item.generateQuote({
|
||||||
|
lineItemPricing: options.lineItemPricing as boolean | undefined,
|
||||||
|
includeQuoteNarrative: options.includeQuoteNarrative as
|
||||||
|
| boolean
|
||||||
|
| undefined,
|
||||||
|
includeItemNarratives: options.includeItemNarratives as
|
||||||
|
| boolean
|
||||||
|
| undefined,
|
||||||
|
showPreview: true,
|
||||||
|
metadata: {
|
||||||
|
quoteId: quote.id,
|
||||||
|
createdById: quote.createdById ?? undefined,
|
||||||
|
createdByName: creator?.name ?? undefined,
|
||||||
|
createdByEmail: creator?.email ?? undefined,
|
||||||
|
createdAt: quote.createdAt?.toISOString(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const previewBase64 = Buffer.from(previewBuffer).toString("base64");
|
||||||
|
|
||||||
|
const response = apiResponse.successful(
|
||||||
|
"Quote preview generated successfully!",
|
||||||
|
{
|
||||||
|
mimeType: "application/pdf",
|
||||||
|
contentBase64: previewBase64,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return c.json(response, response.status as ContentfulStatusCode);
|
||||||
|
},
|
||||||
|
authMiddleware({ permissions: ["sales.opportunity.quote.preview"] }),
|
||||||
|
);
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { createRoute } from "../../../../modules/api-utils/createRoute";
|
||||||
|
import { opportunities } from "../../../../managers/opportunities";
|
||||||
|
import { apiResponse } from "../../../../modules/api-utils/apiResponse";
|
||||||
|
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||||
|
import { authMiddleware } from "../../../middleware/authorization";
|
||||||
|
|
||||||
|
/* POST /v1/sales/opportunities/opportunity/:identifier/refresh */
|
||||||
|
export default createRoute(
|
||||||
|
"post",
|
||||||
|
["/opportunities/opportunity/:identifier/refresh"],
|
||||||
|
async (c) => {
|
||||||
|
const identifier = c.req.param("identifier");
|
||||||
|
const item = await opportunities.fetchItem(identifier);
|
||||||
|
|
||||||
|
const refreshed = await item.refreshFromCW();
|
||||||
|
|
||||||
|
const response = apiResponse.successful(
|
||||||
|
"Opportunity refreshed from ConnectWise successfully!",
|
||||||
|
refreshed.toJson(),
|
||||||
|
);
|
||||||
|
|
||||||
|
return c.json(response, response.status as ContentfulStatusCode);
|
||||||
|
},
|
||||||
|
authMiddleware({ permissions: ["sales.opportunity.refresh"] }),
|
||||||
|
);
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user