mcp-zenskar 1.1.9 → 1.1.11
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.
- package/README.md +12 -16
- package/package.json +1 -1
- package/src/mcp-config.json +239 -90
- package/src/server.js +301 -7
package/README.md
CHANGED
|
@@ -1,18 +1,17 @@
|
|
|
1
1
|
# Zenskar MCP Server
|
|
2
2
|
|
|
3
|
-
MCP server for the Zenskar API. 103 tools covering customers, contracts, invoices, payments, credit notes, accounting, products, plans,
|
|
3
|
+
MCP server for the Zenskar API. 103 tools covering customers, contracts, invoices, payments, credit notes, accounting, products, plans, and more.
|
|
4
4
|
|
|
5
5
|
## What it does
|
|
6
6
|
|
|
7
|
-
- Customers: list, search, create, update, addresses, contacts, payment methods
|
|
8
|
-
- Contracts: create, read, update, delete, amend, add phases and pricing, expire
|
|
7
|
+
- Customers: list, search, create, update, delete, addresses, contacts, payment methods
|
|
8
|
+
- Contracts: create, read, update, delete, amend, add phases and pricing, pause/resume, expire
|
|
9
9
|
- Invoices: list, get, approve, void, generate, credit notes, download
|
|
10
10
|
- Payments: create, edit, refund, delete, auto-charge
|
|
11
11
|
- Credit notes: list, create against invoice, get by ID
|
|
12
12
|
- Accounting: chart of accounts, journal entries and lines, balance sheet, income statement, account balances
|
|
13
13
|
- Products: CRUD, pricing configurations
|
|
14
14
|
- Plans: list, create, add products, preview estimates
|
|
15
|
-
- Quotes: create, preview, accept (converts to contract)
|
|
16
15
|
- Business entities: list, get, create, update
|
|
17
16
|
- Jobs: monitor async operations
|
|
18
17
|
- Custom attributes and tax categories
|
|
@@ -91,6 +90,7 @@ Once configured, you can ask Claude to interact with your Zenskar data:
|
|
|
91
90
|
| `getCustomerById` | Get a customer by ID |
|
|
92
91
|
| `createCustomer` | Create a customer with address and tax info |
|
|
93
92
|
| `updateCustomer` | Update customer details (partial update) |
|
|
93
|
+
| `deleteCustomer` | Permanently delete a customer by ID (only allowed when they have no active contracts or unpaid invoices) |
|
|
94
94
|
|
|
95
95
|
### Contacts
|
|
96
96
|
| Tool | Description |
|
|
@@ -99,6 +99,7 @@ Once configured, you can ask Claude to interact with your Zenskar data:
|
|
|
99
99
|
| `getContactById` | Get a contact by ID |
|
|
100
100
|
| `createContact` | Create a contact for a customer |
|
|
101
101
|
| `updateContact` | Update a contact's details |
|
|
102
|
+
| `deleteContact` | Delete a contact by ID |
|
|
102
103
|
|
|
103
104
|
### Contracts
|
|
104
105
|
| Tool | Description |
|
|
@@ -112,6 +113,9 @@ Once configured, you can ask Claude to interact with your Zenskar data:
|
|
|
112
113
|
| `createContractPhase` | Add a phase to a contract (add-ons, expansions) |
|
|
113
114
|
| `createContractPhasePricing` | Add pricing to a contract phase |
|
|
114
115
|
| `expireContract` | Expire an active contract |
|
|
116
|
+
| `pauseContract` | Pause an active contract from a given start date, with an unpause-extension policy (`extend` or `overlap`) and optional end date for auto-resume |
|
|
117
|
+
| `editPauseContract` | Edit an existing pause phase — set or change the resume date, shift the start, or change the unpause policy |
|
|
118
|
+
| `resumeContract` | Resume a paused contract |
|
|
115
119
|
| `createContractPrompt` | Create a contract prompt |
|
|
116
120
|
| `extractContractFromRaw` | Extract contract data from raw text using AI |
|
|
117
121
|
|
|
@@ -133,7 +137,8 @@ Once configured, you can ask Claude to interact with your Zenskar data:
|
|
|
133
137
|
| `generateInvoicePaymentLink` | Generate a payment link for an invoice |
|
|
134
138
|
| `payInvoice` | Initiate payment for an invoice |
|
|
135
139
|
| `approveInvoice` | Approve an invoice for billing |
|
|
136
|
-
| `voidInvoice` | Void an
|
|
140
|
+
| `voidInvoice` | Void an invoice |
|
|
141
|
+
| `deleteInvoice` | Delete a draft invoice |
|
|
137
142
|
| `generateInvoice` | Generate an invoice for a contract and date range |
|
|
138
143
|
| `createInvoiceCreditNote` | Create a credit note against an invoice |
|
|
139
144
|
| `createInvoiceCharge` | Auto-charge an invoice via payment gateway |
|
|
@@ -171,8 +176,6 @@ Once configured, you can ask Claude to interact with your Zenskar data:
|
|
|
171
176
|
| `listPlans` | List plan templates |
|
|
172
177
|
| `getPlanById` | Get a plan by ID with phases and pricing |
|
|
173
178
|
| `createPlan` | Create a plan template |
|
|
174
|
-
| `addProductsToPlan` | Add products to an existing plan |
|
|
175
|
-
| `previewPlanEstimate` | Preview estimated billing for a plan |
|
|
176
179
|
|
|
177
180
|
### Accounting
|
|
178
181
|
| Tool | Description |
|
|
@@ -189,14 +192,6 @@ Once configured, you can ask Claude to interact with your Zenskar data:
|
|
|
189
192
|
| `getAccountBalance` | Get balance for a specific GL account |
|
|
190
193
|
| `recogniseRevenue` | Trigger revenue recognition up to a date |
|
|
191
194
|
|
|
192
|
-
### Quotes
|
|
193
|
-
| Tool | Description |
|
|
194
|
-
|---|---|
|
|
195
|
-
| `createQuote` | Create a quote/proposal |
|
|
196
|
-
| `previewQuoteEstimate` | Preview estimated billing for a quote |
|
|
197
|
-
| `getQuoteById` | Get a quote by ID |
|
|
198
|
-
| `acceptQuote` | Accept a quote, converting to a contract |
|
|
199
|
-
|
|
200
195
|
### Custom Attributes and Tax
|
|
201
196
|
| Tool | Description |
|
|
202
197
|
|---|---|
|
|
@@ -227,6 +222,7 @@ Once configured, you can ask Claude to interact with your Zenskar data:
|
|
|
227
222
|
| `updateCustomerAddress` | Update a customer address |
|
|
228
223
|
| `listPaymentMethods` | List payment methods for a customer |
|
|
229
224
|
| `attachPaymentMethod` | Attach a payment method to a customer |
|
|
225
|
+
| `deletePaymentMethod` | Delete a payment method from a customer |
|
|
230
226
|
|
|
231
227
|
### Metrics and Usage Events
|
|
232
228
|
| Tool | Description |
|
|
@@ -239,7 +235,7 @@ Once configured, you can ask Claude to interact with your Zenskar data:
|
|
|
239
235
|
| `listRawMetrics` | List Usage Events with filtering; backend/API may also call these raw metrics |
|
|
240
236
|
| `createRawMetric` | Create a Usage Event schema; backend/API calls this a raw metric |
|
|
241
237
|
| `getRawMetricById` | Get a Usage Event by ID; backend/API may also call it a raw metric |
|
|
242
|
-
| `getRawMetricLogs` | Get
|
|
238
|
+
| `getRawMetricLogs` | Get recent event rows for a Usage Event using the same preview-query path as the frontend |
|
|
243
239
|
| `getRawMetricBySlug` | Get a Usage Event by API slug; backend/API may also call it a raw metric |
|
|
244
240
|
| `ingestRawMetricEvent` | Ingest a usage event |
|
|
245
241
|
|
package/package.json
CHANGED
package/src/mcp-config.json
CHANGED
|
@@ -586,7 +586,18 @@
|
|
|
586
586
|
},
|
|
587
587
|
{
|
|
588
588
|
"name": "payInvoice",
|
|
589
|
-
"description": "Initiate payment for an invoice using a payload.",
|
|
589
|
+
"description": "Initiate payment for an invoice using a payload. MONEY-MOVING ACTION — only call when the user explicitly authorizes payment for this specific invoice.",
|
|
590
|
+
"needsApproval": true,
|
|
591
|
+
"approvalConfig": {
|
|
592
|
+
"title": "Pay Invoice",
|
|
593
|
+
"description": "This will initiate a payment against the invoice using the provided payload.",
|
|
594
|
+
"warningText": "Money-moving action — funds will be charged. Verify the invoice and amount before confirming.",
|
|
595
|
+
"confirmText": "Pay Invoice",
|
|
596
|
+
"cancelText": "Cancel",
|
|
597
|
+
"sensitiveFields": [
|
|
598
|
+
"payload"
|
|
599
|
+
]
|
|
600
|
+
},
|
|
590
601
|
"args": [
|
|
591
602
|
{
|
|
592
603
|
"name": "payload",
|
|
@@ -1060,7 +1071,7 @@
|
|
|
1060
1071
|
},
|
|
1061
1072
|
{
|
|
1062
1073
|
"name": "getRawMetricLogs",
|
|
1063
|
-
"description": "Get Usage Event
|
|
1074
|
+
"description": "Get Usage Event event rows for a specific resource by ID. This follows the same visual-preview query path used by the frontend meter page, so it returns recent event rows rather than only backend sample logs. Backend/API terminology may also call this a raw metric.",
|
|
1064
1075
|
"args": [
|
|
1065
1076
|
{
|
|
1066
1077
|
"name": "rawMetricId",
|
|
@@ -1068,6 +1079,43 @@
|
|
|
1068
1079
|
"type": "string",
|
|
1069
1080
|
"required": true,
|
|
1070
1081
|
"position": "path"
|
|
1082
|
+
},
|
|
1083
|
+
{
|
|
1084
|
+
"name": "filters",
|
|
1085
|
+
"description": "Optional visual-query filter array, matching the frontend usage-event viewer format. Default is an empty filter list.",
|
|
1086
|
+
"type": "array",
|
|
1087
|
+
"required": false,
|
|
1088
|
+
"position": "body"
|
|
1089
|
+
},
|
|
1090
|
+
{
|
|
1091
|
+
"name": "limit",
|
|
1092
|
+
"description": "Maximum number of event rows to return. Defaults to 20, matching frontend behavior.",
|
|
1093
|
+
"type": "integer",
|
|
1094
|
+
"required": false,
|
|
1095
|
+
"position": "body",
|
|
1096
|
+
"default": 20
|
|
1097
|
+
},
|
|
1098
|
+
{
|
|
1099
|
+
"name": "offset",
|
|
1100
|
+
"description": "Pagination offset for event rows. Defaults to 0.",
|
|
1101
|
+
"type": "integer",
|
|
1102
|
+
"required": false,
|
|
1103
|
+
"position": "body",
|
|
1104
|
+
"default": 0
|
|
1105
|
+
},
|
|
1106
|
+
{
|
|
1107
|
+
"name": "aggregate_operation",
|
|
1108
|
+
"description": "Optional aggregate operation object used by the frontend for event counts (for example COUNT on timestamp). Leave null for normal row listing.",
|
|
1109
|
+
"type": "object",
|
|
1110
|
+
"required": false,
|
|
1111
|
+
"position": "body"
|
|
1112
|
+
},
|
|
1113
|
+
{
|
|
1114
|
+
"name": "order_by",
|
|
1115
|
+
"description": "Optional ordering array. Defaults to [{column:'timestamp', type:'DESC'}] to match frontend event viewing.",
|
|
1116
|
+
"type": "array",
|
|
1117
|
+
"required": false,
|
|
1118
|
+
"position": "body"
|
|
1071
1119
|
}
|
|
1072
1120
|
],
|
|
1073
1121
|
"requestTemplate": {
|
|
@@ -1075,7 +1123,7 @@
|
|
|
1075
1123
|
"method": "GET"
|
|
1076
1124
|
},
|
|
1077
1125
|
"responseTemplate": {
|
|
1078
|
-
"prependBody": "## Usage Event Logs\n\nHere are
|
|
1126
|
+
"prependBody": "## Usage Event Logs\n\nHere are recent event rows for the specified usage event:\n\n"
|
|
1079
1127
|
}
|
|
1080
1128
|
},
|
|
1081
1129
|
{
|
|
@@ -2682,7 +2730,18 @@
|
|
|
2682
2730
|
},
|
|
2683
2731
|
{
|
|
2684
2732
|
"name": "deleteContract",
|
|
2685
|
-
"description": "
|
|
2733
|
+
"description": "Permanently delete a DRAFT contract. ONLY works on contracts with status='draft'. Do NOT call on ACTIVE, EXPIRED, or PAUSED contracts. DESTRUCTIVE: removes all phases, products, and pricing associations. NEVER call as automatic recovery from another failed operation (e.g., do not retry as a fallback after expireContract or voidInvoice fails). ONLY call when the user explicitly says 'delete' or 'remove' for THIS draft contract. If a non-destructive action fails, surface the error verbatim and stop.",
|
|
2734
|
+
"needsApproval": true,
|
|
2735
|
+
"approvalConfig": {
|
|
2736
|
+
"title": "Delete Draft Contract",
|
|
2737
|
+
"description": "This will permanently delete the draft contract along with all its phases, products, and pricing associations.",
|
|
2738
|
+
"warningText": "DESTRUCTIVE — this cannot be undone. Only draft contracts may be deleted; verify status before confirming.",
|
|
2739
|
+
"confirmText": "Delete Contract",
|
|
2740
|
+
"cancelText": "Cancel",
|
|
2741
|
+
"sensitiveFields": [
|
|
2742
|
+
"contractId"
|
|
2743
|
+
]
|
|
2744
|
+
},
|
|
2686
2745
|
"args": [
|
|
2687
2746
|
{
|
|
2688
2747
|
"name": "contractId",
|
|
@@ -2919,7 +2978,19 @@
|
|
|
2919
2978
|
},
|
|
2920
2979
|
{
|
|
2921
2980
|
"name": "expireContract",
|
|
2922
|
-
"description": "Expire an
|
|
2981
|
+
"description": "Expire an ACTIVE or PAUSED contract by adjusting its end_date. Allowed status transitions: ACTIVE→EXPIRED, PAUSED→EXPIRED. EXPIRED is terminal — calling on an already-expired contract returns 400; do NOT retry, do NOT escalate to deleteContract or any other destructive tool as a fallback. Verify contract.status via getContractById before calling. Idempotency: NO. The contract expires at end of day 23:59:59.999999.",
|
|
2982
|
+
"needsApproval": true,
|
|
2983
|
+
"approvalConfig": {
|
|
2984
|
+
"title": "Expire Contract",
|
|
2985
|
+
"description": "This will set the contract's end_date and transition status to EXPIRED. EXPIRED is terminal.",
|
|
2986
|
+
"warningText": "Once expired, the contract cannot be re-activated through this endpoint. Future-dated phases beyond the expiry will be removed.",
|
|
2987
|
+
"confirmText": "Expire Contract",
|
|
2988
|
+
"cancelText": "Cancel",
|
|
2989
|
+
"sensitiveFields": [
|
|
2990
|
+
"contractId",
|
|
2991
|
+
"expiry_date"
|
|
2992
|
+
]
|
|
2993
|
+
},
|
|
2923
2994
|
"args": [
|
|
2924
2995
|
{
|
|
2925
2996
|
"name": "contractId",
|
|
@@ -2930,7 +3001,7 @@
|
|
|
2930
3001
|
},
|
|
2931
3002
|
{
|
|
2932
3003
|
"name": "expiry_date",
|
|
2933
|
-
"description": "Date to expire the contract (e.g. 2026-12-31). Defaults to today if not provided.",
|
|
3004
|
+
"description": "Date to expire the contract (e.g. 2026-12-31). Defaults to today if not provided. Must not be earlier than contract.start_date.",
|
|
2934
3005
|
"type": "string",
|
|
2935
3006
|
"required": false,
|
|
2936
3007
|
"position": "body"
|
|
@@ -2949,7 +3020,18 @@
|
|
|
2949
3020
|
},
|
|
2950
3021
|
{
|
|
2951
3022
|
"name": "voidInvoice",
|
|
2952
|
-
"description": "Void an invoice
|
|
3023
|
+
"description": "Void an invoice. ONLY works on approved or paid invoices with invoice_total == 0 (zero-balance, typically after a full credit-note offset). For non-zero approved invoices, issue a credit note via createInvoiceCreditNote first to bring the balance to zero, then void. If the call returns INVOICE_CANNOT_BE_VOIDED, surface that error verbatim — do NOT retry, do NOT escalate to deleteInvoice.",
|
|
3024
|
+
"needsApproval": true,
|
|
3025
|
+
"approvalConfig": {
|
|
3026
|
+
"title": "Void Invoice",
|
|
3027
|
+
"description": "This will set the invoice status to 'void' and post the corresponding accounting entries. Only zero-balance approved/paid invoices are eligible.",
|
|
3028
|
+
"warningText": "Voiding affects accounting records and cannot be easily undone.",
|
|
3029
|
+
"confirmText": "Void Invoice",
|
|
3030
|
+
"cancelText": "Cancel",
|
|
3031
|
+
"sensitiveFields": [
|
|
3032
|
+
"invoiceId"
|
|
3033
|
+
]
|
|
3034
|
+
},
|
|
2953
3035
|
"args": [
|
|
2954
3036
|
{
|
|
2955
3037
|
"name": "invoiceId",
|
|
@@ -2970,6 +3052,40 @@
|
|
|
2970
3052
|
"prependBody": "## Invoice Voided\n\n"
|
|
2971
3053
|
}
|
|
2972
3054
|
},
|
|
3055
|
+
{
|
|
3056
|
+
"name": "deleteInvoice",
|
|
3057
|
+
"description": "Permanently delete a draft invoice. ONLY works on status='draft'. For approved invoices, use voidInvoice (with caveats) or createInvoiceCreditNote — do NOT call deleteInvoice on approved invoices. DESTRUCTIVE: record removed permanently. ONLY call when the user explicitly says 'delete' for THIS invoice; never call as automatic recovery from another failed operation.",
|
|
3058
|
+
"needsApproval": true,
|
|
3059
|
+
"approvalConfig": {
|
|
3060
|
+
"title": "Delete Invoice",
|
|
3061
|
+
"description": "This will permanently delete the draft invoice.",
|
|
3062
|
+
"warningText": "DESTRUCTIVE — invoice record will be removed and cannot be recovered.",
|
|
3063
|
+
"confirmText": "Delete Invoice",
|
|
3064
|
+
"cancelText": "Cancel",
|
|
3065
|
+
"sensitiveFields": [
|
|
3066
|
+
"invoiceId"
|
|
3067
|
+
]
|
|
3068
|
+
},
|
|
3069
|
+
"args": [
|
|
3070
|
+
{
|
|
3071
|
+
"name": "invoiceId",
|
|
3072
|
+
"description": "The unique identifier (UUID) of the draft or upcoming invoice to delete.",
|
|
3073
|
+
"type": "string",
|
|
3074
|
+
"required": true,
|
|
3075
|
+
"position": "path"
|
|
3076
|
+
}
|
|
3077
|
+
],
|
|
3078
|
+
"requestTemplate": {
|
|
3079
|
+
"url": "/invoices/{invoiceId}",
|
|
3080
|
+
"method": "DELETE",
|
|
3081
|
+
"headers": {
|
|
3082
|
+
"Content-Type": "application/json"
|
|
3083
|
+
}
|
|
3084
|
+
},
|
|
3085
|
+
"responseTemplate": {
|
|
3086
|
+
"prependBody": "## Invoice Deleted\n\n"
|
|
3087
|
+
}
|
|
3088
|
+
},
|
|
2973
3089
|
{
|
|
2974
3090
|
"name": "createInvoiceCreditNote",
|
|
2975
3091
|
"description": "Create a credit note against a specific invoice. The credit_note_amount is in the invoice's currency (e.g. 25 for $25). The API stores amounts in cents internally. Returns the created credit note.",
|
|
@@ -3009,7 +3125,7 @@
|
|
|
3009
3125
|
},
|
|
3010
3126
|
{
|
|
3011
3127
|
"name": "generateInvoice",
|
|
3012
|
-
"description": "Generate
|
|
3128
|
+
"description": "Generate ONE invoice covering ALL products active in the (contract, customer, from_date, to_date) tuple. The invoice contains every applicable product as separate line items — do NOT call once per product. Granularity is per-phase, NOT per-product. To bill a multi-phase contract, call ONCE per phase using that phase's start/end as from_date/to_date — e.g., a 2-phase contract with 2 products = 2 calls (yielding 2 invoices, each with 2 line items), NOT 4 calls. Iterating per product produces fragmented duplicate invoices and is incorrect. Use for billing replay or on-demand invoice generation.",
|
|
3013
3129
|
"args": [
|
|
3014
3130
|
{
|
|
3015
3131
|
"name": "contract_id",
|
|
@@ -3265,7 +3381,18 @@
|
|
|
3265
3381
|
},
|
|
3266
3382
|
{
|
|
3267
3383
|
"name": "deleteManualPayment",
|
|
3268
|
-
"description": "
|
|
3384
|
+
"description": "Permanently delete a manual payment record. Only works on payments in an eligible status for deletion. DESTRUCTIVE — only call when the user explicitly says 'delete' for THIS payment; never as automatic recovery from another failed operation.",
|
|
3385
|
+
"needsApproval": true,
|
|
3386
|
+
"approvalConfig": {
|
|
3387
|
+
"title": "Delete Manual Payment",
|
|
3388
|
+
"description": "This will permanently remove the manual payment record.",
|
|
3389
|
+
"warningText": "DESTRUCTIVE — payment record will be removed and cannot be recovered. Linked invoice balance may shift as a result.",
|
|
3390
|
+
"confirmText": "Delete Payment",
|
|
3391
|
+
"cancelText": "Cancel",
|
|
3392
|
+
"sensitiveFields": [
|
|
3393
|
+
"paymentId"
|
|
3394
|
+
]
|
|
3395
|
+
},
|
|
3269
3396
|
"args": [
|
|
3270
3397
|
{
|
|
3271
3398
|
"name": "paymentId",
|
|
@@ -4089,7 +4216,7 @@
|
|
|
4089
4216
|
},
|
|
4090
4217
|
{
|
|
4091
4218
|
"name": "createProductPricing",
|
|
4092
|
-
"description": "Create a new pricing configuration for a product. The pricing_data object is a discriminated union — the 'pricing_type' field determines which pricing model schema
|
|
4219
|
+
"description": "Create a new pricing configuration for a product. The pricing_data object is a discriminated union — the 'pricing_type' field determines which pricing model schema. IMPORTANT: ask the user explicitly for currency, quantity_type (fixed|metered), and unit amount BEFORE calling. Do not silently default any of these. unit_amount is in MAJOR currency units (float), e.g. 3 means $3, not 300 cents.",
|
|
4093
4220
|
"args": [
|
|
4094
4221
|
{
|
|
4095
4222
|
"name": "productId",
|
|
@@ -4114,11 +4241,18 @@
|
|
|
4114
4241
|
},
|
|
4115
4242
|
{
|
|
4116
4243
|
"name": "pricing_data",
|
|
4117
|
-
"description": "Pricing data object (required).
|
|
4244
|
+
"description": "Pricing data object (required). MUST include 'pricing_type' discriminator AND 'currency' (ISO 4217, e.g. 'USD'). 'unit_amount' is in MAJOR currency units (float) — 3 means $3, NOT 300 cents. Do NOT convert to cents. Supported pricing_type values with examples: flat_fee: {pricing_type:'flat_fee', unit_amount:100, currency:'USD'}. per_unit: {pricing_type:'per_unit', unit_amount:3, currency:'USD'}. tiered: {pricing_type:'tiered', unit_amount:[10,5], up_to:[100,null], currency:'USD'}. volume: {pricing_type:'volume', unit_amount:[10,5], up_to:[100,null], currency:'USD'}. percent: {pricing_type:'percent', percentage:5.0, currency:'USD'}. package: {pricing_type:'package', package_size:10, unit_amount:50, currency:'USD'}. step: {pricing_type:'step', unit_amount:[...], up_to:[...], currency:'USD'}. matrix: {pricing_type:'matrix', dimensions:[...], values:[...], currency:'USD'}. Optional inside pricing_data for per_unit: 'proration_type' ('day_based'|'cadence_based'), 'charge_full_amount' (bool).",
|
|
4118
4245
|
"type": "object",
|
|
4119
4246
|
"required": true,
|
|
4120
4247
|
"position": "body"
|
|
4121
4248
|
},
|
|
4249
|
+
{
|
|
4250
|
+
"name": "quantity",
|
|
4251
|
+
"description": "Quantity configuration (REQUIRED for usage-based pricing — ask the user). Top-level object, NOT inside pricing_data. Shape: {type: 'fixed'|'metered', quantity?: number, unit?: string, aggregate_id?: UUID}. 'fixed' means a static quantity (e.g. seats). 'metered' means consumption tracked via a billable metric — set aggregate_id to the billable-metric UUID. Without a quantity object the resulting pricing has no billing_metric and renders as 0 in the UI.",
|
|
4252
|
+
"type": "object",
|
|
4253
|
+
"required": false,
|
|
4254
|
+
"position": "body"
|
|
4255
|
+
},
|
|
4122
4256
|
{
|
|
4123
4257
|
"name": "billing_period",
|
|
4124
4258
|
"description": "Billing cadence for this pricing. Object with 'cadence' (ISO 8601 duration, e.g. 'P1M' for monthly, 'P3M' for quarterly, 'P1Y' for annually) and optional 'offset'. Example: {cadence: 'P1M'}.",
|
|
@@ -4154,7 +4288,7 @@
|
|
|
4154
4288
|
},
|
|
4155
4289
|
{
|
|
4156
4290
|
"name": "listPlans",
|
|
4157
|
-
"description": "Retrieve a paginated list of plans
|
|
4291
|
+
"description": "Retrieve a paginated list of plans that define reusable contract structures. Hits /plans — the same data the Zenskar app's Plans page reads.",
|
|
4158
4292
|
"args": [
|
|
4159
4293
|
{
|
|
4160
4294
|
"name": "cursor",
|
|
@@ -4186,7 +4320,7 @@
|
|
|
4186
4320
|
}
|
|
4187
4321
|
],
|
|
4188
4322
|
"requestTemplate": {
|
|
4189
|
-
"url": "/
|
|
4323
|
+
"url": "/plans",
|
|
4190
4324
|
"method": "GET",
|
|
4191
4325
|
"headers": {
|
|
4192
4326
|
"Content-Type": "application/json"
|
|
@@ -4209,7 +4343,7 @@
|
|
|
4209
4343
|
}
|
|
4210
4344
|
],
|
|
4211
4345
|
"requestTemplate": {
|
|
4212
|
-
"url": "/
|
|
4346
|
+
"url": "/plans/{planId}",
|
|
4213
4347
|
"method": "GET",
|
|
4214
4348
|
"headers": {
|
|
4215
4349
|
"Content-Type": "application/json"
|
|
@@ -4221,7 +4355,7 @@
|
|
|
4221
4355
|
},
|
|
4222
4356
|
{
|
|
4223
4357
|
"name": "createPlan",
|
|
4224
|
-
"description": "Create a new plan
|
|
4358
|
+
"description": "Create a new plan — a reusable contract template with products and pricing. Hits POST /plans (same store the Zenskar app's Plans page reads).",
|
|
4225
4359
|
"args": [
|
|
4226
4360
|
{
|
|
4227
4361
|
"name": "name",
|
|
@@ -4281,7 +4415,7 @@
|
|
|
4281
4415
|
}
|
|
4282
4416
|
],
|
|
4283
4417
|
"requestTemplate": {
|
|
4284
|
-
"url": "/
|
|
4418
|
+
"url": "/plans",
|
|
4285
4419
|
"method": "POST",
|
|
4286
4420
|
"headers": {
|
|
4287
4421
|
"Content-Type": "application/json"
|
|
@@ -4291,73 +4425,6 @@
|
|
|
4291
4425
|
"prependBody": "## Created Plan\n\n"
|
|
4292
4426
|
}
|
|
4293
4427
|
},
|
|
4294
|
-
{
|
|
4295
|
-
"name": "addProductsToPlan",
|
|
4296
|
-
"description": "Add products with pricing to an existing plan.",
|
|
4297
|
-
"args": [
|
|
4298
|
-
{
|
|
4299
|
-
"name": "planId",
|
|
4300
|
-
"description": "The unique identifier of the plan.",
|
|
4301
|
-
"type": "string",
|
|
4302
|
-
"required": true,
|
|
4303
|
-
"position": "path"
|
|
4304
|
-
},
|
|
4305
|
-
{
|
|
4306
|
-
"name": "products",
|
|
4307
|
-
"description": "Array of product objects to add, each with product_id and pricing configuration.",
|
|
4308
|
-
"type": "array",
|
|
4309
|
-
"required": true,
|
|
4310
|
-
"position": "body"
|
|
4311
|
-
}
|
|
4312
|
-
],
|
|
4313
|
-
"requestTemplate": {
|
|
4314
|
-
"url": "/templates/plan/{planId}/products",
|
|
4315
|
-
"method": "POST",
|
|
4316
|
-
"headers": {
|
|
4317
|
-
"Content-Type": "application/json"
|
|
4318
|
-
}
|
|
4319
|
-
},
|
|
4320
|
-
"responseTemplate": {
|
|
4321
|
-
"prependBody": "## Updated Plan Products\n\n"
|
|
4322
|
-
}
|
|
4323
|
-
},
|
|
4324
|
-
{
|
|
4325
|
-
"name": "previewPlanEstimate",
|
|
4326
|
-
"description": "Preview the estimated billing for a plan, showing projected charges per phase.",
|
|
4327
|
-
"args": [
|
|
4328
|
-
{
|
|
4329
|
-
"name": "planId",
|
|
4330
|
-
"description": "The unique identifier of the plan.",
|
|
4331
|
-
"type": "string",
|
|
4332
|
-
"required": true,
|
|
4333
|
-
"position": "path"
|
|
4334
|
-
},
|
|
4335
|
-
{
|
|
4336
|
-
"name": "start_date",
|
|
4337
|
-
"description": "Start date for the estimate (YYYY-MM-DD).",
|
|
4338
|
-
"type": "string",
|
|
4339
|
-
"required": true,
|
|
4340
|
-
"position": "body"
|
|
4341
|
-
},
|
|
4342
|
-
{
|
|
4343
|
-
"name": "end_date",
|
|
4344
|
-
"description": "End date for the estimate (YYYY-MM-DD). Optional — calculated from plan duration if omitted.",
|
|
4345
|
-
"type": "string",
|
|
4346
|
-
"required": false,
|
|
4347
|
-
"position": "body"
|
|
4348
|
-
}
|
|
4349
|
-
],
|
|
4350
|
-
"requestTemplate": {
|
|
4351
|
-
"url": "/templates/plan/{planId}/preview",
|
|
4352
|
-
"method": "POST",
|
|
4353
|
-
"headers": {
|
|
4354
|
-
"Content-Type": "application/json"
|
|
4355
|
-
}
|
|
4356
|
-
},
|
|
4357
|
-
"responseTemplate": {
|
|
4358
|
-
"prependBody": "## Plan Estimate Preview\n\n"
|
|
4359
|
-
}
|
|
4360
|
-
},
|
|
4361
4428
|
{
|
|
4362
4429
|
"name": "listCustomAttributes",
|
|
4363
4430
|
"description": "Retrieve a list of custom attribute definitions configured for the organization.",
|
|
@@ -5070,7 +5137,18 @@
|
|
|
5070
5137
|
},
|
|
5071
5138
|
{
|
|
5072
5139
|
"name": "deleteCustomer",
|
|
5073
|
-
"description": "
|
|
5140
|
+
"description": "Permanently delete a customer by ID. DESTRUCTIVE and cannot be undone. The customer must not have active contracts or unpaid invoices. ONLY call when the user explicitly says 'delete' for THIS customer; never as automatic recovery from another failed operation.",
|
|
5141
|
+
"needsApproval": true,
|
|
5142
|
+
"approvalConfig": {
|
|
5143
|
+
"title": "Delete Customer",
|
|
5144
|
+
"description": "This will permanently delete the customer and all associated records.",
|
|
5145
|
+
"warningText": "DESTRUCTIVE — customer record cannot be recovered. Verify there are no active contracts or unpaid invoices first.",
|
|
5146
|
+
"confirmText": "Delete Customer",
|
|
5147
|
+
"cancelText": "Cancel",
|
|
5148
|
+
"sensitiveFields": [
|
|
5149
|
+
"customerId"
|
|
5150
|
+
]
|
|
5151
|
+
},
|
|
5074
5152
|
"args": [
|
|
5075
5153
|
{
|
|
5076
5154
|
"name": "customerId",
|
|
@@ -5093,7 +5171,18 @@
|
|
|
5093
5171
|
},
|
|
5094
5172
|
{
|
|
5095
5173
|
"name": "deleteContact",
|
|
5096
|
-
"description": "
|
|
5174
|
+
"description": "Permanently delete a contact by ID. DESTRUCTIVE — only call when the user explicitly says 'delete' for THIS contact; never as automatic recovery from another failed operation.",
|
|
5175
|
+
"needsApproval": true,
|
|
5176
|
+
"approvalConfig": {
|
|
5177
|
+
"title": "Delete Contact",
|
|
5178
|
+
"description": "This will permanently delete the contact record.",
|
|
5179
|
+
"warningText": "DESTRUCTIVE — contact record will be removed and cannot be recovered.",
|
|
5180
|
+
"confirmText": "Delete Contact",
|
|
5181
|
+
"cancelText": "Cancel",
|
|
5182
|
+
"sensitiveFields": [
|
|
5183
|
+
"contactId"
|
|
5184
|
+
]
|
|
5185
|
+
},
|
|
5097
5186
|
"args": [
|
|
5098
5187
|
{
|
|
5099
5188
|
"name": "contactId",
|
|
@@ -5116,7 +5205,19 @@
|
|
|
5116
5205
|
},
|
|
5117
5206
|
{
|
|
5118
5207
|
"name": "deletePaymentMethod",
|
|
5119
|
-
"description": "
|
|
5208
|
+
"description": "Permanently delete a payment method from a customer. DESTRUCTIVE — saved card/bank details will be removed; recurring auto-charges using this method will fail. Only call when the user explicitly says 'delete' for THIS payment method.",
|
|
5209
|
+
"needsApproval": true,
|
|
5210
|
+
"approvalConfig": {
|
|
5211
|
+
"title": "Delete Payment Method",
|
|
5212
|
+
"description": "This will permanently delete the payment method from the customer's profile.",
|
|
5213
|
+
"warningText": "DESTRUCTIVE — saved payment details will be removed. Any auto-charge or scheduled charge using this method will fail.",
|
|
5214
|
+
"confirmText": "Delete Payment Method",
|
|
5215
|
+
"cancelText": "Cancel",
|
|
5216
|
+
"sensitiveFields": [
|
|
5217
|
+
"customerId",
|
|
5218
|
+
"paymentMethodId"
|
|
5219
|
+
]
|
|
5220
|
+
},
|
|
5120
5221
|
"args": [
|
|
5121
5222
|
{
|
|
5122
5223
|
"name": "customerId",
|
|
@@ -5146,7 +5247,7 @@
|
|
|
5146
5247
|
},
|
|
5147
5248
|
{
|
|
5148
5249
|
"name": "pauseContract",
|
|
5149
|
-
"description": "Pause an active contract.
|
|
5250
|
+
"description": "Pause an active contract. ALWAYS ask the user explicitly for both 'start_date' and 'unpause_extension_policy' before calling — do NOT silently default. A future-dated start_date will create a scheduled pause that has not yet begun; the contract's top-level status stays 'active' until start_date passes. Use 'editPauseContract' to adjust the pause window or set a resume date afterward.",
|
|
5150
5251
|
"args": [
|
|
5151
5252
|
{
|
|
5152
5253
|
"name": "contractId",
|
|
@@ -5157,14 +5258,14 @@
|
|
|
5157
5258
|
},
|
|
5158
5259
|
{
|
|
5159
5260
|
"name": "start_date",
|
|
5160
|
-
"description": "Date when the pause begins (ISO 8601 format, e.g. 2026-04-01T00:00:00).",
|
|
5261
|
+
"description": "Date when the pause begins (ISO 8601 format, e.g. 2026-04-01T00:00:00). REQUIRED — ask the user; never default to today or a future date silently.",
|
|
5161
5262
|
"type": "string",
|
|
5162
5263
|
"required": true,
|
|
5163
5264
|
"position": "body"
|
|
5164
5265
|
},
|
|
5165
5266
|
{
|
|
5166
5267
|
"name": "unpause_extension_policy",
|
|
5167
|
-
"description": "How to handle the contract end date when unpaused.",
|
|
5268
|
+
"description": "How to handle the contract end date when unpaused. 'extend' pushes the end_date out by the pause duration; 'overlap' keeps end_date fixed. ASK the user.",
|
|
5168
5269
|
"type": "string",
|
|
5169
5270
|
"required": true,
|
|
5170
5271
|
"position": "body",
|
|
@@ -5192,9 +5293,57 @@
|
|
|
5192
5293
|
"prependBody": "## Contract Paused\n\n"
|
|
5193
5294
|
}
|
|
5194
5295
|
},
|
|
5296
|
+
{
|
|
5297
|
+
"name": "editPauseContract",
|
|
5298
|
+
"description": "Edit an existing pause phase on a contract. Use this when 'resumeContract' returns 'pause phase not found' for a future-dated pause, or when the user wants to set a resume date (pause end_date), shift the pause start, or change the unpause_extension_policy. Hits PATCH /contract_v2/{contractId}/pause.",
|
|
5299
|
+
"args": [
|
|
5300
|
+
{
|
|
5301
|
+
"name": "contractId",
|
|
5302
|
+
"description": "The unique identifier (UUID) of the contract whose pause phase to edit.",
|
|
5303
|
+
"type": "string",
|
|
5304
|
+
"required": true,
|
|
5305
|
+
"position": "path"
|
|
5306
|
+
},
|
|
5307
|
+
{
|
|
5308
|
+
"name": "start_date",
|
|
5309
|
+
"description": "New pause start date (ISO 8601). Optional.",
|
|
5310
|
+
"type": "string",
|
|
5311
|
+
"required": false,
|
|
5312
|
+
"position": "body"
|
|
5313
|
+
},
|
|
5314
|
+
{
|
|
5315
|
+
"name": "end_date",
|
|
5316
|
+
"description": "Resume date — when the pause ends (ISO 8601). Set this to schedule a future resume.",
|
|
5317
|
+
"type": "string",
|
|
5318
|
+
"required": false,
|
|
5319
|
+
"position": "body"
|
|
5320
|
+
},
|
|
5321
|
+
{
|
|
5322
|
+
"name": "unpause_extension_policy",
|
|
5323
|
+
"description": "How to handle the contract end date when unpaused. 'extend' or 'overlap'.",
|
|
5324
|
+
"type": "string",
|
|
5325
|
+
"required": false,
|
|
5326
|
+
"position": "body",
|
|
5327
|
+
"enum": [
|
|
5328
|
+
"extend",
|
|
5329
|
+
"overlap"
|
|
5330
|
+
]
|
|
5331
|
+
}
|
|
5332
|
+
],
|
|
5333
|
+
"requestTemplate": {
|
|
5334
|
+
"url": "/contract_v2/{contractId}/pause",
|
|
5335
|
+
"method": "PATCH",
|
|
5336
|
+
"headers": {
|
|
5337
|
+
"Content-Type": "application/json"
|
|
5338
|
+
}
|
|
5339
|
+
},
|
|
5340
|
+
"responseTemplate": {
|
|
5341
|
+
"prependBody": "## Pause Phase Updated\n\n"
|
|
5342
|
+
}
|
|
5343
|
+
},
|
|
5195
5344
|
{
|
|
5196
5345
|
"name": "resumeContract",
|
|
5197
|
-
"description": "Resume a
|
|
5346
|
+
"description": "Resume an actively-running pause on a contract. PRECONDITIONS: the pause's start_date must be strictly in the past (not today, not future). For pauses scheduled for a future start, or to set a future resume date, use 'editPauseContract' instead (sets the pause's end_date). The error 'pause phase not found' means no pause phase has yet started — NOT that no pause exists. If this call fails, surface the error verbatim and consider 'editPauseContract'; do NOT escalate to deleteContract or any destructive fallback.",
|
|
5198
5347
|
"args": [
|
|
5199
5348
|
{
|
|
5200
5349
|
"name": "contractId",
|
package/src/server.js
CHANGED
|
@@ -211,6 +211,20 @@ function normalizeAccountCategory(category) {
|
|
|
211
211
|
return normalized;
|
|
212
212
|
}
|
|
213
213
|
|
|
214
|
+
function toDisplayAmount(value) {
|
|
215
|
+
const num = Number(value);
|
|
216
|
+
if (!Number.isFinite(num)) return null;
|
|
217
|
+
return num / 100;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function normalizeReportAccountCategory(reportType, category) {
|
|
221
|
+
const normalized = normalizeAccountCategory(category);
|
|
222
|
+
if (reportType === 'getBalanceSheet' && normalized === 'Liabilities') {
|
|
223
|
+
return 'Liabilities & Equity';
|
|
224
|
+
}
|
|
225
|
+
return normalized;
|
|
226
|
+
}
|
|
227
|
+
|
|
214
228
|
function getCategoryOrder(reportType, category) {
|
|
215
229
|
const balanceSheetOrder = {
|
|
216
230
|
Assets: 1,
|
|
@@ -247,7 +261,7 @@ function buildAccountingStatementView(reportType, rows, accountLookup) {
|
|
|
247
261
|
const sectionsMap = new Map();
|
|
248
262
|
|
|
249
263
|
rows.forEach(row => {
|
|
250
|
-
const category =
|
|
264
|
+
const category = normalizeReportAccountCategory(reportType, row.account_category);
|
|
251
265
|
const periodKey = buildPeriodKey(row);
|
|
252
266
|
const periodEntry = periodMap.get(periodKey) || {
|
|
253
267
|
key: periodKey,
|
|
@@ -290,8 +304,11 @@ function buildAccountingStatementView(reportType, rows, accountLookup) {
|
|
|
290
304
|
interval_start: row.interval_start || null,
|
|
291
305
|
interval_end: row.interval_end || null,
|
|
292
306
|
balance: row.balance ?? 0,
|
|
307
|
+
display_balance: toDisplayAmount(row.balance ?? 0),
|
|
293
308
|
debits: row.debits ?? 0,
|
|
294
|
-
|
|
309
|
+
display_debits: toDisplayAmount(row.debits ?? 0),
|
|
310
|
+
credits: row.credits ?? 0,
|
|
311
|
+
display_credits: toDisplayAmount(row.credits ?? 0)
|
|
295
312
|
};
|
|
296
313
|
account.total_balance += Number(row.balance || 0);
|
|
297
314
|
account.total_debits += Number(row.debits || 0);
|
|
@@ -302,11 +319,17 @@ function buildAccountingStatementView(reportType, rows, accountLookup) {
|
|
|
302
319
|
interval_end: row.interval_end || null,
|
|
303
320
|
balance: 0,
|
|
304
321
|
debits: 0,
|
|
305
|
-
credits: 0
|
|
322
|
+
credits: 0,
|
|
323
|
+
display_balance: 0,
|
|
324
|
+
display_debits: 0,
|
|
325
|
+
display_credits: 0
|
|
306
326
|
};
|
|
307
327
|
sectionPeriod.balance += Number(row.balance || 0);
|
|
308
328
|
sectionPeriod.debits += Number(row.debits || 0);
|
|
309
329
|
sectionPeriod.credits += Number(row.credits || 0);
|
|
330
|
+
sectionPeriod.display_balance = toDisplayAmount(sectionPeriod.balance);
|
|
331
|
+
sectionPeriod.display_debits = toDisplayAmount(sectionPeriod.debits);
|
|
332
|
+
sectionPeriod.display_credits = toDisplayAmount(sectionPeriod.credits);
|
|
310
333
|
section.totals_by_period[periodKey] = sectionPeriod;
|
|
311
334
|
section.total_balance += Number(row.balance || 0);
|
|
312
335
|
section.total_debits += Number(row.debits || 0);
|
|
@@ -328,8 +351,11 @@ function buildAccountingStatementView(reportType, rows, accountLookup) {
|
|
|
328
351
|
})),
|
|
329
352
|
totals_by_period: summarizePeriods(section.totals_by_period),
|
|
330
353
|
total_balance: section.total_balance,
|
|
354
|
+
display_total_balance: toDisplayAmount(section.total_balance),
|
|
331
355
|
total_debits: section.total_debits,
|
|
332
|
-
|
|
356
|
+
display_total_debits: toDisplayAmount(section.total_debits),
|
|
357
|
+
total_credits: section.total_credits,
|
|
358
|
+
display_total_credits: toDisplayAmount(section.total_credits)
|
|
333
359
|
}));
|
|
334
360
|
|
|
335
361
|
return {
|
|
@@ -339,6 +365,127 @@ function buildAccountingStatementView(reportType, rows, accountLookup) {
|
|
|
339
365
|
};
|
|
340
366
|
}
|
|
341
367
|
|
|
368
|
+
function normalizeChartAccountCategory(account) {
|
|
369
|
+
if (!account) return '';
|
|
370
|
+
const category = account.account_category || '';
|
|
371
|
+
if (category === 'Liabilities') return 'Liabilities & Equity';
|
|
372
|
+
return category;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function buildChartOfAccountsView(accounts) {
|
|
376
|
+
const baseAccounts = [...accounts]
|
|
377
|
+
.map(account => ({
|
|
378
|
+
...account,
|
|
379
|
+
account_category: normalizeChartAccountCategory(account)
|
|
380
|
+
}))
|
|
381
|
+
.sort((a, b) => {
|
|
382
|
+
const categoryDiff = normalizeChartAccountCategory(a).localeCompare(normalizeChartAccountCategory(b));
|
|
383
|
+
return categoryDiff !== 0 ? categoryDiff : String(a.name || '').localeCompare(String(b.name || ''));
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
const grouped = new Map();
|
|
387
|
+
baseAccounts.forEach(account => {
|
|
388
|
+
const category = normalizeChartAccountCategory(account) || 'Uncategorized';
|
|
389
|
+
if (!grouped.has(category)) grouped.set(category, []);
|
|
390
|
+
grouped.get(category).push(account);
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
const syntheticGroups = ['Assets', 'Liabilities & Equity', 'Equity', 'Income', 'Expenses'];
|
|
394
|
+
syntheticGroups.forEach(category => {
|
|
395
|
+
if (!grouped.has(category)) grouped.set(category, []);
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
const sections = Array.from(grouped.entries())
|
|
399
|
+
.sort((a, b) => a[0].localeCompare(b[0]))
|
|
400
|
+
.map(([category, entries]) => ({
|
|
401
|
+
id: category,
|
|
402
|
+
name: category,
|
|
403
|
+
description: category,
|
|
404
|
+
account_category: category,
|
|
405
|
+
is_parent: true,
|
|
406
|
+
parent_path: null,
|
|
407
|
+
children: entries.map(entry => ({
|
|
408
|
+
...entry,
|
|
409
|
+
parent_path: entry.parent_path || category
|
|
410
|
+
}))
|
|
411
|
+
}));
|
|
412
|
+
|
|
413
|
+
const liabEqSection = sections.find(section => section.name === 'Liabilities & Equity');
|
|
414
|
+
if (liabEqSection) {
|
|
415
|
+
const hasEquity = liabEqSection.children.some(child => child.name === 'Equity');
|
|
416
|
+
if (!hasEquity) {
|
|
417
|
+
liabEqSection.children.push({
|
|
418
|
+
id: 'Equity',
|
|
419
|
+
name: 'Equity',
|
|
420
|
+
description: 'Equity',
|
|
421
|
+
account_category: 'Liabilities & Equity',
|
|
422
|
+
balance_normality: 'credit',
|
|
423
|
+
is_parent: true,
|
|
424
|
+
parent_path: 'Liabilities & Equity',
|
|
425
|
+
custom_data: { default_account: false },
|
|
426
|
+
children: []
|
|
427
|
+
});
|
|
428
|
+
liabEqSection.children.push({
|
|
429
|
+
id: 'Equity:Retained Earnings',
|
|
430
|
+
name: 'Retained Earnings',
|
|
431
|
+
description: 'Retained Earnings',
|
|
432
|
+
account_category: 'Liabilities & Equity',
|
|
433
|
+
balance_normality: 'credit',
|
|
434
|
+
is_parent: false,
|
|
435
|
+
parent_path: 'Equity',
|
|
436
|
+
custom_data: { default_account: false }
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
return sections;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function enrichJobsResult(result) {
|
|
445
|
+
if (!result || !Array.isArray(result.results)) return result;
|
|
446
|
+
const counts = result.results.reduce((acc, job) => {
|
|
447
|
+
const status = job.status || 'unknown';
|
|
448
|
+
acc[status] = (acc[status] || 0) + 1;
|
|
449
|
+
return acc;
|
|
450
|
+
}, {});
|
|
451
|
+
|
|
452
|
+
return {
|
|
453
|
+
...result,
|
|
454
|
+
summary: {
|
|
455
|
+
total_count: result.total_count ?? result.results.length,
|
|
456
|
+
returned_count: result.results.length,
|
|
457
|
+
page_status_counts: counts,
|
|
458
|
+
has_more: !!result.next,
|
|
459
|
+
note: result.next
|
|
460
|
+
? `Showing ${result.results.length} jobs from the current page. Use the cursor to continue through the remaining jobs.`
|
|
461
|
+
: `Showing ${result.results.length} jobs from the current page.`
|
|
462
|
+
}
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function enrichListAccountsResult(result) {
|
|
467
|
+
if (!result || !Array.isArray(result.results)) return result;
|
|
468
|
+
const sortedResults = [...result.results].sort((a, b) => {
|
|
469
|
+
const categoryA = normalizeAccountCategory(a.account_category);
|
|
470
|
+
const categoryB = normalizeAccountCategory(b.account_category);
|
|
471
|
+
const categoryDiff = categoryA.localeCompare(categoryB);
|
|
472
|
+
return categoryDiff !== 0 ? categoryDiff : String(a.name || '').localeCompare(String(b.name || ''));
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
const grouped = sortedResults.reduce((acc, account) => {
|
|
476
|
+
const category = normalizeAccountCategory(account.account_category);
|
|
477
|
+
acc[category] = acc[category] || [];
|
|
478
|
+
acc[category].push(account);
|
|
479
|
+
return acc;
|
|
480
|
+
}, {});
|
|
481
|
+
|
|
482
|
+
return {
|
|
483
|
+
...result,
|
|
484
|
+
results: sortedResults,
|
|
485
|
+
grouped_view: grouped
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
|
|
342
489
|
async function fetchJson(url, headers) {
|
|
343
490
|
const response = await fetch(url, { method: 'GET', headers });
|
|
344
491
|
const text = await response.text();
|
|
@@ -353,6 +500,58 @@ async function fetchJson(url, headers) {
|
|
|
353
500
|
}
|
|
354
501
|
}
|
|
355
502
|
|
|
503
|
+
async function executeFrontendStyleRawMetricLogs(args, headers, baseUrl) {
|
|
504
|
+
const rawMetricId = args.rawMetricId;
|
|
505
|
+
const rawMetricUrl = `${baseUrl}/rawmetric/${encodeURIComponent(rawMetricId)}`;
|
|
506
|
+
const rawMetric = await fetchJson(rawMetricUrl, headers);
|
|
507
|
+
|
|
508
|
+
if (!rawMetric?.api_slug) {
|
|
509
|
+
throw new Error(`Unable to resolve api_slug for raw metric ${rawMetricId}`);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
const previewHeaders = {
|
|
513
|
+
...headers,
|
|
514
|
+
apiversion: '20240301'
|
|
515
|
+
};
|
|
516
|
+
|
|
517
|
+
const payload = {
|
|
518
|
+
limit: args.limit ?? 20,
|
|
519
|
+
offset: args.offset ?? 0,
|
|
520
|
+
order_by: args.order_by ?? [{ column: 'timestamp', type: 'DESC' }],
|
|
521
|
+
aggregate_operation: args.aggregate_operation ?? null,
|
|
522
|
+
customer_mapping: null,
|
|
523
|
+
end_date_mapping: null,
|
|
524
|
+
start_date_mapping: null,
|
|
525
|
+
table_name: `raw_metric_${rawMetric.api_slug}`,
|
|
526
|
+
visual_query: {
|
|
527
|
+
groups: [
|
|
528
|
+
{
|
|
529
|
+
filters: Array.isArray(args.filters) ? args.filters : [],
|
|
530
|
+
logic: 'AND'
|
|
531
|
+
}
|
|
532
|
+
],
|
|
533
|
+
logic: 'AND'
|
|
534
|
+
}
|
|
535
|
+
};
|
|
536
|
+
|
|
537
|
+
const response = await fetch(`${baseUrl}/aggregate/visualquery/preview`, {
|
|
538
|
+
method: 'POST',
|
|
539
|
+
headers: previewHeaders,
|
|
540
|
+
body: JSON.stringify(payload)
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
const responseText = await response.text();
|
|
544
|
+
if (!response.ok) {
|
|
545
|
+
throw new Error(`API request failed: ${response.status} ${response.statusText}\nResponse: ${responseText}`);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
try {
|
|
549
|
+
return JSON.parse(responseText);
|
|
550
|
+
} catch (_error) {
|
|
551
|
+
return responseText;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
356
555
|
async function enrichAccountingReportResult(toolName, result, headers, baseUrl) {
|
|
357
556
|
if (!result || !Array.isArray(result.results)) {
|
|
358
557
|
return result;
|
|
@@ -374,16 +573,92 @@ async function enrichAccountingReportResult(toolName, result, headers, baseUrl)
|
|
|
374
573
|
...row,
|
|
375
574
|
account_name: account.name || row.account_id || null,
|
|
376
575
|
account_description: account.description || null,
|
|
377
|
-
account_category:
|
|
576
|
+
account_category: normalizeReportAccountCategory(toolName, account.account_category),
|
|
378
577
|
account_parent_path: account.parent_path || null,
|
|
379
|
-
balance_normality: account.balance_normality || null
|
|
578
|
+
balance_normality: account.balance_normality || null,
|
|
579
|
+
display_balance: toDisplayAmount(row.balance ?? 0),
|
|
580
|
+
display_debits: toDisplayAmount(row.debits ?? 0),
|
|
581
|
+
display_credits: toDisplayAmount(row.credits ?? 0)
|
|
380
582
|
};
|
|
381
583
|
});
|
|
382
584
|
|
|
585
|
+
let statementView = buildAccountingStatementView(toolName, enrichedRows, accountLookup);
|
|
586
|
+
|
|
587
|
+
if (toolName === 'getBalanceSheet') {
|
|
588
|
+
const params = new URLSearchParams();
|
|
589
|
+
if (headers.apiversion) params.set('apiversion', headers.apiversion);
|
|
590
|
+
const incomeStatementUrl = `${baseUrl}/accounting_new/income_statement/v2`;
|
|
591
|
+
const incomeStatement = await fetchJson(incomeStatementUrl, headers);
|
|
592
|
+
const incomeRows = Array.isArray(incomeStatement?.results) ? incomeStatement.results : [];
|
|
593
|
+
const retainedByPeriod = {};
|
|
594
|
+
|
|
595
|
+
incomeRows.forEach(row => {
|
|
596
|
+
const periodKey = buildPeriodKey(row);
|
|
597
|
+
const period = retainedByPeriod[periodKey] || {
|
|
598
|
+
interval_start: row.interval_start || null,
|
|
599
|
+
interval_end: row.interval_end || null,
|
|
600
|
+
balance: 0,
|
|
601
|
+
debits: 0,
|
|
602
|
+
credits: 0
|
|
603
|
+
};
|
|
604
|
+
period.balance += Number(row.balance || 0);
|
|
605
|
+
period.debits += Number(row.debits || 0);
|
|
606
|
+
period.credits += Number(row.credits || 0);
|
|
607
|
+
retainedByPeriod[periodKey] = period;
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
const retainedPeriods = summarizePeriods(
|
|
611
|
+
Object.fromEntries(
|
|
612
|
+
Object.entries(retainedByPeriod).map(([key, value]) => [
|
|
613
|
+
key,
|
|
614
|
+
{
|
|
615
|
+
...value,
|
|
616
|
+
display_balance: toDisplayAmount(value.balance),
|
|
617
|
+
display_debits: toDisplayAmount(value.debits),
|
|
618
|
+
display_credits: toDisplayAmount(value.credits)
|
|
619
|
+
}
|
|
620
|
+
])
|
|
621
|
+
)
|
|
622
|
+
);
|
|
623
|
+
|
|
624
|
+
const retainedAccount = {
|
|
625
|
+
account_id: 'Equity:Retained Earnings',
|
|
626
|
+
account_name: 'Retained Earnings (Derived)',
|
|
627
|
+
account_description: 'Derived from the companion income statement for MCP presentation.',
|
|
628
|
+
account_category: 'Liabilities & Equity',
|
|
629
|
+
parent_path: 'Equity',
|
|
630
|
+
balance_normality: 'credit',
|
|
631
|
+
periods: retainedPeriods,
|
|
632
|
+
total_balance: retainedPeriods.reduce((sum, period) => sum + Number(period.balance || 0), 0),
|
|
633
|
+
display_total_balance: toDisplayAmount(retainedPeriods.reduce((sum, period) => sum + Number(period.balance || 0), 0)),
|
|
634
|
+
total_debits: retainedPeriods.reduce((sum, period) => sum + Number(period.debits || 0), 0),
|
|
635
|
+
display_total_debits: toDisplayAmount(retainedPeriods.reduce((sum, period) => sum + Number(period.debits || 0), 0)),
|
|
636
|
+
total_credits: retainedPeriods.reduce((sum, period) => sum + Number(period.credits || 0), 0),
|
|
637
|
+
display_total_credits: toDisplayAmount(retainedPeriods.reduce((sum, period) => sum + Number(period.credits || 0), 0))
|
|
638
|
+
};
|
|
639
|
+
|
|
640
|
+
let liabEqSection = statementView.sections.find(section => section.category === 'Liabilities & Equity');
|
|
641
|
+
if (!liabEqSection) {
|
|
642
|
+
liabEqSection = {
|
|
643
|
+
category: 'Liabilities & Equity',
|
|
644
|
+
accounts: [],
|
|
645
|
+
totals_by_period: [],
|
|
646
|
+
total_balance: 0,
|
|
647
|
+
display_total_balance: 0,
|
|
648
|
+
total_debits: 0,
|
|
649
|
+
display_total_debits: 0,
|
|
650
|
+
total_credits: 0,
|
|
651
|
+
display_total_credits: 0
|
|
652
|
+
};
|
|
653
|
+
statementView.sections.push(liabEqSection);
|
|
654
|
+
}
|
|
655
|
+
liabEqSection.accounts.push(retainedAccount);
|
|
656
|
+
}
|
|
657
|
+
|
|
383
658
|
return {
|
|
384
659
|
...result,
|
|
385
660
|
results: enrichedRows,
|
|
386
|
-
statement_view:
|
|
661
|
+
statement_view: statementView
|
|
387
662
|
};
|
|
388
663
|
} catch (error) {
|
|
389
664
|
logger.warn(`[${toolName}] Failed to enrich accounting report output; returning raw report`, {
|
|
@@ -770,6 +1045,13 @@ async function executeAPICall(tool, args) {
|
|
|
770
1045
|
});
|
|
771
1046
|
|
|
772
1047
|
try {
|
|
1048
|
+
if (tool.name === 'getRawMetricLogs') {
|
|
1049
|
+
const baseUrl = process.env.ZENSKAR_API_BASE_URL || 'https://api.zenskar.com';
|
|
1050
|
+
const result = await executeFrontendStyleRawMetricLogs(cleanArgs, headers, baseUrl);
|
|
1051
|
+
logger.debug(`[${tool.name}] Successfully processed frontend-style usage-event logs`);
|
|
1052
|
+
return result;
|
|
1053
|
+
}
|
|
1054
|
+
|
|
773
1055
|
const response = await fetch(fullUrl, {
|
|
774
1056
|
method,
|
|
775
1057
|
headers,
|
|
@@ -801,6 +1083,18 @@ async function executeAPICall(tool, args) {
|
|
|
801
1083
|
const baseUrl = process.env.ZENSKAR_API_BASE_URL || 'https://api.zenskar.com';
|
|
802
1084
|
result = await enrichAccountingReportResult(tool.name, result, headers, baseUrl);
|
|
803
1085
|
}
|
|
1086
|
+
if (tool.name === 'listJobs') {
|
|
1087
|
+
result = enrichJobsResult(result);
|
|
1088
|
+
}
|
|
1089
|
+
if (tool.name === 'listAccounts') {
|
|
1090
|
+
result = enrichListAccountsResult(result);
|
|
1091
|
+
}
|
|
1092
|
+
if (tool.name === 'getChartOfAccounts' && Array.isArray(result)) {
|
|
1093
|
+
result = {
|
|
1094
|
+
raw_accounts: result,
|
|
1095
|
+
chart_view: buildChartOfAccountsView(result)
|
|
1096
|
+
};
|
|
1097
|
+
}
|
|
804
1098
|
|
|
805
1099
|
// Apply response template if available
|
|
806
1100
|
if (tool.responseTemplate?.prependBody) {
|