@cxtms/cx-schema 1.9.67 → 1.9.241

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/dist/cli.js +43 -9
  2. package/dist/cli.js.map +1 -1
  3. package/package.json +1 -1
  4. package/schemas/actions/all.json +5 -1
  5. package/schemas/actions/reload.json +17 -0
  6. package/schemas/actions/setSessionStorage.json +38 -0
  7. package/schemas/actions/sound.json +39 -0
  8. package/schemas/actions/vibrate.json +48 -0
  9. package/schemas/components/avatar.json +1 -0
  10. package/schemas/components/badge.json +79 -0
  11. package/schemas/components/barcodeScanner.json +28 -0
  12. package/schemas/components/camera.json +177 -0
  13. package/schemas/components/card.json +78 -0
  14. package/schemas/components/dashboard-widget.json +39 -0
  15. package/schemas/components/dashboard.json +46 -0
  16. package/schemas/components/dataGrid.json +15 -0
  17. package/schemas/components/field-collection.json +86 -2
  18. package/schemas/components/field.json +39 -4
  19. package/schemas/components/form.json +4 -0
  20. package/schemas/components/index.json +17 -1
  21. package/schemas/components/infoLine.json +1 -0
  22. package/schemas/components/map.json +167 -0
  23. package/schemas/components/module.json +32 -0
  24. package/schemas/components/planner.json +1 -0
  25. package/schemas/components/progressBar.json +1 -0
  26. package/schemas/components/timeline.json +216 -74
  27. package/schemas/components/timelineGrid.json +5 -1
  28. package/schemas/fields/autocomplete-googleplaces.json +2 -2
  29. package/schemas/fields/datetime.json +2 -2
  30. package/schemas/fields/index.json +4 -0
  31. package/schemas/fields/number-select.json +88 -0
  32. package/schemas/fields/rangedatetime.json +10 -2
  33. package/schemas/fields/select-async.json +2 -2
  34. package/schemas/fields/toggle.json +91 -0
  35. package/schemas/schema.graphql +11016 -12171
  36. package/schemas/schemas.json +131 -4
  37. package/schemas/workflows/flow/entity.json +3 -1
  38. package/schemas/workflows/input.json +1 -1
  39. package/schemas/workflows/output.json +1 -1
  40. package/schemas/workflows/tasks/all.json +9 -0
  41. package/schemas/workflows/tasks/commodity.json +8 -0
  42. package/schemas/workflows/tasks/contact-address.json +25 -0
  43. package/schemas/workflows/tasks/dispatch-routes.json +64 -0
  44. package/schemas/workflows/tasks/edi.json +15 -0
  45. package/schemas/workflows/tasks/export.json +21 -0
  46. package/schemas/workflows/tasks/httpRequest.json +4 -0
  47. package/schemas/workflows/tasks/import.json +5 -0
  48. package/schemas/workflows/tasks/order-move.json +38 -0
  49. package/schemas/workflows/tasks/order.json +6 -0
  50. package/schemas/workflows/tasks/organization-config.json +101 -0
  51. package/schemas/workflows/tasks/tracking-event.json +11 -3
  52. package/schemas/workflows/tasks/workflow-execute.json +4 -0
  53. package/schemas/workflows/variable.json +2 -2
  54. package/schemas/workflows/workflow.json +39 -2
  55. package/skills/cxtms-developer/SKILL.md +5 -2
  56. package/skills/cxtms-developer/ref-cli-auth.md +2 -0
  57. package/skills/cxtms-developer/ref-entity-commodity.md +41 -5
  58. package/skills/cxtms-developer/ref-entity-contact.md +46 -0
  59. package/skills/cxtms-developer/ref-entity-dispatch-routing.md +195 -0
  60. package/skills/cxtms-developer/ref-entity-geography.md +26 -1
  61. package/skills/cxtms-developer/ref-entity-job.md +6 -2
  62. package/skills/cxtms-developer/ref-entity-order-sub.md +1 -1
  63. package/skills/cxtms-developer/ref-entity-order.md +36 -3
  64. package/skills/cxtms-developer/ref-entity-organization.md +4 -2
  65. package/skills/cxtms-developer/ref-entity-shared.md +79 -3
  66. package/skills/cxtms-developer/ref-graphql-query.md +246 -4
  67. package/skills/cxtms-module-builder/SKILL.md +33 -6
  68. package/skills/cxtms-module-builder/ref-components-data.md +85 -0
  69. package/skills/cxtms-module-builder/ref-components-display.md +282 -8
  70. package/skills/cxtms-module-builder/ref-components-forms.md +386 -21
  71. package/skills/cxtms-module-builder/ref-components-interactive.md +2 -0
  72. package/skills/cxtms-module-builder/ref-components-layout.md +288 -29
  73. package/skills/cxtms-module-builder/ref-components-specialized.md +168 -20
  74. package/skills/cxtms-workflow-builder/SKILL.md +45 -6
  75. package/skills/cxtms-workflow-builder/ref-entity.md +100 -4
  76. package/skills/cxtms-workflow-builder/ref-expressions-ncalc.md +35 -2
  77. package/skills/cxtms-workflow-builder/ref-expressions-template.md +15 -1
  78. package/skills/cxtms-workflow-builder/ref-flow.md +6 -1
  79. package/skills/cxtms-workflow-builder/ref-query.md +16 -0
  80. package/skills/cxtms-workflow-builder/ref-utilities.md +32 -2
  81. package/templates/workflow-public-api.yaml +1 -0
@@ -50,6 +50,10 @@
50
50
  "description": "Inputs to pass to the child workflow",
51
51
  "additionalProperties": true
52
52
  },
53
+ "executionId": {
54
+ "type": "string",
55
+ "description": "Optional caller-provided GUID idempotency key for retry-safe workflow execution"
56
+ },
53
57
  "async": {
54
58
  "type": "boolean",
55
59
  "description": "Execute asynchronously"
@@ -31,14 +31,14 @@
31
31
  },
32
32
  "key": {
33
33
  "type": "string",
34
- "description": "Optional key within configuration"
34
+ "description": "Optional key within configuration. If omitted, the variable receives the whole config object with organizationConfigId injected alongside custom values."
35
35
  }
36
36
  },
37
37
  "required": ["configName"],
38
38
  "additionalProperties": true
39
39
  }
40
40
  ],
41
- "description": "Load value from configuration"
41
+ "description": "Load value from organization configuration. When a full config is loaded without a key, the runtime injects organizationConfigId into the returned object; keyed lookups return only the keyed value."
42
42
  }
