node-autotask 0.11.0 → 0.12.1

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 (41) hide show
  1. package/MCP_TOOL_CATALOG.json +26 -8
  2. package/MCP_TOOL_MANIFEST.md +12 -11
  3. package/N8N_MCP_MANIFEST.json +3 -3
  4. package/N8N_MCP_MANIFEST.md +4 -4
  5. package/README.md +1 -1
  6. package/capabilities.json +1352 -1233
  7. package/dist/capabilities.cjs +2 -2
  8. package/dist/capabilities.js +1 -1
  9. package/dist/{chunk-QAX222R6.cjs → chunk-36PG2IU2.cjs} +1647 -394
  10. package/dist/{chunk-T5FLSJOD.cjs → chunk-CSRCMBZ2.cjs} +1351 -1232
  11. package/dist/{chunk-YKEX4SK7.cjs → chunk-IHRXVIUU.cjs} +519 -39
  12. package/dist/{chunk-7L23FDQW.js → chunk-NACI2NDF.js} +1351 -1232
  13. package/dist/{chunk-CJONZQ7E.js → chunk-P35GFG2H.js} +1259 -6
  14. package/dist/{chunk-25ZUKHS4.cjs → chunk-PGQIGE63.cjs} +135 -135
  15. package/dist/{chunk-SXHPUQMB.js → chunk-PPTAMCFA.js} +2 -2
  16. package/dist/{chunk-LNRN45XS.js → chunk-ZOA5HUFX.js} +498 -18
  17. package/dist/{index-B_xToSpq.d.ts → index-DZfd9Gt7.d.cts} +251 -2
  18. package/dist/{index-Dv_GSZaN.d.cts → index-UilX3PFL.d.ts} +251 -2
  19. package/dist/index.cjs +20 -20
  20. package/dist/index.d.cts +4 -4
  21. package/dist/index.d.ts +4 -4
  22. package/dist/index.js +5 -5
  23. package/dist/operations/index.cjs +123 -588
  24. package/dist/operations/index.d.cts +4 -4
  25. package/dist/operations/index.d.ts +4 -4
  26. package/dist/operations/index.js +10 -475
  27. package/dist/pool.cjs +8 -8
  28. package/dist/pool.d.cts +2 -2
  29. package/dist/pool.d.ts +2 -2
  30. package/dist/pool.js +4 -4
  31. package/dist/resources/index.cjs +4 -4
  32. package/dist/resources/index.d.cts +2 -2
  33. package/dist/resources/index.d.ts +2 -2
  34. package/dist/resources/index.js +3 -3
  35. package/dist/types/index.d.cts +1 -1
  36. package/dist/types/index.d.ts +1 -1
  37. package/docs/API.md +301 -2
  38. package/docs/N8N-ADAPTER.md +1 -1
  39. package/package.json +1 -1
  40. package/dist/{index-cQ6CRexR.d.cts → index-BXtKN1M5.d.cts} +201 -201
  41. package/dist/{index-cQ6CRexR.d.ts → index-BXtKN1M5.d.ts} +201 -201
package/docs/API.md CHANGED
@@ -847,7 +847,7 @@ be cross-mode-refused.
847
847
  whose MCP annotation is `readOnlyHint !== true` — today exactly **254
848
848
  tools**: the 252 dedicated write/delete tools (198 `effect=write` + 54
849
849
  `effect=destructive`, every one of them has a dedicated tool) +
