@firedrill-tools/netsuite 0.1.2

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 (92) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +214 -0
  3. package/firedrill/agent.target.json +16 -0
  4. package/firedrill/baseline.scenario.json +6905 -0
  5. package/firedrill/concurrency-limited.scenario.json +11 -0
  6. package/firedrill/conformance.suite.json +21 -0
  7. package/firedrill/ns-analytics.drill.json +73 -0
  8. package/firedrill/ns-concurrency.drill.json +53 -0
  9. package/firedrill/ns-default-identity.drill.json +43 -0
  10. package/firedrill/ns-denied.drill.json +38 -0
  11. package/firedrill/ns-insufficient-permission.drill.json +53 -0
  12. package/firedrill/ns-invalid-login.drill.json +53 -0
  13. package/firedrill/ns-order-to-cash.drill.json +93 -0
  14. package/firedrill/ns-record-locked.drill.json +53 -0
  15. package/firedrill/ns-records.drill.json +68 -0
  16. package/firedrill/ns-role-ar-clerk.drill.json +58 -0
  17. package/firedrill/ns-role-sales-rep.drill.json +58 -0
  18. package/firedrill/ns-subsidiary-scope.drill.json +58 -0
  19. package/firedrill/ns-tight-limits.drill.json +58 -0
  20. package/firedrill/ns-transform-lost.drill.json +63 -0
  21. package/firedrill/ns-write-outage.drill.json +63 -0
  22. package/firedrill/record-locked.scenario.json +11 -0
  23. package/firedrill/tight-limits.scenario.json +17 -0
  24. package/firedrill/tools/netsuite/app/assets/ATTRIBUTION.md +37 -0
  25. package/firedrill/tools/netsuite/app/assets/fonts/LICENSE.txt +92 -0
  26. package/firedrill/tools/netsuite/app/assets/oracle-netsuite.svg +1 -0
  27. package/firedrill/tools/netsuite/app/assets/oracle.svg +1 -0
  28. package/firedrill/tools/netsuite/app/site/app.js +182 -0
  29. package/firedrill/tools/netsuite/app/site/assets/fonts/open-sans-latin-ext.woff2 +0 -0
  30. package/firedrill/tools/netsuite/app/site/assets/fonts/open-sans-latin.woff2 +0 -0
  31. package/firedrill/tools/netsuite/app/site/assets/oracle-netsuite.svg +1 -0
  32. package/firedrill/tools/netsuite/app/site/assets/oracle.svg +1 -0
  33. package/firedrill/tools/netsuite/app/site/icons.js +59 -0
  34. package/firedrill/tools/netsuite/app/site/index.html +68 -0
  35. package/firedrill/tools/netsuite/app/site/menus.js +111 -0
  36. package/firedrill/tools/netsuite/app/site/overlay.js +121 -0
  37. package/firedrill/tools/netsuite/app/site/store.js +95 -0
  38. package/firedrill/tools/netsuite/app/site/styles/base.css +131 -0
  39. package/firedrill/tools/netsuite/app/site/styles/chrome.css +183 -0
  40. package/firedrill/tools/netsuite/app/site/styles/pages.css +134 -0
  41. package/firedrill/tools/netsuite/app/site/styles/tables.css +92 -0
  42. package/firedrill/tools/netsuite/app/site/ui.js +119 -0
  43. package/firedrill/tools/netsuite/app/site/views/common.js +134 -0
  44. package/firedrill/tools/netsuite/app/site/views/customer-form.js +152 -0
  45. package/firedrill/tools/netsuite/app/site/views/customer.js +147 -0
  46. package/firedrill/tools/netsuite/app/site/views/customers.js +111 -0
  47. package/firedrill/tools/netsuite/app/site/views/home-chrome.js +75 -0
  48. package/firedrill/tools/netsuite/app/site/views/home-data.js +153 -0
  49. package/firedrill/tools/netsuite/app/site/views/home.js +162 -0
  50. package/firedrill/tools/netsuite/app/site/views/invoice-new.js +131 -0
  51. package/firedrill/tools/netsuite/app/site/views/invoice.js +232 -0
  52. package/firedrill/tools/netsuite/app/site/views/invoices.js +99 -0
  53. package/firedrill/tools/netsuite/app/site/views/items.js +95 -0
  54. package/firedrill/tools/netsuite/app/site/views/order-new.js +154 -0
  55. package/firedrill/tools/netsuite/app/site/views/order.js +214 -0
  56. package/firedrill/tools/netsuite/app/site/views/orders.js +93 -0
  57. package/firedrill/tools/netsuite/app/site/views/payments.js +65 -0
  58. package/firedrill/tools/netsuite/app/site/views/pickers.js +117 -0
  59. package/firedrill/tools/netsuite/app/site/views/subsidiaries.js +40 -0
  60. package/firedrill/tools/netsuite/app/site/views/suiteql.js +81 -0
  61. package/firedrill/tools/netsuite/behavior.mjs +13 -0
  62. package/firedrill/tools/netsuite/lib/body.mjs +185 -0
  63. package/firedrill/tools/netsuite/lib/collections.mjs +202 -0
  64. package/firedrill/tools/netsuite/lib/errors.mjs +91 -0
  65. package/firedrill/tools/netsuite/lib/handlers-meta.mjs +100 -0
  66. package/firedrill/tools/netsuite/lib/handlers-read.mjs +141 -0
  67. package/firedrill/tools/netsuite/lib/handlers-session.mjs +52 -0
  68. package/firedrill/tools/netsuite/lib/handlers-write.mjs +6 -0
  69. package/firedrill/tools/netsuite/lib/metadata.mjs +148 -0
  70. package/firedrill/tools/netsuite/lib/primitives.mjs +105 -0
  71. package/firedrill/tools/netsuite/lib/project-tran.mjs +153 -0
  72. package/firedrill/tools/netsuite/lib/project.mjs +178 -0
  73. package/firedrill/tools/netsuite/lib/q.mjs +280 -0
  74. package/firedrill/tools/netsuite/lib/session.mjs +188 -0
  75. package/firedrill/tools/netsuite/lib/suiteql-parse.mjs +382 -0
  76. package/firedrill/tools/netsuite/lib/suiteql-run.mjs +270 -0
  77. package/firedrill/tools/netsuite/lib/suiteql-select.mjs +118 -0
  78. package/firedrill/tools/netsuite/lib/suiteql-tables.mjs +162 -0
  79. package/firedrill/tools/netsuite/lib/tran.mjs +133 -0
  80. package/firedrill/tools/netsuite/lib/wire-decode.mjs +107 -0
  81. package/firedrill/tools/netsuite/lib/wire.mjs +228 -0
  82. package/firedrill/tools/netsuite/lib/write-customer.mjs +181 -0
  83. package/firedrill/tools/netsuite/lib/write-invoice.mjs +319 -0
  84. package/firedrill/tools/netsuite/lib/write-order.mjs +269 -0
  85. package/firedrill/tools/netsuite/netsuite.tool.json +4888 -0
  86. package/firedrill/transform-response-lost.scenario.json +11 -0
  87. package/firedrill/world.json +7711 -0
  88. package/firedrill/write-outage.scenario.json +11 -0
  89. package/firedrill.json +5 -0
  90. package/package.json +52 -0
  91. package/starter.json +6904 -0
  92. package/test/conformance.mjs +701 -0