43
43
  },
44
44
  "required": ["name"],
@@ -67,8 +67,8 @@
67
67
  },
68
68
  "workflowType": {
69
69
  "type": "string",
70
- "enum": ["Document", "Quote", "Flow", "Webhook", "PublicApi"],
71
- "description": "Workflow type: Document (PDF/Excel generation), Quote, Flow (declarative state machine), Webhook (HTTP endpoint), or PublicApi (REST API endpoint). Omit for standard process workflows."
70
+ "enum": ["Document", "Quote", "Flow", "Webhook", "PublicApi", "McpTool", "McpResource", "McpPrompt"],
71
+ "description": "Workflow type, including organization-scoped MCP tools, resources, and prompts. Omit for standard process workflows."
72
72
  },
73
73
  "runAs": {
74
74
  "type": "string",
@@ -266,6 +266,19 @@
266
266
  "required": ["path", "method"],
267
267
  "additionalProperties": true
268
268
  },
269
+ "mcp": {
270
+ "type": "object",
271
+ "description": "MCP metadata for McpTool, McpResource, and McpPrompt workflows",
272
+ "properties": {
273
+ "name": { "type": "string", "pattern": "^[a-zA-Z0-9_-]{1,64}$" },
274
+ "title": { "type": "string" },
275
+ "description": { "type": "string" },
276
+ "uri": { "type": "string", "format": "uri" },
277
+ "mimeType": { "type": "string" },
278
+ "timeout": { "type": "integer", "minimum": 0, "default": 60 }
279
+ },
280
+ "additionalProperties": false
281
+ },
269
282
  "entity": {
270
283
  "$ref": "flow/entity.json",
271
284
  "description": "Entity configuration for Flow workflows"
@@ -329,6 +342,30 @@
329
342
  "then": {
330
343
  "required": ["workflow", "activities", "api"]
331
344
  }
345
+ },
346
+ {
347
+ "if": {
348
+ "properties": { "workflow": { "properties": { "workflowType": { "enum": ["McpTool", "McpPrompt"] } }, "required": ["workflowType"] } }
349
+ },
350
+ "then": {
351
+ "required": ["workflow", "activities", "mcp"],
352
+ "properties": {
353
+ "mcp": { "required": ["name"] },
354
+ "workflow": { "properties": { "executionMode": { "const": "Sync" } } }
355
+ }
356
+ }
357
+ },
358
+ {
359
+ "if": {
360
+ "properties": { "workflow": { "properties": { "workflowType": { "const": "McpResource" } }, "required": ["workflowType"] } }
361
+ },
362
+ "then": {
363
+ "required": ["workflow", "activities", "mcp"],
364
+ "properties": {
365
+ "mcp": { "required": ["uri"] },
366
+ "workflow": { "properties": { "executionMode": { "const": "Sync" } } }
367
+ }
368
+ }
332
369
  }
333
370
  ],
334
371
  "additionalProperties": true
@@ -39,6 +39,7 @@ Use `--feature <feature_name>` with `cx-cli create` to automatically place files
39
39
 
40
40
  !cat skills/cxtms-developer/ref-entity-order-sub.md
41
41
  !cat skills/cxtms-developer/ref-entity-job.md
42
+ !cat skills/cxtms-developer/ref-entity-dispatch-routing.md
42
43
 
43
44
  ### Pricing & Accounting Lookups
44
45
 
@@ -66,8 +67,10 @@ Use `--feature <feature_name>` with `cx-cli create` to automatically place files
66
67
  | **Primary** | Order, Contact, Commodity, AccountingTransaction | ref-entity-order/contact/commodity/accounting.md |
67
68
  | **Order sub** | OrderEntity, TrackingEvent, EventDefinition, LinkedOrder, OrderDocument | ref-entity-order-sub.md |
68
69
  | **Job** | Job, JobOrder, JobStatus | ref-entity-job.md |
70
+ | **Dispatch routing** | DispatchRouteStatus, DispatchRouteTemplate, DispatchRoute, stops | ref-entity-dispatch-routing.md |
71
+ | **Order moves** | OrderMove, OrderMoveLeg, move and leg statuses | ref-entity-dispatch-routing.md |
69
72
  | **Pricing** | Rate, Lane, Discount, AccountingItem, AccountingAccount, PaymentTerm | ref-entity-rate.md |
70
- | **Shared** | Tag, Attachment, Division, EquipmentType, PackageType, Note/NoteThread | ref-entity-shared.md |
73
+ | **Shared** | Tag, Attachment, Division, EquipmentType, Equipment, EquipmentStatus, PackageType, Note/NoteThread | ref-entity-shared.md |
71
74
  | **Geography** | Country, State, City, Port, Vessel, CustomCode, ModeOfTransportation | ref-entity-geography.md |
72
75
  | **Equipment** | Equipment, EquipmentStatus | ref-entity-equipment.md |
73
76
  | **Warehouse** | InventoryItem, WarehouseLocation, CargoMovement (Order variant) | ref-entity-warehouse.md |
@@ -90,7 +93,7 @@ Most entities have `customValues` — a `Dictionary<string, object?>` stored as
90
93
 
91
94
  **Sub-entities**: OrderEntity, OrderCommodity, ContactAddress, ContactPaymentMethod, CommodityType, CommodityTag, OrderTag, LinkedOrder, InventoryItemTag
92
95
 
93
- **Lookup entities**: Job, JobStatus, Tag, Attachment, EventDefinition, Rate, Lane, Discount, AccountingItem, Port, Country, State, City, ModeOfTransportation
96
+ **Lookup entities**: Job, JobStatus, DispatchRouteStatus, DispatchRouteTemplate, DispatchRoute, DispatchRouteStop, DispatchRouteTemplateStop, Tag, Attachment, EventDefinition, Rate, Lane, Discount, AccountingItem, Port, Country, State, City, ModeOfTransportation
94
97
 
95
98
  **Fleet**: Equipment (replace semantics — null clears, non-null full-replaces)
96
99
 
@@ -86,6 +86,8 @@ npx cxtms deploy-all --org 42
86
86
 
87
87
  Validates all YAML files first, then pushes modules and workflows to the server. Skips files with validation errors and reports results.
88
88
 
89
+ Deploying a module whose matching server record was soft-deleted restores and updates that record. The server resolves deleted modules by module and organization ID, avoiding a duplicate-key insert during reinstall.
90
+
89
91
  > Note: `deploy-all` pushes local YAML files to the CX server (per-file `updateWorkflow` / `updateAppModule` mutations). It does **not** touch git or create a PR. Use `app release` to open a git PR from the server's pending changes.