850
- `autotask_write` + `autotask_delete`. The remaining **600 tools** (the 587
850
+ `autotask_write` + `autotask_delete`. The remaining **601 tools** (the 588
851
851
  dedicated read tools + the 10 core read tools + `autotask_read` +
852
852
  `autotask_catalog` + `autotask_describe`) all carry `readOnlyHint: true`
853
853
  and are safe ungated: every op they can route is `effect=read` (zero
@@ -1145,7 +1145,7 @@ Standard methods (every resource, `BaseResource`): `query` (stream, 1000-page gu
1145
1145
  | `contract_service_units` | ContractServiceUnit | entity | extended | yes (contracts) | query, count, get, resolve | getEntityInfo, getFieldInfo, getUdfInfo | autotask_contractServiceUnit_get |
1146
1146
  | `contract_services` | ContractService | entity | extended | yes (contracts) | query, count, get, create, update, resolve | createIfNotExists, getEntityInfo, getFieldInfo, getUdfInfo | autotask_contractService_get |
1147
1147
  | `contract_ticket_purchases` | ContractTicketPurchases | entity | extended | yes (contracts) | query, count, get, create, update, resolve | getEntityInfo, getFieldInfo, getUdfInfo | autotask_contractTicketPurchase_get |
1148
- | `contracts` | Contract | entity | extended | no | query, count, get, create, update, resolve, search | createIfNotExists, getEntityInfo, getFieldInfo, getUdfInfo | autotask_contract_get |
1148
+ | `contracts` | Contract | entity | extended | no | query, count, get, create, update, resolve, search | createIfNotExists, getEntityInfo, getFieldInfo, getUdfInfo, profitability | autotask_contract_get |
1149
1149
  | `countries` | Country | entity | advanced | no | query, count, get, update, resolve, search | getEntityInfo, getFieldInfo, getUdfInfo | autotask_country_get |
1150
1150
  | `departments` | Department | entity | extended | no | query, count, get, create, update, resolve, search | getEntityInfo, getFieldInfo, getUdfInfo | autotask_department_get |
1151
1151
  | `domain_registrars` | DomainRegistrar | entity | extended | no | query, count, get, create, update, resolve, search | getEntityInfo, getFieldInfo, getUdfInfo | autotask_DomainRegistrar_get |
@@ -2601,6 +2601,305 @@ resource's query surface is pinned blind), and `next` (concrete next steps; the
2601
2601
  remedy names the authority op). Hit paths carry no `miss` key. Thrown outcomes
2602
2602
  (`NOT_FOUND` by numeric id, `RESOLUTION_AMBIGUOUS`, `RESOLUTION_TRUNCATED`) are unchanged.
2603
2603
 
2604
+ ### 11.26 The contract P&L composite — `contracts.profitability` (issue #100)
2605
+
2606
+ Issue #100 adds the contract profit-and-loss composite op (an additive read;
2607
+ the plan change — a new registry op — is a minor at publish). One call
2608
+ returns the contract's reconciled P&L over a date window:
2609
+
2610
+ - **`contracts.profitability(contractId, opts)` — the closed opts bag.**
2611
+ `dateFrom`/`dateTo` are required (`YYYY-MM-DD`, `dateTo >= dateFrom`) and
2612
+ the window is capped at 36 calendar months (the per-month windowing budget
2613
+ stays bounded; OQ1). `labourCostMethod` (default
2614
+ `billing_item_our_cost`; the closed 4-set
2615
+ `billing_item_our_cost` / `contract_role_cost` / `resource_internal_cost` /
2616
+ `provided`) selects the START rung of the cost-rate ladder;
2617
+ `labourRatePerHour` (finite `>= 0`) is the `provided` catch-all rung and is
2618
+ REQUIRED when `labourCostMethod` is `provided`. `includeRecurringCosts` /
2619
+ `includeUnbilledRevenue` (default true) gate the charges/units folds and
2620
+ the expected-revenue basis; `externalMonthlyCosts` (finite `>= 0`, indexed
2621
+ by month from `dateFrom`, at most one entry per window month) is the
2622
+ caller-supplied non-vendor tooling cost, surfaced in
2623
+ `tooling_cost.caller_supplied` (stamped non-vendor — never folded into the
2624
+ vendor figure); `groupByActivity` (default true) gates the
2625
+ `ticketCategory` activity fold; `staleAfterDays` (default 30, integer `>=
2626
+ 1`) is the open-stale horizon; `maxTicketsForJoin` (default 500, integer
2627
+ 1..500) caps the ticket join set fed to the expenses + miscoding probe
2628
+ (sorted, first N, the `N of M` note) and the time-referenced ticket ids;
2629
+ `limit` (default 50, clamped 500) is
2630
+ every part's `MaxRecords` + the capped lists. Validation collects the
2631
+ COMPLETE problem list and throws ONE typed `CONFIG_ERROR` pre-request —
2632
+ zero wire on any malformed bag (the refusal table is house style: every
2633
+ message names the key + the valid values).
2634
+ - **Wire plan: ≤21 bounded reads, zero mutating, all pre-existing ops.**
2635
+ Issue order: contract `get` → customer `GET /V1.0/Companies/{companyID}` →
2636
+ the Contracts `entityInformation/fields` read (the `contractType` LABEL —
2637
+ verified on the ww29 sandbox 2026-10-16: 1 Time & Materials, 3 Fixed
2638
+ Price, 4 Block Hours, 6 Retainer, 7 Recurring Service, 8 Per Ticket, 9
2639
+ Umbrella; the folds still branch on `/block\s*hours?/i` and
2640
+ `/time.*materials?/i` over the label, never an id; `contractCategory` is
2641
+ ignored — all its values are inactive; an absent/0 type skips the read; an absent type, an
2642
+ unlisted id or a catalog fault is `CONTRACT_TYPE_UNKNOWN`, surfaced in
2643
+ `contract_type { id, label, absent? }`) → `ContractServices` / `ContractServiceUnits` (windowed on the period
2644
+ `startDate`) / `ContractCharges` / `ContractRoleCosts` / `ContractRates`
2645
+ queries → `ContractBlocks` query (Block Hours contracts only, by label;
2646
+ skipped otherwise) → `BillingItems` query → invoices
2647
+ (fetch-by-ids on the distinct billed `invoiceID`s; skipped on none) →
2648
+ `TimeEntries` query → `Tickets` activity query (`or[ and[contractID,
2649
+ createDate in window], id in <the ticket ids the in-window time
2650
+ references> ]` — an older or closed ticket carrying this period's time is
2651
+ fetched, never reported as unresolvable) → the tickets
2652
+ `entityInformation/fields` metadata read (the open-status picklist) → open
2653
+ `Tickets` query (`status in` the open set) → resources (fetch-by-ids on the
2654
+ distinct time-row `resourceID`s; skipped on none) → `ExpenseItems` query +
2655
+ the ONE extra bounded miscoding probe (`TimeEntries` with `ticketID in
2656
+ join` + `or[contractID noteq, contractID notExist]`; the join is the
2657
+ fetched tickets whose record says THIS contract; skipped on an empty set)
2658
+ → `ConfigurationItems` query (`contractID` = the CIs this contract covers,
2659
+ never the whole company; default-on) → `Subscriptions` query (CI join;
2660
+ skipped on no CIs) → the Subscriptions `entityInformation/fields` read
2661
+ (the `status` + `periodType` labels; only when subscriptions came back).
2662
+ Every window filter is `gte dateFrom` + `lt` the day
2663
+ after `dateTo` (a datetime field keeps the last day's later rows). All
2664
+ windows and month buckets are UTC days: date-only fields (`itemDate`,
2665
+ units `startDate`/`endDate`) come back as UTC midnight (`Z`), datetimes
2666
+ (ticket `createDate`) as UTC instants, and bucketing slices the ISO
2667
+ string — in a UTC+10/11 tenant a ticket created late local time buckets
2668
+ one UTC day (possibly one month) earlier than its local date. The
2669
+ `time_entries.getPosted` / `getUnposted` endpoints are NEVER called (the
2670
+ pinned vendor 500 on AU03); posted/unposted derives from the
2671
+ `billingItems.timeEntryID` join (unposted = no billing item exists yet).
2672
+ The structural exclusions (`/Services/`, `/ExpenseReports/`,
2673
+ `/BillingCodes/` list reads, getPosted/getUnposted) are pinned by the
2674
+ recorded-request test. Baseline = 12 (a resolvable contract); the
2675
+ typical busy contract = 18; the full fixture = 20 (21 on a Block Hours
2676
+ contract). `wires: { issued, requests, budget: 21, note? }` rides the
2677
+ result as the honesty stamp: `issued` counts logical part reads (a
2678
+ fetch-by-ids part counts once), `requests` the raw HTTP requests (its
2679
+ chunk queries + per-id gets counted individually; a read-cache-served
2680
+ contract `get` adds no request).
2681
+ - **Per-month windowing (parts `billingItems` + `timeEntries` +
2682
+ `contractServiceUnits`).** One
2683
+ full-window page (`MaxRecords: limit`); when it comes back FULL with a
2684
+ live next cursor, the part re-queries PER CALENDAR MONTH intersecting the
2685
+ window (one page per month, the date field bounded to that month, in
2686
+ month order; rows deduped by `id`). A still-full month stamps
2687
+ `hasMore`/`capped` + the truncation note (`month YYYY-MM hit the limit of
2688
+ N — figures cover the returned rows only; widen with a narrower window or
2689
+ higher limit`) — NO cursor walk (the house single-page rule; the composite
2690
+ owns its own windowing so per-month figures stay correct by construction).
2691
+ Every expansion read counts toward `wires.issued` (the note says why);
2692
+ the 36-month cap bounds the worst case. A still-full month is also named
2693
+ in that month's `coverage_gaps` (`<part>: truncated at limit N`).
2694
+ - **The cost-rate ladder (per time entry, zero extra wire).** Rung 1
2695
+ `billing_item.ourCost`: a LINE cost, not a rate — Σ the finite `>= 0`
2696
+ `ourCost` over the entry's billed lines (partial billing across months
2697
+ sums naturally; an entry whose billed lines carry NO finite `ourCost`
2698
+ falls THROUGH — the rung is unusable, not zero). Rung 2
2699
+ `contract_role_costs.rate`: the `resourceID:roleID` row is preferred over
2700
+ the role-only row (`matched: 'resource+role' | 'role'`); `rate × hours`.
2701
+ Rung 3 `resource.internalCost`: the fetch-by-ids GET-fallback rows only
2702
+ (a `NOT_FOUND` miss makes the rung unavailable for that resource — the
2703
+ ww29 resources list-scoping is seen through by get-by-id, live-verified
2704
+ 2026-10-15; the quirk is cited in the part note when the chunk queries
2705
+ 0-rowed and the gets resolved). Rung 4 `provided`: `labourRatePerHour ×
2706
+ hours` (in the ladder only when the rate is finite). `hours(entry)` =
2707
+ `hoursWorked` when finite and `> 0`, else 0. A row resolving to
2708
+ `missing` contributes 0 to `labour_cost.total` but is audited:
2709
+ `costRateResolution.sources` (per-rung `entries`/`hours`/`cost`, ALWAYS
2710
+ including `missing`), `unresolvedHours` (= the `missing` hours — the
2711
+ lower-bound honesty number; `total_cost` is a LOWER BOUND exactly when it
2712
+ is `> 0`), and the pinned note lines (the `N hours unresolved (missing
2713
+ rate) — total_cost is a lower bound` line; the per-resource NOT_FOUND
2714
+ line; the no-billing-item line). NEVER a silent zero. `contract_rates`
2715
+ is NOT a ladder rung (the customer billing rate — the wrong side of the
2716
+ P&L).
2717
+ - **Monthly P&L (one row per calendar month in the window).**
2718
+ `revenue_invoiced` (Σ the line revenue of the month's items on
2719
+ non-voided invoices; line revenue = `internalCurrencyExtendedPrice`,
2720
+ falling back to `extendedPrice`) · `revenue_by_type { recurring, labour,
2721
+ charges, expenses, other }` (the invoiced split, keyed on the verified
2722
+ `billingItemType` picklist: 1 Labor / 2 Labor Adjustment → labour, 3 Cost
2723
+ → charges, 4 Expense → expenses, 5 Subscription → other, 6 Recurring
2724
+ Service/Bundle → recurring, 7 Recurring Contract Setup Fee → charges, 8
2725
+ Milestone → other; an absent/unknown type falls back to the line's FKs in
2726
+ the order `timeEntryID` → labour, `expenseItemID` → expenses, a charge FK
2727
+ → charges, a contract service/bundle/period/adjustment FK → recurring —
2728
+ labour billed against a service carries `contractServiceID` too, verified)
2729
+ ·
2730
+ `revenue_voided_excluded` (Σ on voided invoices) ·
2731
+ `revenue_posted_uninvoiced` (Σ on posted items with no invoice yet) ·
2732
+ `revenue_unresolved_invoice` (Σ on items whose invoice could not be read
2733
+ or was beyond the join cap — not recognized revenue, never silent: the
2734
+ month's `coverage_gaps` names `invoices: N line(s) on unresolved
2735
+ invoices`) · `revenue_expected` (Σ the period `internalCurrencyPrice`
2736
+ (fallback `price`) of the service units whose
2737
+ period STARTS in the month — a multi-month period is counted once, when
2738
+ it bills; `null` when the services/units basis is absent or the units
2739
+ part is `absent` — unknown, not 0. Unit `price`/`cost` are PERIOD totals
2740
+ (units × per-unit, prorated for a partial period — verified: the type-6
2741
+ recurring billing item has `itemDate` = the unit `startDate` and
2742
+ `extendedPrice` = the unit `price`). Units are normally one month but can
2743
+ span a year; the start-month attribution matches billing, so the monthly
2744
+ figures are a billing-period (cash) view, NOT accrual — an annual unit
2745
+ lands in its start month in one lump) ·
2746
+ `revenue_expected_unbilled = max(0, expected − the month's invoiced
2747
+ SERVICE lines)` (labour and charges never mask a missed recurring
2748
+ invoice) · `unposted_time { entries, hours, value, absent? }` (billable
2749
+ time with no billing item yet — derived from the billing join, never
2750
+ getPosted/getUnposted; `value` = Σ `hoursToBill ×
2751
+ internalCurrencyContractHourlyRate` (fallback `contractHourlyRate`) on
2752
+ Time & Materials contracts only, else `null` + `absent:
2753
+ 'NOT_TIME_AND_MATERIALS_CONTRACT'`, `'CONTRACT_TYPE_UNKNOWN'`, or
2754
+ `'NO_CONTRACT_RATE'` when a role has no contract rate) · `labour_hours`
2755
+ (Σ `hoursWorked` over positive entries — the same rule as the cost) ·
2756
+ `labour_cost { total, unresolvedHours, rateSources }` (the per-month
2757
+ mirror of the ladder audit) · `tooling_cost { vendor, caller_supplied }`
2758
+ (vendor = Σ `periodCost × the periodType monthly share` — by the tenant
2759
+ `periodType` LABEL (monthly 1, quarterly 1/3, semi-annual 1/6,
2760
+ annual/yearly 1/12); a One-Time subscription is never prorated: its
2761
+ `periodCost` is booked whole in its `effectiveDate` month; when the
2762
+ Subscriptions catalog read fails, the verified id maps apply (periodType
2763
+ 1 One-Time, 2 Monthly, 3 Quarterly, 4 Semi-Annual, 5 Yearly; status 1
2764
+ Active, 2 Cancelled, 3 Inactive) and the note says so; an unrecognised
2765
+ periodType is excluded and named; only Active subscriptions count —
2766
+ Cancelled and Inactive are excluded — over the subscriptions on this
2767
+ contract's CIs active in the month, REPORTED ONLY:
2768
+ never folded into `total_cost`, since subscriptions bill outside the
2769
+ contract; `null` when the subscriptions part is skipped/absent;
2770
+ caller-supplied = the bag value, `0` when absent) · `recurring_cost`
2771
+ (charge COST — `extendedCost`, falling back to `unitCost × unitQuantity`
2772
+ when it is absent or 0 while a unit cost exists; never `billableAmount` —
2773
+ plus the `cost` of the units starting in the month, when included) ·
2774
+ `expense_cost` (Σ `internalCurrencyExpenseAmount`) · `total_cost` (labour
2775
+ + caller-supplied tooling + recurring + expenses) · `profit =
2776
+ revenue_invoiced − total_cost` · `margin = profit / revenue_invoiced`
2777
+ when `> 0`, else `null` (the division is refused, not zeroed) ·
2778
+ `break_even_hours` (the labour hours the invoiced revenue absorbs at the
2779
+ average resolved cost rate before profit hits zero: `(revenue_invoiced −
2780
+ non-labour cost) / (Σcost / Σhours over resolved rows)`; `0` when the
2781
+ non-labour cost already exceeds revenue; `null` with no resolved hours or
2782
+ a zero rate — including an empty month) · `coverage_gaps` (why the
2783
+ month's figures may be incomplete: `<part>: absent (<code>)` /
2784
+ `truncated at limit N` / `capped at limit N` over units, charges, role
2785
+ costs, billing, time, expenses and subscriptions, plus the unresolved
2786
+ invoice lines; `[]` = complete). The P&L is single-currency in the
2787
+ INTERNAL currency (revenue and costs alike); money fields are emitted
2788
+ rounded to cents.
2789
+ - **The 11 data-quality checks (always present, order-stable, `count: 0` =
2790
+ a passed check).** Typed `{ code, count, sample_ids }` warnings
2791
+ (`count` = the full in-memory/fetched-set count, never capped;
2792
+ `sample_ids` = first `limit`, deduped + sorted): `miscoded_time_out` (the
2793
+ ONE extra bounded probe — time worked on THIS contract's tickets coded to
2794
+ another contract or to none) · `cross_contract_ticket_time` (a fetched
2795
+ ticket says it belongs elsewhere) · `duplicate_entries` (exact
2796
+ `resourceID+dateWorked+ticketID+hoursWorked` tuple; `count` = all copies,
2797
+ samples = copies beyond the lowest `id`) · `zero_negative_hours` ·
2798
+ `time_outside_ticket_period` (`dateWorked < ticket.createDate` or after
2799
+ the earlier of `resolvedDateTime`/`completedDate` — a fetched ticket's
2800
+ whole post-resolution life, not just the first late entry) ·
2801
+ `missing_cost_rate` (the ladder's `missing` rows; the resource NOT_FOUND
2802
+ misses ride the note) · `recurring_cost_drift` (month-over-month beyond
2803
+ the 0.005 float dust; samples the charge/unit ids whose monthly
2804
+ contribution changed) · `expected_unbilled_revenue` (expected > the
2805
+ invoiced service lines + 0.005; the pure "expected with nothing billed"
2806
+ case carries the EMPTY sample — that is the assertion) ·
2807
+ `cross_contract_tickets` (the
2808
+ ticket-side drift + the entry-side unresolvable set) ·
2809
+ `open_stale_tickets` (this contract's tickets; `now − lastActivityDate >
2810
+ staleAfterDays`, `now` =
2811
+ the run's wall clock stamped `period.asOf`; absent `lastActivityDate` is
2812
+ unknown, not zero) · `estimate_vs_actual` (both estimates prorated to the
2813
+ window — `estimate × overlapDays / termDays` over the contract term — and
2814
+ flagged at `> 10%` deviation; samples `[contractId]`). 10 of 11 are pure
2815
+ in-memory folds (zero extra wire).
2816
+ - **The response-side folds.** `activity_categories` (by `ticketCategory`:
2817
+ hours, cost = labour + that category's expenses, ticketCount, entryCount,
2818
+ sample ticket numbers) · `highest_cost_tickets` (cost desc, capped at
2819
+ `limit`; a missing-rate ticket prices 0 and is flagged via the audit —
2820
+ never silently dropped) · `possible_miscoding` (the probe rows + the
2821
+ confirmed cross-contract entries, closed reason set
2822
+ `no_contract_on_entry` / `ticket_of_other_contract` /
2823
+ `ticket_not_in_fetched_set`) · `open_renewal_items` (this contract's open
2824
+ tickets with the stale-days detail, subscription expirations with the
2825
+ prorated monthly cost, the contract end, one `included_hours_burn` row
2826
+ per purchased hour block) · `included_hours` (Block Hours contracts only:
2827
+ `{ value: { purchased, approved, remaining } }` from the `ContractBlocks`
2828
+ `hours` vs the vendor `hoursApproved`; any other contract type is `{
2829
+ value: null, absent: 'NOT_BLOCK_HOURS_CONTRACT' }`, an unresolvable type
2830
+ `'CONTRACT_TYPE_UNKNOWN'` — service units are a
2831
+ quantity, never read as hours, and retainers are dollar-denominated).
2832
+ The probe rows are EXCLUDED from the P&L folds (diagnostic — time not
2833
+ coded to this contract).
2834
+ - **Degradation (per part typed `ok` / `blind` / `skipped` / `absent`; the
2835
+ op NEVER fails the call on a degraded part).** Only the contract read is
2836
+ fatal (the root: `NOT_FOUND` — or any other fault on it — propagates).
2837
+ A vendor fault on any bounded list part degrades that part to `status:
2838
+ 'absent'` + `absent: <the typed error code>` + the note (never a true
2839
+ zero — no `emptyReason`), and every monthly row it feeds names it in
2840
+ `coverage_gaps`; a non-404 fault on the customer read degrades to
2841
+ `customer: null` + `parts.customer: 'absent'` + `customerAbsent: <code>`.
2842
+ Every tickets call is
2843
+ `contractID`-filtered, so the pinned `unfiltered-blind` tickets class
2844
+ never stamps a filtered empty (a filtered 0 is a TRUE zero with the
2845
+ honest note); the 0-row parts carry the pinned
2846
+ `tenant-empty-for-user` `emptyReason` stamp where attached. A dangling
2847
+ customer FK (`companies.get` 404) degrades to `customer: null` +
2848
+ `parts.customer: 'skipped'` — NOT an error (the CIs read still fires on
2849
+ the contract row's `companyID`). A field-catalog fault degrades the open
2850
+ pass to a typed skip (the fault read still counts in `wires.issued`).
2851
+ fetch-by-ids per-item faults (invoices/resources) are typed partial
2852
+ misses named in the part note — a miss never aborts the rest. An empty
2853
+ tenant (the ww29 shape) degrades EVERY part — 13 wires, all-zero monthly
2854
+ rows with the divisions refused (`margin: null`, `break_even_hours:
2855
+ null`, `tooling_cost.vendor: null`, `revenue_expected: null`), all 11
2856
+ warnings at `count: 0`, zero tool failure.
2857
+ - **0.12.1 corrections.** The 0.12.0 folds counted the charge
2858
+ `billableAmount` (sell price) as recurring cost, read service `units` as
2859
+ included hours, joined every company CI's subscriptions and added a
2860
+ yearly `periodCost` every month, compared the expected recurring revenue
2861
+ against all billed lines (labour included) and priced it at today's
2862
+ `unitPrice`, read service units unwindowed, reported time on older
2863
+ tickets as unresolvable, defined break-even without revenue, folded
2864
+ expenses in their own currency, and threw on any bounded-part fault. The
2865
+ bullets above are the corrected semantics.
2866
+ - **Registration surface.** The plan row (`capabilities.plan.json` →
2867
+ re-derived hash) carries `kind: 'helper'`, `helperBasis: 'composite'`,
2868
+ `effect: 'read'`, `dryRun: false`, the closed-bag `inputSchema` (the
2869
+ signature-audited arg map in `scripts/lib/args-derive.mjs`); the emitted
2870
+ method delegates to the hand-written special module
2871
+ (`src/resources/special/contracts.ts`). The MCP projection serves
2872
+ `autotask_contract_profitability` (catalog + manifest); the N8N
2873
+ manifests legitimately EXCLUDE the op — `contracts` is not an n8n
2874
+ curated entity (the curation rule is evidence-based: the daily support
2875
+ subset is ticket/contact/company/resource/time_entry only).
2876
+ - **Test pins.** `test/resources/contracts.profitability.test.ts`: the
2877
+ hand-computed 2-month synthetic fixture (every figure derived from the
2878
+ fixture rows; `wires.issued = 18` with the exact queue; every wire pinned
2879
+ in issue order; the structural exclusions + zero-mutating recorded paths;
2880
+ the ladder audit incl. the per-month `rateSources`; the 11 checks with
2881
+ exact counts/samples; the activity/highest-cost/miscoding/renewal folds;
2882
+ the per-month windowing (exact extra month queries, id-dedup, the
2883
+ still-full month note; `wires.issued` counts the expansion); the
2884
+ join-set/`limit` caps; the malformed-bag refusal table (one complete
2885
+ problem list, zero wire); the contract NOT_FOUND; the 0.12.1 pins: the
2886
+ charge cost/period-type/line-type classifiers, the Block Hours
2887
+ `contract_blocks` path, T&M unposted pricing + the time-referenced old
2888
+ ticket, the exclusive window ends, the month-level `coverage_gaps`).
2889
+ `test/resources/profitability-degradation.test.ts`: the empty-tenant
2890
+ 13-wire suite (every part degrades, zero tool failure), the dangling
2891
+ customer FK, the non-404 customer fault (`absent`), a bounded-part fault
2892
+ (`absent` + `coverage_gaps`, the call resolves), the resources blind-list get-fallback (rung 3 from the
2893
+ gets, the quirk note), the per-item invoice fault (retries off — the
2894
+ fault semantics are the pin, not the transport's 5xx budget), the
2895
+ fieldCatalog-fault open-pass skip, the status-helper sentinel
2896
+ portability (the `unfiltered-blind` predicate stamps the unfiltered call
2897
+ only), and the §6.4 rate-ladder branch table over synthetic entry sets
2898
+ (rung-1 line-cost sum, rung-1 fall-through, resource+role preference,
2899
+ role-only, rung-3 get-fallback, the resource miss, the provided
2900
+ catch-all/start, the provided-without-rate and fully-missing `missing`
2901
+ — never a silent zero — and `internalCost: 0` as a RESOLVED zero).
2902
+
2604
2903
  ## 12. Untrusted-content projection (issue #1)
2605
2904
 
2606
2905
  The agent/MCP projection marks EXTERNALLY-SOURCED free text as untrusted,
@@ -82,7 +82,7 @@ Two sources in, markdown table out: the node repo (the ops column is re-derived
82
82
  | `contractServiceUnits` | ContractServiceUnit | yes | get, getMany, count | — | count, get, getEntityInfo, getFieldInfo, getUdfInfo, query, resolve |
83
83
  | `contractServices` | ContractService | yes | create, update, get, getMany, count, createIfNotExists | createIfNotExists → `contract_services.createIfNotExists` | count, create, createIfNotExists, get, getEntityInfo, getFieldInfo, getUdfInfo, query, resolve, update |
84
84
  | `contractTicketPurchases` | ContractTicketPurchases | yes | create, update, get, getMany, count | — | count, create, get, getEntityInfo, getFieldInfo, getUdfInfo, query, resolve, update |
85
- | `contracts` | Contract | no | create, update, get, getMany, count, createIfNotExists | createIfNotExists → `contracts.createIfNotExists` | count, create, createIfNotExists, get, getEntityInfo, getFieldInfo, getUdfInfo, query, resolve, search, update |
85
+ | `contracts` | Contract | no | create, update, get, getMany, count, createIfNotExists | createIfNotExists → `contracts.createIfNotExists` | count, create, createIfNotExists, get, getEntityInfo, getFieldInfo, getUdfInfo, profitability, query, resolve, search, update |
86
86
  | `countries` | Country | no | get, getMany, update, count | — | count, get, getEntityInfo, getFieldInfo, getUdfInfo, query, resolve, search, update |
87
87
  | `departments` | Department | no | count, create, get, getMany, update | — | count, create, get, getEntityInfo, getFieldInfo, getUdfInfo, query, resolve, search, update |
88
88
  | `domainRegistrar` | DomainRegistrar | no | create, update, get, getMany, count | — | count, create, get, getEntityInfo, getFieldInfo, getUdfInfo, query, resolve, search, update |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-autotask",
3
- "version": "0.11.0",
3
+ "version": "0.12.1",
4
4
  "description": "Fully-typed TypeScript SDK for the Autotask (Datto) PSA REST API. Zero runtime dependencies. Built for building MCP servers and integrations.",
5
5
  "type": "module",
6
6
  "sideEffects": false,