package/LICENSE ADDED
@@ -0,0 +1,201 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "[]"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright 2026 Reload Tech Inc.
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+ See the License for the specific language governing permissions and
201
+ limitations under the License.
package/README.md ADDED
@@ -0,0 +1,214 @@
1
+ # @firedrill-tools/netsuite
2
+
3
+ A synthetic **NetSuite** Tool for [Firedrill](https://firedrill.run): one fictional two-subsidiary OneWorld account exposed through a
4
+ subset of **SuiteTalk REST Web Services** — the record service under `/services/rest/record/v1` and **SuiteQL** under
5
+ `/services/rest/query/v1/suiteql` — plus four operations shaped like Oracle's MCP Standard Tools. All state is local and
6
+ deterministic: Firedrill owns the ledger, virtual time and evidence. No Oracle service is contacted, no e-mail is delivered and no
7
+ money moves.
8
+
9
+ Version `0.1.0`, engine `>=0.1.0 <0.2.0`, licence Apache-2.0. The package ships a **browser app** as well as the backend: a
10
+ recreation of the NetSuite application shell (see *Browser app* below) that drives the same operations an agent calls.
11
+
12
+ ## Install
13
+
14
+ ```sh
15
+ firedrill tool add @firedrill-tools/netsuite --install
16
+ ```
17
+
18
+ `firedrill tool add` seeds the starter account from `starter.json` (virtual time 2026-09-16T17:00:00Z, 09:00 account time at a fixed
19
+ −08:00). Grant the operations your agent needs, for example:
20
+
21
+ ```json
22
+ { "packageId": "netsuite", "operationId": "customer.list" },
23
+ { "packageId": "netsuite", "operationId": "sales-order.transform" },
24
+ { "packageId": "netsuite", "operationId": "suiteql.query" }
25
+ ```
26
+
27
+ ## Connecting a client
28
+
29
+ **HTTP.** Use `FIREDRILL_HTTP_URL` as the base URL instead of `https://<accountId>.suitetalk.api.netsuite.com` and
30
+ `FIREDRILL_HTTP_TOKEN` as the OAuth 2 Bearer token; the token's *value* is not inspected, the acting identity comes from the actor's
31
+ attributes. Paths keep NetSuite's shape, for example `GET /services/rest/record/v1/customer?q=…&limit=…&offset=…`,
32
+ `PATCH /services/rest/record/v1/invoice/{id}`, `POST /services/rest/record/v1/salesOrder/{id}/!transform/invoice` and
33
+ `POST /services/rest/query/v1/suiteql` (which requires `Prefer: transient`). Request headers decoded to arguments:
34
+ `Prefer`, `X-NetSuite-Idempotency-Key` (must be an RFC 4122 UUID), `X-NetSuite-PropertyNameValidation`
35
+ (`ignore|warning|error`, default `warning`) and `If-Match`. Successful writes answer **204** with a relative `Location` header.
36
+
37
+ **Canonical / MCP.** `record.get`, `record.metadata`, `suiteql.run` and `subsidiary.list` carry the argument shapes of the MCP
38
+ Standard Tools `ns_getRecord`, `ns_getRecordTypeMetadata`, `ns_runCustomSuiteQL` and `ns_getSubsidiaries`. `record.get` and
39
+ `suiteql.run` have **no** HTTP route — call them through the canonical operation endpoint.
40
+
41
+ ## Identities
42
+
43
+ | actor attribute | meaning |
44
+ |---|---|
45
+ | `netsuiteAccountId` (optional) | The account the token belongs to. Present and ≠ `TSTDRV2184096` → `INVALID_LOGIN` (401) on every operation. |
46
+ | `netsuiteRoleId` (optional) | The role in use. Absent → role `3` (Administrator). An unseeded role id → `INVALID_LOGIN`. |
47
+ | `netsuiteEmployeeId` (optional) | The logged-in employee; drives `createdBy`/`lastModifiedBy`. Absent → the account default `101`. Unknown → `INVALID_LOGIN`. |
48
+ | `netsuiteSubsidiaryId` (optional) | Overrides the role's subsidiary restriction. `"0"` means all subsidiaries. |
49
+
50
+ Rules apply in order: **account** → **permission** (NetSuite's `none < view < create < edit < full` over `LIST_CUSTJOB`,
51
+ `TRAN_SALESORD`, `TRAN_CUSTINVC`, `TRAN_CUSTPYMT`, `LIST_ITEM`, `LIST_SUBSIDIARY`, `SETUP_RECORD_METADATA`, `REPO_ANALYTICS`) →
52
+ **subsidiary scope**. An out-of-scope record answers `NONEXISTENT_ID` (404) rather than 403, as NetSuite hides it. There is no
53
+ per-row owner. **Fresh-install fallback:** an actor without attributes acts as employee 101 in the Administrator role.
54
+
55
+ Seeded roles: `3` Administrator, `1001` A/R Clerk (sales orders read-only, no delete), `1002` Sales Rep (no invoices, payments or
56
+ SuiteQL), `1003` Canada Controller (A/R Clerk permissions restricted to subsidiary 2), `1004` Restricted Viewer (no permissions).
57
+
58
+ ## Starting data
59
+
60
+ **Whitlock Instruments Inc**, a fictional Seattle oceanographic-instrument maker with a Canadian subsidiary, 69 state rows:
61
+ 2 subsidiaries, 3 employees, 5 roles, 12 customers (ids 1001–1012), 10 items (ids 2001–2010), 9 sales orders, 11 invoices and
62
+ 5 customer payments (ids 5001–5055), plus `meta/account`, `meta/counters`, `meta/limits` and one `line-counters` row per transaction.
63
+
64
+ Deliberate edge cases: an inactive customer (1004) and an inactive item (2006); a customer with no e-mail (1003); an individual
65
+ rather than a company (1008); a customer whose `entityId` is at the 83-character limit (1011); two customers whose names differ only
66
+ in case (1002 / 1012); an item stocked only in subsidiary 2 (2005); three service items that are **not** in the `inventoryItem`
67
+ collection (2008–2010); a zero-total invoice (5039); a fractional quantity (order 5008); a 200-line order and its 200-line invoice
68
+ (5009 / 5040); a voided invoice (5036); a payment with 1,500.00 unapplied (5054); and a partially billed order (5002).
69
+
70
+ Internal ids are NetSuite-style numeric strings drawn from three counters (`nextEntityId`, `nextItemId`, `nextTransactionId`), and
71
+ the row id **is** the internal id. Money is stored with two decimals and computed in integer cents; quantities and rates carry up to
72
+ four decimals. Balances (`balance`, `overdueBalance`, `unbilledOrders`, `amountRemaining`) are **computed on every read** from the
73
+ transactions, never stored.
74
+
75
+ ## Operations
76
+
77
+ 26 operations over 24 provider-shaped routes: `list`/`get` for customer, sales order, invoice, customer payment and inventory item;
78
+ `create`/`update` for customer, sales order and invoice; `customer.delete`; `sales-order.items.list`; the two transforms
79
+ (`salesOrder → invoice`, `invoice → customerPayment`); `subsidiary.list`; `suiteql.query`; the four MCP-shaped operations; and `session.get`, which returns the signed-in session
80
+ (account, employee, role with its permission levels, subsidiary scope, server time and account date). `session.get` has **no HTTP
81
+ route** — NetSuite exposes this through its application shell, not as a REST resource — and is what the browser app reads to render
82
+ the account/role chrome and to date every screen from world virtual time.
83
+ Declared errors use NetSuite's problem envelope
84
+ (`{ type, title, status, "o:errorDetails": [ { detail, "o:errorCode", … } ] }`): `INVALID_LOGIN` 401, `INSUFFICIENT_PERMISSION` 403,
85
+ `NONEXISTENT_ID` 404, `RCRD_HAS_BEEN_CHANGED` 409, `CONCURRENCY_LIMIT_EXCEEDED` 429, `UNEXPECTED_ERROR` 500, and
86
+ `INVALID_ID` / `INVALID_CONTENT` / `INVALID_KEY_OR_REF` / `INVALID_PARAMETER` / `INVALID_REQUEST` / `USER_ERROR` /
87
+ `RESULT_SET_TOO_LARGE` at 400.
88
+
89
+ **`q` filters.** `customer.list`, `sales-order.list`, `invoice.list`, `customer-payment.list` and `inventory-item.list` accept
90
+ NetSuite's `q` grammar (`field OPERATOR value`, `AND` binding tighter than `OR`, parentheses, `[a,b]` lists) over a published field
91
+ list per record type. Operators follow the field's type: `EMPTY`/`EMPTY_NOT` anywhere, `IS`/`IS_NOT` for booleans and strings,
92
+ `CONTAIN`/`START_WITH`/`ENDWITH`, `ANY_OF`/`BETWEEN`/`EQUAL`/`GREATER…`/`LESS…`/`WITHIN` for numbers, `ON`/`BEFORE`/`AFTER`/
93
+ `ON_OR_BEFORE`/`ON_OR_AFTER` for dates, each with a `_NOT` form. An unknown field or an operator the type does not accept is
94
+ `INVALID_REQUEST`, never a silently ignored filter. Status is filtered by its **stored key** (`pendingFulfillment`, `paidInFull`, …),
95
+ not by NetSuite's `SalesOrd:B` form.
96
+
97
+ **Paging.** `limit` 1–1000 (default 1000; SuiteQL default 10) and an `offset` that must be a multiple of `limit`. Ordering is
98
+ internal id ascending, numerically — NetSuite does not document a default order, so this package fixes one. Pages are also bounded by
99
+ encoded UTF-8 bytes (`meta/limits.maxPageBytes`, default 900,000) and every scan is bounded by `meta/limits.maxScanRows`
100
+ (default 5,000); reaching either bound fails `RESULT_SET_TOO_LARGE` rather than silently truncating.
101
+
102
+ **SuiteQL subset.** `SELECT [DISTINCT] … FROM <table> [[INNER|LEFT] JOIN … ON …] [WHERE …] [GROUP BY …] [HAVING …] [ORDER BY …]
103
+ [FETCH FIRST n ROWS ONLY]` over `customer`, `transaction`, `transactionLine`, `item`, `employee` and `subsidiary`, with column and
104
+ table aliases, `?` bind parameters, `COUNT`/`SUM`/`MIN`/`MAX`/`AVG`, `UPPER`/`LOWER`/`NVL`, `LIKE`, `IN`, `BETWEEN`, `IS [NOT] NULL`
105
+ and `BUILTIN.DF(<column>)`. Every scalar is rendered as a JSON string, as NetSuite does. A join condition must be one or more column
106
+ equalities so the joined table is indexed into a `Map` once per request. Anything outside the subset — a non-`SELECT` statement, a
107
+ second statement, subqueries, `UNION`, `CASE`, window functions, an unknown table or column — fails `INVALID_REQUEST` naming the
108
+ construct. `Prefer: transient` is required on the HTTP route.
109
+
110
+ ## Events and faults
111
+
112
+ Events: `record.changed` (`{ recordType, recordId, changeType: CREATE|UPDATE|DELETE, occurredAt, employeeId, subsidiaryId }`) and
113
+ `transaction.status-changed` (`{ recordType, recordId, tranId, previousStatus, status, occurredAt, total, amountRemaining }`).
114
+ Nothing is delivered anywhere; there are no subscriptions and no callbacks (NetSuite has no first-party outbound webhook for record
115
+ changes, so declaring one would invent a protocol).
116
+
117
+ Faults, each with a scenario in `firedrill/`: `concurrency-limit` (every operation → 429, `Retry-After: 5`), `write-outage` (the nine
118
+ write operations → 500, reads keep working), `record-locked` (the five edit/transform operations → 409) and
119
+ `transform-response-lost` (`after_commit` on the two transforms: the invoice or payment **is** created and its events emitted, but
120
+ the caller sees 500 — the double-billing hazard worth rehearsing).
121
+
122
+ ## Conformance
123
+
124
+ `firedrill tool test netsuite` runs 15 drills over 6 scenarios and 7 actors and passes with **zero coverage violations**: every one
125
+ of the 25 operations has a successful call, every declared error of every operation is observed, both events are emitted and all four
126
+ faults activate, over two identical runs. See [`specs/netsuite/VERIFICATION.md`](../../specs/netsuite/VERIFICATION.md) in this
127
+ repository for the exact commands and outputs.
128
+
129
+ ## Browser app
130
+
131
+ `firedrill serve` publishes a browser app for this Tool (**Tools → Open app**). It is a recreation of the NetSuite
132
+ application shell — the dark application bar with the Oracle NetSuite lockup, global search, Recent Records, Create New,
133
+ Shortcuts, Help and the user menu; the blue global menu bar (Activities, Payments, Transactions, Lists, Reports,
134
+ Analytics, Documents, Setup, Customization, Commerce, Support) with their real submenus; the account/role strip; and the
135
+ release footer. Screens:
136
+
137
+ | screen | route | operations |
138
+ |---|---|---|
139
+ | Home dashboard — the full portlet grid: Reminders (seven counters), Recent Records, Navigation, Tasks, Key Performance Indicators, Trend Graphs (invoiced sales by month), Report Snapshots (Sales by Customer), Settings, Shortcuts, Custom Search (Open Invoices), Calendar (month grid on the account date), Phone Calls, Tips. Every figure is paged out of the Tool's own collections; Tasks, Phone Calls and calendar events are rendered empty and labelled not simulated | `#/home` | `invoice.list`/`get`, `sales-order.list`/`get`, `customer-payment.list`, `session.get` |
140
+ | Customers list — quick filter mapped to a real `q`, Show Inactives, NetSuite's paging strip | `#/customers` | `customer.list`, `customer.get` |
141
+ | Customer record — Primary Information / Address / Financial / Transactions / System Information subtabs, Edit form, Actions → Delete with confirmation | `#/customers/<id>` | `customer.get`, `customer.update`, `customer.delete`, `sales-order.list`, `invoice.list`, `subsidiary.list` |
142
+ | Sales Orders list with a status filter | `#/orders` | `sales-order.list`, `sales-order.get` |
143
+ | Sales order record — Items sublist with its own paging, totals, **Bill** (whole order or a quantity per line) | `#/orders/<id>` | `sales-order.get`, `sales-order.items.list`, `sales-order.transform` |
144
+ | Enter Sales Order — customer and item pickers that page as you type, line grid, live subtotal | `#/orders/new` | `customer.list`/`get`, `inventory-item.list`/`get`, `sales-order.create` |
145
+ | Invoices list — Open / Overdue / Paid In Full / All views | `#/invoices` | `invoice.list`, `invoice.get` |
146
+ | Invoice record — items, totals, applied payments, **Accept Payment** | `#/invoices/<id>` | `invoice.get`, `invoice.update`, `invoice.transform`, `customer-payment.list`/`get` |
147
+ | Create Invoice | `#/invoices/new` | `invoice.create`, `customer.list`, `inventory-item.list` |
148
+ | Customer Payments list | `#/payments` | `customer-payment.list`, `customer-payment.get` |
149
+ | Items (read-only) with a detail panel | `#/items` | `inventory-item.list`, `inventory-item.get` |
150
+ | Subsidiaries (read-only) | `#/subsidiaries` | `subsidiary.list` |
151
+ | SuiteQL Query Tool — editor, results grid with paging, raw response body | `#/suiteql` | `suiteql.query` |
152
+
153
+ The app and the API share one world: a customer created in the app is visible over `GET /services/rest/record/v1/customer`
154
+ and the reverse, and the app re-reads after every write and whenever `getContext().revision` moves.
155
+
156
+ Permissions and subsidiary scope come from the acting role through `session.get`: an action the role cannot perform is
157
+ disabled, and a refused read renders NetSuite's own "Permission Violation" text. "Today", due-date colouring and the
158
+ account date come from world virtual time (`session.get.accountDate`), never from the browser clock, so replaying a world
159
+ on another day renders identically.
160
+
161
+ **Controls that are present but not simulated.** Every persistent control of the real client is rendered with its label and
162
+ hover state; those outside this Tool's scope open a short "Not simulated by this Tool" panel: global search suggestions
163
+ (the search box itself filters customers), Help, Shortcuts personalization, the user menu (Set Preferences, Change
164
+ Password, Change Role; Log Out is disabled), the role switcher, portlet setup and Personalize Dashboard, the Tasks / Phone Calls / Calendar portlets (their New and View All buttons; activities are not modelled, so the lists are empty rather than invented), the Navigation portlet's out-of-scope menu entries, Customize
165
+ View / Export on every list, Print / Email / More and the unsupported entries of each record's Actions menu (Fulfill,
166
+ Close Order, Void, Credit Memo, Make Copy, Merge, Make Inactive), New Item, New Subsidiary, Save & New / Save & Print /
167
+ Reset on the entry forms, Save Query / Query History in the SuiteQL tool, and every global-menu entry outside the table
168
+ above (Activities, Reports, Documents, Customization, Commerce, Support and the unimplemented Transactions, Lists,
169
+ Payments and Setup entries). Nothing invents data for them.
170
+
171
+ Assets: `app/assets/oracle-netsuite.svg` and `app/assets/oracle.svg` are the official downloaded Oracle marks (sources and
172
+ checksums in `app/assets/ATTRIBUTION.md`); the bundled face is Open Sans (SIL OFL 1.1, `app/assets/fonts/LICENSE.txt`),
173
+ since NetSuite's own Oracle Sans is proprietary. Interface glyphs are original SVG paths in the app's `icons.js`.
174
+
175
+ ## Trademarks
176
+
177
+ Oracle, NetSuite, Oracle NetSuite, SuiteTalk, SuiteQL, SuiteAnalytics and the Oracle NetSuite logo are trademarks of
178
+ Oracle Corporation. This package is an independent, unofficial simulation: product names and logos are used only to
179
+ identify the simulated service inside a Firedrill test environment. It is not affiliated with, endorsed by, sponsored by
180
+ or connected to Oracle Corporation, and it contacts no Oracle service.
181
+
182
+ ## Limitations
183
+
184
+ - **No official client has been run against this package.** Fidelity is `behavioral` / `stateful` / `contract`; nothing claims
185
+ `validated`. Oracle's SuiteCloud SDK and the MCP Standard Tools SuiteApp were used as documentation, not as test clients.
186
+ - The account time zone is a fixed −08:00 with no daylight saving, and the fiscal calendar is not modelled.
187
+ - Currency is per subsidiary (USD / CAD) with **no exchange rates and no consolidation**; a CAD transaction's total is a CAD number.
188
+ - Taxes are a flat per-subsidiary rate (8.6 % / 5 %) on each line, not a tax-code engine; there are no tax groups, no nexuses and no
189
+ tax items.
190
+ - No fulfillments, item receipts, credit memos, journal entries, deposits, custom records, custom fields, saved searches, workflows,
191
+ SuiteScript, file cabinet, employees-as-vendors, or sublists other than `item`, `apply`, `addressBook` and `subsidiary`.
192
+ - Sales-order status is derived from billed quantities only (approval and fulfilment are not simulated), so `partiallyFulfilled` and
193
+ `pendingBilling` appear in starter data but are never *computed*.
194
+ - `subsidiary`, `employee` and `role` rows are read-only; there is no operation that writes them.
195
+ - The metadata catalog is assembled on every call from a field table in `lib/metadata.mjs` that mirrors the state schemas and the `q`
196
+ filter tables. It is not the real NetSuite `application/schema+json` document, and its `x-ns-*` keys are this package's own.
197
+ - Two request shapes are refused by the framework before this Tool's codecs see them, so they answer Firedrill's envelope rather than
198
+ NetSuite's: a body that is not JSON at all, and a body over the framework's size limit. A `__proto__` key is also removed by the
199
+ framework's JSON decoding before the codec runs; a `constructor` or `prototype` key does reach the codec and answers
200
+ `INVALID_REQUEST`.
201
+ - When the *world* denies an operation (an actor whose grants do not cover it), the denial is raised by Firedrill before any handler
202
+ runs. The provider-shaped routes still answer a coherent NetSuite envelope — `403` with `INSUFFICIENT_PERMISSION` and a generic
203
+ "Permission Violation: ..." detail — because the codec maps a denied outcome itself and never quotes the framework's own
204
+ message (which names the internal operation id). The detail is therefore less specific than a role-permission refusal, which names
205
+ the missing permission. On the canonical operation endpoint the denial keeps Firedrill's `world.OPERATION_DENIED` outcome, as it must.
206
+ - An argument that violates an operation's own input schema (a value longer or larger than the schema allows) is rejected by
207
+ Firedrill before the handler runs. The routes still answer a coherent NetSuite envelope — `400` with `INVALID_PARAMETER` and a
208
+ generic "Invalid request parameter value. ..." detail — because the codec derives the NetSuite code from the outcome status the
209
+ framework reports, so the body's `status` always matches the HTTP status line. The bounds this package documents (`fields` at most
210
+ 40 names, `q` at most 2000 characters, per-field lengths on write bodies) are checked in the handlers, so those cases answer the
211
+ specific `INVALID_PARAMETER` / `INVALID_REQUEST` / `INVALID_CONTENT` detail with `o:errorQueryParam` or `o:errorPath`.
212
+ - `Location` headers and every `links[].href` are **relative** paths: a codec cannot see the request host.
213
+ - Idempotency is the framework's: a retry carrying the same `X-NetSuite-Idempotency-Key` replays the recorded outcome. NetSuite's own
214
+ header behaviour (and its 4-hour window) is not otherwise simulated.
@@ -0,0 +1,16 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "target": {
4
+ "id": "conformance-agent",
5
+ "kind": "command",
6
+ "bindings": [
7
+ "http"
8
+ ],
9
+ "executable": "node",
10
+ "arguments": [
11
+ "test/conformance.mjs"
12
+ ],
13
+ "workingDirectory": ".",
14
+ "timeoutMs": 10000
15
+ }
16
+ }