90
92
 
91
93
  ## App Manifest Management
@@ -116,11 +116,14 @@ Field names as used in workflow expressions: `{{ entity.description }}`, `{{ ent
116
116
  | `getChildCommodities(filter)` | `[Commodity]` | Child commodities |
117
117
  | `getParentCommodity` | `Commodity` | Parent commodity |
118
118
  | `getRelatedOrders(filter)` | `[Order]` | Related orders |
119
- | `getRelatedOrder(filter)` | `Order` | First related order |
119
+ | `getRelatedOrder(filter)` | `Order` | First non-draft related order, batched across commodity rows; optional order filter; lowest `orderId` wins when multiple orders match |
120
+ | `getRelatedDispatchRoutes(filter?, orderBy?)` | `[DispatchRoute]` | Dispatch routes linked through non-draft orders attached to this commodity or any descendant commodity |
120
121
  | `getCommodityTrackingNumber(idPropertyName)` | `TrackingNumber` | Lookup |
121
122
  | `getCommodityAttachments(filter)` | `[Attachment]` | |
122
- | `lastTrackingEvent(eventDefinitionName, orderBy?)` | `TrackingEvent` | Most recent (or earliest) tracking event, resolved via batched DataLoader. `orderBy` is **honoured**: omit or prefix with `-` for DESC (latest event: `COALESCE(EventDate, Created) DESC, TrackingEventId DESC`); no prefix for ASC (earliest event: same columns ASC). Default is DESC. |
123
+ | `getWeightTotal(weightUnit)` | `decimal?` | Total weight converted to requested unit; defaults to commodity `weightUnit` when omitted |
124
+ | `lastTrackingEvent(eventDefinitionName, orderBy?)` | `TrackingEvent` | Most recent (or earliest) tracking event, resolved via batched DataLoader. `eventDefinitionName` may contain pipe-separated names such as `Departed|Delivered`; the resolver selects the winner across all matching definitions. `orderBy` is **honoured**: omit or prefix with `-` for DESC (latest event: `COALESCE(EventDate, Created) DESC, TrackingEventId DESC`); no prefix for ASC (earliest event: same columns ASC). Default is DESC. |
123
125
  | `changeHistory(startDate, endDate, maxResults)` | `[ChangeHistory]` | Audit trail |
126
+ | `getContact(idPropertyName)` | `Contact` | Resolves a contact ID stored in `customValues[idPropertyName]` within the same organization; returns null when no valid ID exists |
124
127
 
125
128
  ## Container/Child Pattern (Self-Referencing)
126
129
 
@@ -139,6 +142,7 @@ Container Commodity (parent)
139
142
  - Changing warehouse location on parent cascades to children
140
143
  - Changing commodity status on parent cascades to children
141
144
  - Deleting parent cascades to children
145
+ - `updateCommodity` can optionally cascade `customValues` to direct children (`cascadeToChildren`) or promote markers to the parent once all direct siblings have those keys (`promoteParentWhenAllChildren`)
142
146
 
143
147
  ## CommodityTrackingNumber Sub-Entity
144
148
 
@@ -206,9 +210,37 @@ conditions:
206
210
  # Update via Commodity/Update task
207
211
  inputs:
208
212
  commodityId: "{{ entity.commodityId }}"
209
- commodity:
210
- CustomValues.lotNumber: "LOT-001"
211
- CustomValues.hazmat: true
213
+ entity:
214
+ customValues:
215
+ lotNumber: "LOT-001"
216
+ hazmat: true
217
+
218
+ # Pallet/carton scan cascade: parent scan marks direct children
219
+ inputs:
220
+ commodityId: "{{ pallet.commodityId }}"
221
+ cascadeToChildren: true
222
+ entity:
223
+ customValues:
224
+ scanned: true
225
+
226
+ # Carton scan promotion: final marked sibling marks the parent
227
+ inputs:
228
+ commodityId: "{{ carton.commodityId }}"
229
+ promoteParentWhenAllChildren: true
230
+ entity:
231
+ customValues:
232
+ scanned: true
233
+ ```
234
+
235
+ Resolve custom-value contact references:
236
+
237
+ ```graphql
238
+ getCommodities(organizationId: 1, take: 1) {
239
+ items {
240
+ commodityId
241
+ getContact(idPropertyName: "shipperContactId") { contactId name }
242
+ }
243
+ }
212
244
  ```
213
245
 
214
246
  **Entities with own customValues:**
@@ -238,3 +270,7 @@ aggregations:
238
270
  parameter: "eventCode"
239
271
  expression: "any([Commodity.CommodityEvents], [each.TrackingEvent.EventDefinition.EventCode] = [eventCode])"
240
272
  ```
273
+
274
+ ### Last tracking event business days
275
+
276
+ `lastTrackingEventBusinessDays(path, contactId?, contactIdPropertyName?, eventDefinitionName?, orderBy?)` returns an `int?` business-day count for the commodity's last tracking event. Use `contactIdPropertyName: "carrierId"` when each grid row stores its carrier in `Commodity.CustomValues.carrierId`; the resolver reads the row value server-side and filters business calendar availability blocks to org-wide plus that contact.
@@ -45,6 +45,18 @@ Field names as used in workflow expressions: `{{ entity.name }}`, `{{ entity.cus
45
45
  | `lastModified` | `DateTime` | |
46
46
  | `lastModifiedBy` | `string` | User ID |
47
47
 
48
+ When an organization user's profile first/last name changes, linked contact profile names can be regenerated from trimmed `contactFirstName` + `contactLastName` so `name` remains in sync with the person display name.
49
+
50
+ ### Import behavior
51
+
52
+ Contact CSV/import handling protects division scoping:
53
+
54
+ - `DivisionId` values are accepted only when they belong to the target organization. Foreign, stale, or unparsable division IDs are removed from the import row.
55
+ - For existing contacts, an invalid `DivisionId` is ignored so the current division is not overwritten.
56
+ - For new contacts, if no valid division remains, the contact is created in the importing user's division when available.
57
+ - Nested `division.divisionName` input is resolved to an organization division and converted to `DivisionId`; the nested `division` object is not persisted.
58
+ - Empty strings are stripped from add/update fields so blank CSV cells do not clear existing values or break enum/numeric conversions.
59
+
48
60
  ## Navigation Properties
49
61
 
50
62
  | Field | Type | Notes |
@@ -78,6 +90,7 @@ Field names as used in workflow expressions: `{{ entity.name }}`, `{{ entity.cus
78
90
  | `getContactAddressByType(addressType)` | `[ContactAddress]` | Filter by "Billing", "Shipping", "Other" |
79
91
  | `getFirstContactAddressByType(addressType)` | `ContactAddress` | First match |
80
92
  | `getContactAttachments(filter, orderBy)` | `[Attachment]` | |
93
+ | `getContactAddresses(filter, orderBy)` | `[ContactAddress]` | Contact-scoped address resolver with server-side Lucene filtering and sorting |
81
94
  | `getCustomValuesAttachment(filter)` | `Attachment` | From customValues `attachmentId` key |
82
95
  | `availableCredit` | `[AvailableCreditByCurrency]` | Carrier/Customer only |
83
96
  | `changeHistory(startDate, endDate, maxResults)` | `[ChangeHistory]` | Audit trail |
@@ -101,6 +114,7 @@ Field names as used in workflow expressions: `{{ entity.name }}`, `{{ entity.cus
101
114
  | `Store` | 13 | |
102
115
  | `ContactUser` | 14 | User associated with contact |
103
116
  | `USPPI` | 15 | US Principal Party in Interest |
117
+ | `Port` | 16 | Port contact/location reference |
104
118
 
105
119
  ## ContactAddress Sub-Entity
106
120
 
@@ -115,6 +129,7 @@ Field names as used in workflow expressions: `{{ entity.name }}`, `{{ entity.cus
115
129
  | `stateCode` | `string?` | FK to State |
116
130
  | `postalCode` | `string?` | |
117
131
  | `isInactive` | `bool?` | |
132
+ | `name` | `string?` | Optional label for the address/location (max 200 chars) |
118
133
  | `latitude` | `double?` | From Location.Y (GraphQL resolved) |
119
134
  | `longitude` | `double?` | From Location.X (GraphQL resolved) |
120
135
  | `customValues` | `Dictionary` | Own customValues (separate from Contact) |
@@ -123,6 +138,10 @@ Field names as used in workflow expressions: `{{ entity.name }}`, `{{ entity.cus
123
138
 
124
139
  GraphQL resolver: `formattedAddress(outputFormat, addressFormat, lang, multiline)` — formatted string.
125
140
 
141
+ **ContactAddress import notes:** `ContactAddress/Import@1` can update by `ContactAddressId` or `contactAddressId` first, then falls back to business-key matching. Rows are bound as dictionaries, so exported primary-key columns survive re-import even when the casing is camelCase. Export grids should include `contactAddressId`/entity keys when data is intended for re-import. Import aliases `City` to `cityName`, resolves `StateName` to `stateCode`, and accepts `Longitude`/`Latitude` for `location`.
142
+
143
+ Module default-data imports support `entity: "contact"` with `keys` for natural-key matching and `overwrite` for update-vs-skip behavior. Use `ContactType: Port` for port/location contacts when appropriate.
144
+
126
145
  ## Other Related Enums
127
146
 
128
147
  | Enum | Values |
@@ -134,6 +153,33 @@ GraphQL resolver: `formattedAddress(outputFormat, addressFormat, lang, multiline
134
153
  | `ContactStatusStage` | Active=0, Inactive=1 |
135
154
  | `PaymentType` | Card=1, AccountCredit=2, Cash=3, Check=4, BankTransfer=5, Other=7 |
136
155
 
156
+ ## Contact Import
157
+
158
+ GraphQL `importContacts` accepts CSV, JSON, or XLSX uploads and imports rows as dynamic contact field dictionaries.
159
+
160
+ Important rules:
161
+ - Existing contacts match by `ContactId`; otherwise a new contact is created.
162
+ - `ContactId` and `OrganizationId` are primary key fields and are filtered out before create/update field application.
163
+ - Empty strings and nulls are skipped on updates; blank spreadsheet cells do not erase existing values.
164
+ - New contacts require `Name`; rows without a non-empty name are skipped.
165
+ - `ContactType` can come from the row, from the mutation-level `contactType` default, or finally defaults to `Contact`.
166
+ - Nested division objects can set the primary division by name: `division.divisionName` resolves to `divisionId` within the same organization.
167
+
168
+ Example mutation input:
169
+
170
+ ```json
171
+ {
172
+ "organizationId": 1,
173
+ "fileUploadUrl": "uploads/imports/contacts.xlsx",
174
+ "contactType": "Customer",
175
+ "columnMappings": {
176
+ "Name": "Company Name",
177
+ "EmailAddress": "Email",
178
+ "Division.DivisionName": "Division"
179
+ }
180
+ }
181
+ ```
182
+
137
183
  ## CustomValues
138
184
 
139
185
  `Dictionary<string, object?>` stored as PostgreSQL `jsonb`. Access in workflows:
@@ -0,0 +1,195 @@
1
+ # Dispatch Routing Entity Field Reference
2
+
3
+ Dispatch routing covers reusable weekly route templates, daily dispatch routes, route statuses, stop statuses, and stops.
4
+
5
+ > **Proposed API:** The Order Move entities and fields in the section below come from the backend
6
+ > design specification and are not deployed yet. Do not generate production queries or mutations
7
+ > against them until implementation is confirmed.
8
+
9
+ ## DispatchRouteType Enum
10
+
11
+ | Value | Notes |
12
+ |-------|-------|
13
+ | `Delivery` | Delivery route or stop |
14
+ | `Pickup` | Pickup route or stop |
15
+ | `PickupDelivery` | Combined pickup-and-delivery route or stop |
16
+
17
+ ## DispatchRouteStatus
18
+
19
+ | Field | Type | Notes |
20
+ |-------|------|-------|
21
+ | `dispatchRouteStatusId` | `int` | PK |
22
+ | `organizationId` | `int` | Tenant scope |
23
+ | `statusName` | `string` | Display name |
24
+ | `statusDescription` | `string?` | Optional description |
25
+ | `statusStage` | `StatusStage` | `Pending`, `InProgress`, `Completed` |
26
+ | `routeType` | `DispatchRouteType?` | Null applies to any route type |
27
+ | `priority` | `int` | Display sort order |
28
+ | `color` | `string?` | Hex/display color |
29
+ | `customValues` | `Dictionary` | jsonb |
30
+
31
+ ## DispatchRouteStopStatus
32
+
33
+ | Field | Type | Notes |
34
+ |-------|------|-------|
35
+ | `dispatchRouteStopStatusId` | `int` | PK |
36
+ | `organizationId` | `int` | Tenant scope |
37
+ | `statusName` | `string` | Display name; unique per organization for active statuses |
38
+ | `statusDescription` | `string?` | Optional description |
39
+ | `statusStage` | `StatusStage` | `Pending`, `InProgress`, `Completed` |
40
+ | `stopType` | `DispatchRouteType?` | Null applies to any stop type |
41
+ | `priority` | `int` | Display sort order |
42
+ | `color` | `string?` | Hex/display color |
43
+ | `customValues` | `Dictionary` | jsonb |
44
+ | `isDeleted` | `bool` | Soft-delete flag |
45
+
46
+ ## DispatchRouteTemplate
47
+
48
+ | Field | Type | Notes |
49
+ |-------|------|-------|
50
+ | `dispatchRouteTemplateId` | `int` | PK |
51
+ | `organizationId` | `int` | Tenant scope |
52
+ | `name` | `string` | Template name |
53
+ | `routeType` | `DispatchRouteType` | `Delivery`, `Pickup`, or `PickupDelivery` |
54
+ | `daysOfWeek` | `DayOfWeek[]` | Matching generation days |
55
+ | `divisionId` | `int?` | FK to Division |
56
+ | `equipmentTypeId` | `int?` | Planned equipment type |
57
+ | `enabled` | `bool` | Generation skips disabled templates |
58
+ | `stops` | `[DispatchRouteTemplateStop]` | Ordered by `sequence` |
59
+ | `customValues` | `Dictionary` | jsonb |
60
+
61
+ ## DispatchRouteTemplateStop
62
+
63
+ | Field | Type | Notes |
64
+ |-------|------|-------|
65
+ | `dispatchRouteTemplateStopId` | `int` | PK |
66
+ | `dispatchRouteTemplateId` | `int` | Parent template |
67
+ | `stopContactId` | `int?` | Store/location contact (nullable; use with or without `contactAddressId`) |
68
+ | `contactAddressId` | `int?` | Optional stop address |
69
+ | `stopType` | `DispatchRouteType` | Defaults to template route type when omitted |
70
+ | `sequence` | `int` | 1-based order |
71
+ | `estimatedServiceMinutes` | `int?` | Planned service time |
72
+ | `customValues` | `Dictionary` | jsonb (use for ad-hoc address when not using a contact) |
73
+
74
+ ## DispatchRoute
75
+
76
+ | Field | Type | Notes |
77
+ |-------|------|-------|
78
+ | `dispatchRouteId` | `int` | PK |
79
+ | `organizationId` | `int` | Tenant scope |
80
+ | `name` | `string` | Route name |
81
+ | `routeType` | `DispatchRouteType` | `Delivery`, `Pickup`, or `PickupDelivery` |
82
+ | `routeDate` | `Date` | `yyyy-MM-dd` |
83
+ | `dispatchRouteStatusId` | `int` | FK to DispatchRouteStatus |
84
+ | `dispatchRouteTemplateId` | `int?` | Source template when generated |
85
+ | `divisionId` | `int?` | FK to Division |
86
+ | `driverContactId` | `int?` | Driver contact assignment |
87
+ | `equipmentId` | `int?` | Equipment assignment |
88
+ | `isDraft` | `bool` | Generated/new routes start as draft |
89
+ | `isInactive` | `bool` | Soft-delete flag |
90
+ | `collapsedIntoDispatchRouteId` | `int?` | Route merge target |
91
+ | `stops` | `[DispatchRouteStop]` | Ordered by `plannedSequence` |
92
+ | `trackingEvents` | `[TrackingEvent]` | Tracking events linked directly to the route |
93
+ | `customValues` | `Dictionary` | jsonb |
94
+
95
+ ## DispatchRouteStop
96
+
97
+ | Field | Type | Notes |
98
+ |-------|------|-------|
99
+ | `dispatchRouteStopId` | `int` | PK |
100
+ | `dispatchRouteId` | `int` | Parent route |
101
+ | `stopContactId` | `int?` | Store/location contact (nullable; use with or without `contactAddressId`) |
102
+ | `contactAddressId` | `int?` | Optional stop address |
103
+ | `stopType` | `DispatchRouteType` | Defaults to route route type when omitted |
104
+ | `plannedSequence` | `int` | Planned 1-based order |
105
+ | `actualSequence` | `int?` | Actual execution order |
106
+ | `estimatedServiceMinutes` | `int?` | Planned service time |
107
+ | `dispatchRouteStopStatusId` | `int?` | FK to DispatchRouteStopStatus |
108
+ | `dispatchRouteStopStatus` | `DispatchRouteStopStatus?` | Optional expanded stop status |
109
+ | `actualArrivalTime` | `DateTime?` | Actual arrival timestamp |
110
+ | `actualCompletionTime` | `DateTime?` | Actual completion timestamp |
111
+ | `orderMoveId` | `int?` | Optional move this trip stop executes |
112
+ | `orderMove` | `OrderMove?` | Expandable move navigation |
113
+ | `orderIds` | `int[]` | Attached order IDs; always projected |
114
+ | `orders` | `[DispatchRouteStopOrder]` | Attached order links; expand `order` when needed |
115
+ | `trackingEvents` | `[TrackingEvent]` | Tracking events linked directly to this stop |
116
+ | `customValues` | `Dictionary` | jsonb (use for ad-hoc address when not using a contact) |
117
+
118
+ ## DispatchRouteStopOrder
119
+
120
+ | Field | Type | Notes |
121
+ |-------|------|-------|
122
+ | `dispatchRouteStopOrderId` | `int` | PK |
123
+ | `dispatchRouteStopId` | `int` | Parent stop |
124
+ | `orderId` | `int` | Attached order |
125
+ | `order` | `Order?` | Optional expanded order |
126
+ | `dispatchRouteStop` | `DispatchRouteStop?` | Optional expanded stop when reached from `order.dispatchRouteStopOrders` |
127
+ | `customValues` | `Dictionary` | jsonb on the stop/order link |
128
+
129
+ ## Order Moves
130
+
131
+ An order owns ordered `OrderMove` records, and each move owns ordered `OrderMoveLeg` records.
132
+ Vertical-specific routing details belong in `customValues`.
133
+
134
+ ### OrderMove
135
+
136
+ | Field | Type | Notes |
137
+ |-------|------|-------|
138
+ | `orderMoveId` | `int` | PK |
139
+ | `organizationId` | `int` | Tenant scope |
140
+ | `orderId` | `int` | Parent order |
141
+ | `sequence` | `int` | 1-based order within the order |
142
+ | `name` | `string?` | Optional label |
143
+ | `assignedDriverContactId` | `int?` | FK to Contact |
144
+ | `orderMoveStatusId` | `int?` | FK to OrderMoveStatus |
145
+ | `startDate` | `DateTime?` | UTC start |
146
+ | `endDate` | `DateTime?` | UTC end |
147
+ | `customValues` | `Dictionary` | Vertical-specific jsonb data |
148
+ | `orderMoveLegs` | `[OrderMoveLeg]` | Ordered by `sequence` |
149
+ | `dispatchRouteStops` | `[DispatchRouteStop]` | Stops executing this move, ordered by route then planned sequence; may span routes |
150
+ | `trackingEvents` | `[TrackingEvent]` | Move-level milestones |
151
+
152
+ ### OrderMoveLeg
153
+
154
+ | Field | Type | Notes |
155
+ |-------|------|-------|
156
+ | `orderMoveLegId` | `int` | PK |
157
+ | `orderMoveId` | `int` | Parent move |
158
+ | `sequence` | `int` | 1-based order within the move |
159
+ | `name` | `string?` | Optional label |
160
+ | `orderMoveLegStatusId` | `int?` | FK to OrderMoveLegStatus |
161
+ | `startDate` | `DateTime?` | UTC start |
162
+ | `endDate` | `DateTime?` | UTC end |
163
+ | `customValues` | `Dictionary` | Location, event, appointment, and metric data |
164
+ | `trackingEvents` | `[TrackingEvent]` | Leg-level milestones |
165
+
166
+ `OrderMoveStatus` and `OrderMoveLegStatus` are organization-scoped status dictionaries with
167
+ `statusName`, `statusDescription`, `statusStage`, `priority`, `color`, and `customValues`.
168
+ Moves support `OrderMove/Create@1`, `OrderMove/Update@1`, and `OrderMove/Delete@1` workflow tasks.
169
+
170
+ Order creation may include `orderMoves`; the new order supplies their `orderId`, moves default
171
+ to array-position sequence, and nested legs receive 1-based array-order sequences.
172
+ `Order/Import@1` also accepts moves and nested legs. Existing records match by ID, then by
173
+ `ImportOrderOptions.OrderMoveMatchByFields` / `OrderMoveLegMatchByFields` (dotted
174
+ `customValues` paths are supported), and finally by sequence or array position. New records
175
+ are appended and omitted existing records are not deleted.
176
+
177
+ ## GraphQL Notes
178
+
179
+ - Queries: `dispatchRouteStatus`, `dispatchRouteStatuses`, `dispatchRouteStopStatus`, `dispatchRouteStopStatuses`, `dispatchRouteTemplate`, `dispatchRouteTemplates`, `dispatchRoute`, `dispatchRoutes`.
180
+ - Order move queries: `orderMove`, `orderMoves`, `orderMoveStatus`, `orderMoveStatuses`, `orderMoveLegStatus`, and `orderMoveLegStatuses`.
181
+ - Nested order resolvers: `getOrderMoves(filter, orderBy)` returns pending or statusless moves (defaulted to sequence order), while `getOrderMove(filter, orderBy)` returns the first matching pending or statusless move. Moves whose status stage is no longer `Pending` are excluded.
182
+ - Order move mutations create, sparse-update, and soft-delete moves; create/update accepts nested `orderMoveLegs`, reconciled by ID and array order. Omitted or `isDeleted: true` legs are soft-deleted, new already-deleted legs are ignored, and only active legs consume sequence positions.
183
+ - A dynamic order update can supply `orderMoves` to reconcile the full collection with the same rules. New moves inherit `organizationId` and `orderId` from the aggregate; omitted or explicitly deleted moves are soft-deleted; active moves and nested legs are resequenced from 1.
184
+ - Mutations use `input: { organizationId, values }` and return payload fields named `dispatchRouteStatus`, `dispatchRouteStopStatus`, `dispatchRouteTemplate`, `dispatchRoute`, or `generateDispatchRoutesResult`.
185
+ - Stop status mutations: `createDispatchRouteStopStatus`, `updateDispatchRouteStopStatus`, `deleteDispatchRouteStopStatus`.
186
+ - Route stops can be anchored by location (`stopContactId`, `contactAddressId`, ad-hoc `customValues`) or by attached `orderIds`.
187
+ - Route stops optionally expand `orderMove`; the inverse `orderMove.dispatchRouteStops` collection supports filtering and may span multiple dispatch routes.
188
+ - Create accepts nested route stops with `dispatchRouteStopStatusId`, `orderMoveId`, and `orderIds`. `orderMoveId` links the new stop to the move it executes. Dynamic route updates and stop add/update mutations also accept `dispatchRouteStopStatusId` and `orderIds`; statuses are organization-scoped and validated against stop type, and when `orderIds` is present, the attached orders are reconciled to exactly those IDs after organization validation.
189
+ - `updateDispatchRouteStop` uses a dynamic `values` map. It sparse-updates only supplied keys, treats an explicit `null` as clearing a nullable field, and rejects unknown keys. Use `actualArrivalTime` and `actualCompletionTime` to stamp or clear actual stop times without echoing unrelated fields.
190
+ - Dynamic route/template updates can replace the full `stops` array: existing stops with IDs are sparse-updated, new stops are inserted, omitted existing stops are soft-deleted, and sequence/plannedSequence is reassigned from array order. Dedicated stop add/update/remove/reorder mutations remain available for targeted edits.
191
+ - Orders expose `relatedDispatchRoutes(filter, orderBy)` for routes linked through stop order attachments; draft orders return no related routes.
192
+ - Orders expose raw stop membership links through `dispatchRouteStopOrders`; use nested paths such as `dispatchRouteStopOrders.dispatchRouteStop.dispatchRouteStopStatus.statusStage` when filtering orders by route-stop assignment or completion state.
193
+ - Tracking events can be attached directly to dispatch routes and dispatch route stops. Use route-level events for whole-route milestones and stop-level events for arrival, completion, exception, or proof-of-service milestones tied to one stop.
194
+ - Workflow tasks support these links: `TrackingEvent/Create@1` accepts `dispatchRouteId` or `dispatchRouteStopId`; `TrackingEvent/Import@1` requires exactly one target among `orderId`, `dispatchRouteId`, and `dispatchRouteStopId`.
195
+ - Route generation is idempotent per template/date and creates draft routes.
@@ -6,11 +6,12 @@
6
6
  - City
7
7
  - PostalCode
8
8
  - Port
9
+ - Terminal
9
10
  - Vessel
10
11
  - CustomCode
11
12
  - ModeOfTransportation
12
13
 
13
- Country, State, City, PostalCode, Port, Vessel, CustomCode, ModeOfTransportation.
14
+ Country, State, City, PostalCode, Port, Terminal, Vessel, CustomCode, ModeOfTransportation.
14
15
 
15
16
  ## Country
16
17
 
@@ -110,6 +111,30 @@ String-based PK (e.g., UN/LOCODE).
110
111
 
111
112
  ---
112
113
 
114
+ ## Terminal
115
+
116
+ Organization-scoped terminal / operating location (facility record). Optional Port linkage. Soft deleted; codes are unique only among active terminals in the same organization.
117
+
118
+ | Field | Type | Notes |
119
+ |-------|------|-------|
120
+ | `terminalId` | `int` | PK |
121
+ | `organizationId` | `int` | Required; tenant scope |
122
+ | `name` | `string` | Required display name |
123
+ | `code` | `string?` | Optional; unique per organization when not null and `isDeleted=false` |
124
+ | `portId` | `string?` | Optional FK to Port (`organizationId` + `portId`) |
125
+ | `customValues` | `Dictionary` | jsonb; searchable |
126
+ | `isDeleted` | `bool` | Soft delete flag; default false; default queries filter deleted rows |
127
+ | `created` / `createdBy` | `DateTime` / `string` | Audit fields |
128
+ | `lastModified` / `lastModifiedBy` | `DateTime` / `string` | Audit fields |
129
+
130
+ **Navigation:** `organization`, `port`, `createdUser`, `updatedUser`
131
+
132
+ **GraphQL:** `terminal(organizationId, terminalId)`, `terminals(organizationId, filter, search, orderBy)`, `createTerminal`, `updateTerminal`, `deleteTerminal`.
133
+
134
+ **Search:** `name`, `code`, and `customValues`.
135
+
136
+ ---
137
+
113
138
  ## Vessel
114
139
 
115
140
  | Field | Type | Notes |
@@ -36,9 +36,13 @@ Job groups related orders and accounting transactions.
36
36
  | `accountingTransactions` | `[AccountingTransaction]` | Via JobAccountingTransaction join |
37
37
  | `commodities` | `[Commodity]` | Direct FK |
38
38
 
39
- ### GraphQL Computed
39
+ ### GraphQL Child Resolvers
40
40
 
41
- - `getJobOrders(filter)` orders via JobOrder join
41
+ | Field | Type | Notes |
42
+ |-------|------|-------|
43
+ | `getJobOrders(filter)` | `[Order]` | Orders via JobOrder join |
44
+ | `getJobOrder(filter)` | `Order?` | Single order via JobOrder join (UseFirstOrDefault + projection) |
45
+ | `getJobCommodity(filter)` | `Commodity?` | Single commodity linked to job (by JobId) |
42
46
 
43
47
  ---
44
48
 
@@ -81,7 +81,7 @@ Milestone/tracking event on an order or commodity.
81
81
 
82
82
  | Field | Returns | Notes |
83
83
  |-------|---------|-------|
84
- | `businessDays(path: String!)` | `int?` | Business days from the date at `path` to today, using the org's business calendar. `path` is a dot-separated property path on the tracking event (e.g. `"eventDate"`, `"customValues.scheduledAt"`). Returns `null` if path doesn't resolve or value isn't a parseable date. |
84
+ | `businessDays(path: String!, contactId?: Int)` | `int?` | Business days from the date at `path` to today, using the org business calendar. Optional `contactId` scopes availability blocks to org-wide plus matching contact blocks. Instant values are converted to the org timezone before taking the date; date-only values are not timezone-shifted. Returns `null` if path does not resolve or value is not parseable. |
85
85
 
86
86
  ---
87
87
 
@@ -40,6 +40,7 @@ Field names as used in workflow expressions: `{{ entity.orderId }}`, `{{ entity.
40
40
  | Field | Type | Notes |
41
41
  |-------|------|-------|
42
42
  | `orderStatus` | `OrderStatus` | `.statusName`, `.statusStage` |
43
+ | `orderSummary` | `OrderSummaryView` | One-to-one summary view; supports nested sort paths like `orderSummary.totalPieces` |
43
44
  | `division` | `Division` | `.name` |
44
45
  | `equipmentType` | `EquipmentType` | |
45
46
  | `billToContact` | `Contact` | Full contact object |
@@ -50,6 +51,14 @@ Field names as used in workflow expressions: `{{ entity.orderId }}`, `{{ entity.
50
51
  | `createdUser` | `User` | `.firstName`, `.lastName`, `.email` |
51
52
  | `updatedUser` | `User` | |
52
53
 
54
+ ## Workflow Trigger Payload Notes
55
+
56
+ Order entity triggers include scalar status fields (`orderStatusId`, `orderStatusName`) in the lightweight workflow payload. `orderStatusName` is loaded before mapping when needed, so modified-order workflows and Flow auto-transition expressions can rely on it even if the EF event did not preload the `orderStatus` navigation.
57
+
58
+ ## Search Behavior
59
+
60
+ Order GraphQL quick search (`orders(search:)` and `orderGroupBy(search:)`) matches core order fields plus related commodity and inventory item fields. InventoryItem data linked from an order commodity, including child/container commodities, is searchable by `sku`, `productName`, `description`, `modelNumber`, and JSON `customValues`. Use this for warehouse-backed order lookup by SKU, item name, model number, color, size, or similar item attributes.
61
+
53
62
  ## Collection Properties
54
63
 
55
64
  | Field | Type | Notes |
@@ -63,8 +72,10 @@ Field names as used in workflow expressions: `{{ entity.orderId }}`, `{{ entity.
63
72
  | `jobOrders` | `[JobOrder]` | |
64
73
  | `orderTags` | `[OrderTag]` | |
65
74
  | `orderCarriers` | `[OrderCarrier]` | |
75
+ | `dispatchRouteStopOrders` | `[DispatchRouteStopOrder]` | Stop membership links for this order; expand `dispatchRouteStop` to inspect route, stop status, or stop tracking events |
66
76
  | `allTags` | `[OrderAllTagsView]` | View: all tags including from commodities |
67
77
  | `allRelatedOrders` | `[OrderRelatedOrdersView]` | Orders sharing commodities |
78
+ | `relatedDispatchRoutes` | `[DispatchRoute]` | Routes linked through dispatch route stop order attachments; supports `filter` and `orderBy`; draft orders return an empty list |
68
79
  | `attachmentsSummary` | `OrderAttachmentSummaryView?` | DB view: `.totalCount`, `.hasAny` (active attachments) |
69
80
  | `notesSummary` | `OrderNoteSummaryView?` | DB view: `.totalCount`, `.hasAny` (non-deleted notes) |
70
81
  | `outgoingLinks` | `[LinkedOrder]` | |
@@ -98,18 +109,21 @@ These are virtual fields that filter `orderEntities` by type:
98
109
  | `commoditySummary` | `CommoditySummary` | `.totalWeight`, `.totalPieces`, `.totalQuantity`, `.totalVolume` |
99
110
  | `accountingSummary` | `AccountingSummary` | |
100
111
  | `getContact(idPropertyName)` | `Contact` | Resolve contact from `customValues[idPropertyName]` |
112
+ | `getTerminal(idPropertyName)` | `Terminal` | Resolve terminal from int `customValues[idPropertyName]` |
101
113
  | `getPort(idPropertyName)` | `Port` | Resolve port from `customValues[idPropertyName]` |
114
+ | `getTerminal(idPropertyName)` | `Terminal` | Resolve terminal from `customValues[idPropertyName]` |
102
115
  | `getVessel(idPropertyName)` | `Vessel` | Resolve vessel from `customValues[idPropertyName]` |
103
116
  | `getCountry(idPropertyName)` | `Country` | Resolve country from `customValues[idPropertyName]` |
104
117
  | `getRoute(idPropertyName)` | `Route` | Resolve route from `customValues[idPropertyName]` |
105
118
  | `getModeOfTransportation(idPropertyName)` | `ModeOfTransportation` | |
106
119
  | `getCustomCode(idPropertyName)` | `CustomCode` | |
107
120
  | `getRelatedOrderByProperty(idPropertyName)` | `Order` | Resolve related order from `customValues[idPropertyName]` |
121
+ | `relatedDispatchRoutes(filter, orderBy)` | `[DispatchRoute]` | Routes linked through dispatch route stop order attachments; draft orders return an empty list |
108
122
  | `getCharge(chargeDescription)` | `Charge` | Single charge by description |
109
123
  | `getChargesByChargeType(chargeType)` | `[Charge]` | Charges filtered by type |
110
124
  | `getOrderSummary(weightUnit, volumeUnit, dimensionsUnit)` | `OrderSummary` | |
111
- | `lastTrackingEvent(eventDefinitionName, orderBy?)` | `TrackingEvent` | Most recent (or earliest) tracking event, resolved via batched DataLoader. `orderBy` is **honoured**: omit or prefix with `-` for DESC (latest event: `COALESCE(EventDate, Created) DESC, TrackingEventId DESC`); no prefix for ASC (earliest event: same columns ASC). Default is DESC. |
112
- | `businessDays(path: String!)` | `int?` | Business days from the date at `path` to today, using the org's business calendar. `path` is a dot-separated property path on the order (e.g. `"customValues.leg.pickup.scheduledAt"`). Returns `null` if path doesn't resolve or value isn't a parseable date. |
125
+ | `lastTrackingEvent(eventDefinitionName, orderBy?)` | `TrackingEvent` | Most recent (or earliest) tracking event, resolved via batched DataLoader. `eventDefinitionName` may contain pipe-separated names such as `Departed|Delivered`; the resolver selects the winner across all matching definitions. `orderBy` is **honoured**: omit or prefix with `-` for DESC (latest event: `COALESCE(EventDate, Created) DESC, TrackingEventId DESC`); no prefix for ASC (earliest event: same columns ASC). Default is DESC. |
126
+ | `businessDays(path: String!, contactId?: Int)` | `int?` | Business days from the date at `path` to today, using the org business calendar. Optional `contactId` scopes availability blocks to org-wide plus matching contact blocks. Instant values are converted to the org timezone before taking the date; date-only values are not timezone-shifted. Returns `null` if path does not resolve or value is not parseable. |
113
127
  | `attachmentsSummary` | `OrderAttachmentSummaryGqlDto` | `.totalCount` (int), `.hasAny` (bool) — batched DataLoader, backed by DB view |
114
128
  | `notesSummary` | `OrderNoteSummaryGqlDto` | `.totalCount` (int), `.hasAny` (bool) — batched DataLoader, backed by DB view |
115
129
  | `notesCount(threadFilter)` | `int` | |
@@ -179,6 +193,25 @@ inputs:
179
193
 
180
194
  **Known system customValues keys:**
181
195
  - `modeOfTransportationId` / `modeOfTransportationIdDescription` — transport mode
196
+ - `terminalId` — terminal reference; sortable/filterable with `customValues.terminalId->terminal.name`
197
+ - `deliveryLocationId` — contact-address reference; sortable/filterable with `customValues.deliveryLocationId->contactAddress.name`
198
+ - `returnLocationId` — terminal or contact-address return-location reference; sortable/filterable with `customValues.returnLocationId->terminal.name` or `customValues.returnLocationId->contactAddress.name`
199
+
200
+ **Join expression pattern** — Order queries can sort and filter by properties of entities referenced from `customValues` using `customValues.key->entity.property`. Supported aliases include `contact`, `order`, `modeOfTransportation`, `country`, `terminal`, `contactAddress`, `port`, and `vessel`. The `vessel` alias joins an integer custom value to `Vessel.VesselId`.
201
+
202
+ ```graphql
203
+ orders(
204
+ organizationId: 1
205
+ orderBy: "customValues.deliveryLocationId->contactAddress.name"
206
+ filter: "customValues.returnLocationId->contactAddress.name:Chicago*"
207
+ ) { items { orderId orderNumber } }
208
+
209
+ orders(
210
+ organizationId: 1
211
+ orderBy: "customValues.portId->port.name"
212
+ orderBy: "customValues.vesselId->vessel.name"
213
+ ) { items { orderId orderNumber } }
214
+ ```
182
215
 
183
216
  **Resolver pattern** — Many GraphQL fields resolve entities from customValues IDs:
184
- `getContact(idPropertyName)` reads `customValues[idPropertyName]` as a contact ID and returns the full Contact object. Same pattern for ports, vessels, countries, routes, etc.
217
+ `getContact(idPropertyName)` reads `customValues[idPropertyName]` as a contact ID and returns the full Contact object. `getTerminal(idPropertyName)` uses the same pattern for terminal IDs. Same pattern for ports, vessels, countries, routes, etc.