@sixtyninecommerce/admin 0.6.0 → 0.8.0

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 (39) hide show
  1. package/README.md +359 -2
  2. package/dist/apps/assertion.d.ts +30 -3
  3. package/dist/apps/assertion.js +40 -3
  4. package/dist/apps/config.d.ts +39 -0
  5. package/dist/apps/config.js +22 -0
  6. package/dist/apps/credential.d.ts +5 -0
  7. package/dist/apps/credential.js +55 -4
  8. package/dist/apps/index.d.ts +11 -0
  9. package/dist/client.d.ts +54 -1
  10. package/dist/client.js +34 -1
  11. package/dist/custom-objects/index.d.ts +66 -0
  12. package/dist/custom-objects/index.js +48 -0
  13. package/dist/customers/index.d.ts +225 -0
  14. package/dist/customers/index.js +279 -0
  15. package/dist/documents/index.d.ts +432 -0
  16. package/dist/documents/index.js +289 -0
  17. package/dist/index.d.ts +34 -2
  18. package/dist/index.js +16 -1
  19. package/dist/jobs/index.d.ts +143 -0
  20. package/dist/jobs/index.js +132 -0
  21. package/dist/metafields/index.d.ts +291 -0
  22. package/dist/metafields/index.js +141 -0
  23. package/dist/orders/index.d.ts +106 -0
  24. package/dist/orders/index.js +115 -0
  25. package/dist/orders/surcharges.d.ts +121 -0
  26. package/dist/orders/surcharges.js +110 -0
  27. package/dist/payments/index.d.ts +244 -0
  28. package/dist/payments/index.js +103 -0
  29. package/dist/payments/providers.d.ts +281 -0
  30. package/dist/payments/providers.js +205 -0
  31. package/dist/platform/provision-store.d.ts +9 -26
  32. package/dist/platform/provision-store.js +8 -66
  33. package/dist/tax/index.d.ts +128 -0
  34. package/dist/tax/index.js +101 -0
  35. package/dist/tenant/capabilities.d.ts +15 -0
  36. package/dist/types.d.ts +20 -3
  37. package/dist/uploads/index.d.ts +146 -0
  38. package/dist/uploads/index.js +72 -0
  39. package/package.json +3 -3
@@ -0,0 +1,291 @@
1
+ import { type Page, type Transport } from '@sixtyninecommerce/core';
2
+ import type { Call } from '../types.js';
3
+ /**
4
+ * The entities a metafield may hang off.
5
+ *
6
+ * `content_item` is in the app manifest's vocabulary and is deliberately NOT
7
+ * here: content rows live in the CMS, which already models tenant-defined
8
+ * fields, and commerce refuses a value written against it
9
+ * (`METAFIELD_OWNER_INVALID`). A manifest declaring one still registers; only
10
+ * the value surface says no.
11
+ */
12
+ export declare const METAFIELD_OWNER_TYPES: readonly ["product", "variant", "customer", "order", "order_line", "location", "collection"];
13
+ export type MetafieldOwnerType = (typeof METAFIELD_OWNER_TYPES)[number];
14
+ /** Scalar value types. `list<T>` of any of them is also a type. */
15
+ export declare const METAFIELD_SCALAR_TYPES: readonly ["text", "number", "money", "boolean", "date", "json", "reference", "attachment"];
16
+ export type MetafieldScalarType = (typeof METAFIELD_SCALAR_TYPES)[number];
17
+ export type MetafieldType = MetafieldScalarType | `list<${MetafieldScalarType}>`;
18
+ /** §8.4's attachment: opaque to commerce, rendered as a reference on documents. */
19
+ export interface MetafieldAttachment {
20
+ kind: string;
21
+ ref: string;
22
+ label?: string | null;
23
+ }
24
+ /** A `reference` value. The platform does not resolve or fetch it. */
25
+ export interface MetafieldReference {
26
+ entityType: string;
27
+ id: string;
28
+ }
29
+ /**
30
+ * A `money` value is a bare INTEGER in minor units — `1999` is 19.99 — never a
31
+ * decimal, and it carries no currency of its own, exactly as the app-manifest
32
+ * schema describes it. A channel may sell in several currencies, so an app
33
+ * storing money on an entity that has no currency of its own declares a
34
+ * separate field for the currency beside it.
35
+ *
36
+ * `min` / `max` in the definition's `validation` are minor units too, and the
37
+ * engine refuses a decimal bound.
38
+ */
39
+ export type MetafieldMoney = number;
40
+ /**
41
+ * What a merchant, an app and a storefront may each do with a field.
42
+ *
43
+ * **Visibility is not authority.** `app` is always `read_write`; whether an app
44
+ * may actually write is decided by namespace ownership — its own slug, never
45
+ * another app's and never `platform`.
46
+ */
47
+ export interface MetafieldVisibility {
48
+ app: 'read_write';
49
+ /** `edit` is the ONLY value that lets the tenant write the field. */
50
+ merchant: 'hidden' | 'read' | 'edit';
51
+ /** `read` is what the Shop API projects; `hidden` keeps it back-office. */
52
+ storefront: 'hidden' | 'read';
53
+ }
54
+ /** One stored value. */
55
+ export interface Metafield {
56
+ ownerType: MetafieldOwnerType;
57
+ ownerId: string;
58
+ /** The declaring app's slug, or `platform` for the built-ins of §8.3/§8.4. */
59
+ namespace: string;
60
+ key: string;
61
+ /** Shaped by the definition's `type`. */
62
+ value: unknown;
63
+ /** ISO-8601 instant. */
64
+ updatedAt: string;
65
+ }
66
+ /**
67
+ * What MAY be stored on an entity.
68
+ *
69
+ * Three kinds. A definition this STORE registered (`definitions.upsert`, `id`
70
+ * set and `appId` null). A `platform` built-in of §8.3/§8.4, which is code
71
+ * rather than a row (`id` and `appId` both null, present on every store). And an
72
+ * APP's, provisioned from its manifest when the app is installed and on every
73
+ * consent approval (`appId` set) — so `appId` is how you tell an app's field
74
+ * from your own, which is the distinction the engine enforces: only a store's
75
+ * own definitions are editable and removable through this module.
76
+ *
77
+ * `active` goes false when the declaring app withdraws the field or is
78
+ * uninstalled. The VALUES are kept for a redaction window, so the definition
79
+ * still appears here and still describes them, but nothing new can be written
80
+ * under it and the Shop API stops publishing it at once. Withdrawing also ENDS
81
+ * the merchant's consent to that field: declaring it again asks for consent
82
+ * afresh, however the new declaration compares with the inactive row.
83
+ */
84
+ export interface MetafieldDefinition {
85
+ /** Null for a platform built-in, which is code rather than a row. */
86
+ id: string | null;
87
+ /** The declaring app, or null for a platform built-in. */
88
+ appId: string | null;
89
+ ownerType: MetafieldOwnerType;
90
+ namespace: string;
91
+ key: string;
92
+ type: MetafieldType;
93
+ /** The rules values are checked against; null when the app declared none. */
94
+ validation: Record<string, unknown> | null;
95
+ visibility: MetafieldVisibility;
96
+ /**
97
+ * Declared and stored, read by nothing yet. §8.13 promises admin filters and
98
+ * Meilisearch facets on an indexed field; neither exists, so treat this as
99
+ * the app's intent rather than a capability.
100
+ */
101
+ indexed: boolean;
102
+ /**
103
+ * False once the declaring app was uninstalled, or stopped declaring the
104
+ * field. Existing VALUES are kept — §6.9 gives an uninstalled app a 48 h
105
+ * redaction window — but no new value can be written.
106
+ */
107
+ active: boolean;
108
+ }
109
+ export interface SetMetafieldInput {
110
+ ownerType: MetafieldOwnerType;
111
+ ownerId: string;
112
+ /** Your own namespace. An app's is its slug; `platform` is never writable. */
113
+ namespace: string;
114
+ /** Lower snake_case, as the manifest declared it. */
115
+ key: string;
116
+ value: unknown;
117
+ }
118
+ /**
119
+ * A definition this store registers for itself. Whole-record, not a patch: an
120
+ * omitted `validation` clears it.
121
+ */
122
+ export interface UpsertMetafieldDefinitionInput {
123
+ ownerType: MetafieldOwnerType;
124
+ /**
125
+ * Lower snake_case. Never `platform`, and never the namespace of an app
126
+ * installed on this store — both answer `METAFIELD_NAMESPACE_FORBIDDEN`.
127
+ */
128
+ namespace: string;
129
+ /** Lower snake_case. */
130
+ key: string;
131
+ type: MetafieldType;
132
+ validation?: Record<string, unknown> | null;
133
+ /**
134
+ * How the field is PRESENTED, not who owns it: a definition your store
135
+ * registered stays writable AND readable by your store at any `merchant`
136
+ * setting. The flag governs an APP's fields, where `edit` is what lets the
137
+ * merchant write one at all and `hidden` keeps it out of `definitions.list`
138
+ * and `get`.
139
+ */
140
+ visibility: {
141
+ merchant: 'hidden' | 'read' | 'edit';
142
+ storefront: 'hidden' | 'read';
143
+ };
144
+ /** Stored; no search or facet reads it yet. */
145
+ indexed?: boolean;
146
+ }
147
+ export interface MetafieldDefinitionsModule {
148
+ /**
149
+ * Every definition this store has, including the platform built-ins.
150
+ *
151
+ * A `Page` because every list in this SDK is one (spec §9.2), not because
152
+ * this query pages: the Admin API answers the complete array, so the page is
153
+ * always the last one.
154
+ *
155
+ * Filtered by what the CALLER may see, on BOTH axes: an app gets its own
156
+ * namespace and the platform built-ins while a human or an API token gets
157
+ * everything but `merchant: hidden`, and either way a definition only appears
158
+ * when the caller holds its owner entity's READ permission — so a principal
159
+ * that cannot read customers never learns which customer fields an app keeps.
160
+ */
161
+ list(options?: {
162
+ ownerType?: MetafieldOwnerType;
163
+ } & Call): Promise<Page<MetafieldDefinition>>;
164
+ /**
165
+ * Register or replace one of THIS store's own definitions.
166
+ *
167
+ * Needs the owner entity's own WRITE permission — `UpdateOrder` to declare a
168
+ * field on orders, `UpdateCatalog` on products — so nothing has to be granted
169
+ * before a store can use it. Re-registering a key that was withdrawn revives
170
+ * it, with the values it still holds.
171
+ *
172
+ * An APP cannot call this: an app declares its fields in its manifest, which
173
+ * is what the merchant consents to (`METAFIELD_NAMESPACE_FORBIDDEN`).
174
+ *
175
+ * Two refusals that are easy to trip over: changing a definition's `type`
176
+ * while values exist (`METAFIELD_DEFINITION_IN_USE` — nothing re-validates or
177
+ * migrates them, so clear them first), and a `validation.pattern` that can
178
+ * backtrack catastrophically such as `^(a+)+$` (`METAFIELD_INVALID` — the
179
+ * regex runs on the engine every store shares).
180
+ */
181
+ upsert(input: UpsertMetafieldDefinitionInput, options?: Call): Promise<MetafieldDefinition>;
182
+ /**
183
+ * Remove one of this store's own definitions. True when one was removed,
184
+ * false when there was none.
185
+ *
186
+ * Refused while values still exist under it
187
+ * (`METAFIELD_DEFINITION_IN_USE` → `BusinessRuleError`): a value whose
188
+ * definition is gone can never be read back. Clear them with
189
+ * {@link MetafieldsModule.delete} first — which reaches them even on a
190
+ * deleted product or customer. An app's declaration and a `platform` built-in
191
+ * are never removable here.
192
+ */
193
+ delete(ownerType: MetafieldOwnerType, namespace: string, key: string, options?: Call): Promise<boolean>;
194
+ }
195
+ /**
196
+ * App and platform custom data on core entities (spec §8.13, §8.4).
197
+ *
198
+ * **Nothing undeclared is stored.** A value needs a definition — one this store
199
+ * registered (`definitions.upsert`) or a `platform` built-in of §8.3/§8.4. An
200
+ * unknown `(ownerType, namespace, key)` is `METAFIELD_DEFINITION_NOT_FOUND`
201
+ * (→ `NotFoundError`).
202
+ *
203
+ * **An app's declarations arrive with consent.** They are provisioned when the
204
+ * app is installed and on every consent approval. A re-registration applies at
205
+ * once what only NARROWS an active field, and withdraws one at once; it holds
206
+ * back what widens an active field, what is brand new, and — because a
207
+ * withdrawal ends consent — anything declared again over an INACTIVE row, even
208
+ * a narrowing. So a field an app has asked to make more visible keeps its old
209
+ * visibility, a field it has withdrawn stops being published without waiting
210
+ * for anyone, and a field it withdrew and wants back waits for the merchant
211
+ * exactly as a new one does.
212
+ *
213
+ * **Who may write what** (`METAFIELD_NAMESPACE_FORBIDDEN` /
214
+ * `METAFIELD_EDIT_FORBIDDEN`, both → `PermissionDeniedError`):
215
+ *
216
+ * | Principal | May write |
217
+ * |---|---|
218
+ * | an app installation | its OWN namespace (its slug), whatever the visibility says |
219
+ * | a human session or an API token | a definition whose `visibility.merchant` is `edit` |
220
+ * | anybody | never the `platform` namespace — the platform writes its own built-ins |
221
+ *
222
+ * Reading needs the owner entity's own READ permission and writing its own
223
+ * WRITE permission (`UpdateOrder` for an order and its lines, `UpdateCatalog`
224
+ * for a product, and so on). There is no separate metafield permission, so
225
+ * nothing has to be granted before this works.
226
+ *
227
+ * **Everything is per channel.** The owner must belong to the client's tenant
228
+ * (`METAFIELD_OWNER_NOT_FOUND` → `NotFoundError`, the same answer for an id
229
+ * that belongs to another store and one that belongs to nobody), and the value
230
+ * is stored against that channel — a product shared by two stores holds two
231
+ * independent values.
232
+ *
233
+ * Other coded refusals: `METAFIELD_INVALID` (→ `ValidationError`) when the
234
+ * value does not fit the definition's `type` and `validation`, and
235
+ * `METAFIELD_OWNER_INVALID` (→ `ValidationError`) for an owner commerce does
236
+ * not store.
237
+ *
238
+ * **What a storefront sees** (Shop API, server-side in this release): only
239
+ * `visibility.storefront = read` definitions, only on `product`, `variant` and
240
+ * `collection`, and only while the owner itself is public — a disabled product,
241
+ * a variant of a disabled product and a private collection all answer as if
242
+ * they did not exist.
243
+ */
244
+ export interface MetafieldsModule {
245
+ /**
246
+ * Every value on one entity that the caller may see.
247
+ *
248
+ * Not a `Page`: this is the set of fields attached to ONE record — bounded by
249
+ * the definitions the store has declared — the way an entity's own columns
250
+ * are, not a collection to walk.
251
+ */
252
+ get(ownerType: MetafieldOwnerType, ownerId: string, options?: {
253
+ namespace?: string;
254
+ } & Call): Promise<Metafield[]>;
255
+ /**
256
+ * Write one value. Replaces whatever was there; there is no partial merge.
257
+ *
258
+ * The value must fit the definition's `type`: a `money` is a bare integer in
259
+ * minor units, a `date` is an ISO-8601 instant with an explicit offset (and a
260
+ * calendar date that exists — `2026-02-30` is refused rather than rolled over
261
+ * to March), an `attachment` is exactly `{ kind, ref, label? }` and a
262
+ * `reference` exactly `{ entityType, id }`.
263
+ *
264
+ * To CLEAR a field use {@link delete} — `null` is refused, because a stored
265
+ * JSON `null` and an absent row cannot be told apart afterwards.
266
+ *
267
+ * `options.idempotencyKey` is inert on this surface today: the Admin API has
268
+ * no idempotency header yet, so a repeat is a second write. It is harmless to
269
+ * pass and starts being honoured when the engine grows the store.
270
+ */
271
+ set(input: SetMetafieldInput, options?: Call): Promise<Metafield>;
272
+ /**
273
+ * Remove a value. True when one was removed, false when there was none.
274
+ *
275
+ * This is the CLEANUP route, so it is deliberately more permissive than
276
+ * `set`: it works on a withdrawn definition, on a definition that is gone
277
+ * entirely, and on a soft-deleted owner — a deleted product's or customer's
278
+ * values are exactly what a GDPR erasure has to reach. Only WRITING needs a
279
+ * live declaration and a live owner. Namespace ownership is not relaxed: an
280
+ * app still clears only its own namespace.
281
+ */
282
+ delete(ownerType: MetafieldOwnerType, ownerId: string, namespace: string, key: string, options?: Call): Promise<boolean>;
283
+ definitions: MetafieldDefinitionsModule;
284
+ }
285
+ export declare const METAFIELDS_QUERY = "\n query Metafields($ownerType: String!, $ownerId: ID!, $namespace: String) {\n metafields(ownerType: $ownerType, ownerId: $ownerId, namespace: $namespace) {\n \n ownerType\n ownerId\n namespace\n key\n value\n updatedAt\n\n }\n }\n";
286
+ export declare const UPSERT_METAFIELD_DEFINITION_MUTATION = "\n mutation UpsertMetafieldDefinition($input: UpsertMetafieldDefinitionInput!) {\n upsertMetafieldDefinition(input: $input) { \n id\n appId\n ownerType\n namespace\n key\n type\n validation\n visibility {\n app\n merchant\n storefront\n }\n indexed\n active\n }\n }\n";
287
+ export declare const DELETE_METAFIELD_DEFINITION_MUTATION = "\n mutation DeleteMetafieldDefinition($ownerType: String!, $namespace: String!, $key: String!) {\n deleteMetafieldDefinition(ownerType: $ownerType, namespace: $namespace, key: $key)\n }\n";
288
+ export declare const METAFIELD_DEFINITIONS_QUERY = "\n query MetafieldDefinitions($ownerType: String) {\n metafieldDefinitions(ownerType: $ownerType) { \n id\n appId\n ownerType\n namespace\n key\n type\n validation\n visibility {\n app\n merchant\n storefront\n }\n indexed\n active\n }\n }\n";
289
+ export declare const SET_METAFIELD_MUTATION = "\n mutation SetMetafield($input: SetMetafieldInput!) {\n setMetafield(input: $input) { \n ownerType\n ownerId\n namespace\n key\n value\n updatedAt\n }\n }\n";
290
+ export declare const DELETE_METAFIELD_MUTATION = "\n mutation DeleteMetafield($ownerType: String!, $ownerId: ID!, $namespace: String!, $key: String!) {\n deleteMetafield(ownerType: $ownerType, ownerId: $ownerId, namespace: $namespace, key: $key)\n }\n";
291
+ export declare function createMetafieldsModule(transport: Transport): MetafieldsModule;
@@ -0,0 +1,141 @@
1
+ import { createPage } from '@sixtyninecommerce/core';
2
+ /**
3
+ * The entities a metafield may hang off.
4
+ *
5
+ * `content_item` is in the app manifest's vocabulary and is deliberately NOT
6
+ * here: content rows live in the CMS, which already models tenant-defined
7
+ * fields, and commerce refuses a value written against it
8
+ * (`METAFIELD_OWNER_INVALID`). A manifest declaring one still registers; only
9
+ * the value surface says no.
10
+ */
11
+ export const METAFIELD_OWNER_TYPES = [
12
+ 'product',
13
+ 'variant',
14
+ 'customer',
15
+ 'order',
16
+ 'order_line',
17
+ 'location',
18
+ 'collection',
19
+ ];
20
+ /** Scalar value types. `list<T>` of any of them is also a type. */
21
+ export const METAFIELD_SCALAR_TYPES = [
22
+ 'text',
23
+ 'number',
24
+ 'money',
25
+ 'boolean',
26
+ 'date',
27
+ 'json',
28
+ 'reference',
29
+ 'attachment',
30
+ ];
31
+ const METAFIELD_FIELDS = /* GraphQL */ `
32
+ ownerType
33
+ ownerId
34
+ namespace
35
+ key
36
+ value
37
+ updatedAt
38
+ `;
39
+ const METAFIELD_DEFINITION_FIELDS = /* GraphQL */ `
40
+ id
41
+ appId
42
+ ownerType
43
+ namespace
44
+ key
45
+ type
46
+ validation
47
+ visibility {
48
+ app
49
+ merchant
50
+ storefront
51
+ }
52
+ indexed
53
+ active
54
+ `;
55
+ export const METAFIELDS_QUERY = /* GraphQL */ `
56
+ query Metafields($ownerType: String!, $ownerId: ID!, $namespace: String) {
57
+ metafields(ownerType: $ownerType, ownerId: $ownerId, namespace: $namespace) {
58
+ ${METAFIELD_FIELDS}
59
+ }
60
+ }
61
+ `;
62
+ export const UPSERT_METAFIELD_DEFINITION_MUTATION = /* GraphQL */ `
63
+ mutation UpsertMetafieldDefinition($input: UpsertMetafieldDefinitionInput!) {
64
+ upsertMetafieldDefinition(input: $input) { ${METAFIELD_DEFINITION_FIELDS} }
65
+ }
66
+ `;
67
+ export const DELETE_METAFIELD_DEFINITION_MUTATION = /* GraphQL */ `
68
+ mutation DeleteMetafieldDefinition($ownerType: String!, $namespace: String!, $key: String!) {
69
+ deleteMetafieldDefinition(ownerType: $ownerType, namespace: $namespace, key: $key)
70
+ }
71
+ `;
72
+ export const METAFIELD_DEFINITIONS_QUERY = /* GraphQL */ `
73
+ query MetafieldDefinitions($ownerType: String) {
74
+ metafieldDefinitions(ownerType: $ownerType) { ${METAFIELD_DEFINITION_FIELDS} }
75
+ }
76
+ `;
77
+ export const SET_METAFIELD_MUTATION = /* GraphQL */ `
78
+ mutation SetMetafield($input: SetMetafieldInput!) {
79
+ setMetafield(input: $input) { ${METAFIELD_FIELDS} }
80
+ }
81
+ `;
82
+ export const DELETE_METAFIELD_MUTATION = /* GraphQL */ `
83
+ mutation DeleteMetafield($ownerType: String!, $ownerId: ID!, $namespace: String!, $key: String!) {
84
+ deleteMetafield(ownerType: $ownerType, ownerId: $ownerId, namespace: $namespace, key: $key)
85
+ }
86
+ `;
87
+ export function createMetafieldsModule(transport) {
88
+ return {
89
+ async get(ownerType, ownerId, options) {
90
+ const { namespace, ...call } = options ?? {};
91
+ const data = await transport.graphql({
92
+ api: 'admin',
93
+ query: METAFIELDS_QUERY,
94
+ // `null`, not omitted: the argument is nullable and "every namespace"
95
+ // is what null means to the server.
96
+ variables: { ownerType, ownerId, namespace: namespace ?? null },
97
+ }, call);
98
+ return data.metafields;
99
+ },
100
+ async set(input, options) {
101
+ const data = await transport.graphql({ api: 'admin', query: SET_METAFIELD_MUTATION, variables: { input } }, options);
102
+ return data.setMetafield;
103
+ },
104
+ async delete(ownerType, ownerId, namespace, key, options) {
105
+ const data = await transport.graphql({
106
+ api: 'admin',
107
+ query: DELETE_METAFIELD_MUTATION,
108
+ variables: { ownerType, ownerId, namespace, key },
109
+ }, options);
110
+ return data.deleteMetafield;
111
+ },
112
+ definitions: {
113
+ async list(options) {
114
+ const { ownerType, ...call } = options ?? {};
115
+ const data = await transport.graphql({
116
+ api: 'admin',
117
+ query: METAFIELD_DEFINITIONS_QUERY,
118
+ variables: { ownerType: ownerType ?? null },
119
+ }, call);
120
+ const items = data.metafieldDefinitions;
121
+ return createPage(items, { nextCursor: null, hasNext: false, total: items.length }, () => {
122
+ // Unreachable while `hasNext` is false — `Page.next()` short-circuits
123
+ // — and a rejection rather than a silent re-fetch if that changes.
124
+ throw new Error('metafieldDefinitions returns a complete list; there is no next page');
125
+ });
126
+ },
127
+ async upsert(input, options) {
128
+ const data = await transport.graphql({ api: 'admin', query: UPSERT_METAFIELD_DEFINITION_MUTATION, variables: { input } }, options);
129
+ return data.upsertMetafieldDefinition;
130
+ },
131
+ async delete(ownerType, namespace, key, options) {
132
+ const data = await transport.graphql({
133
+ api: 'admin',
134
+ query: DELETE_METAFIELD_DEFINITION_MUTATION,
135
+ variables: { ownerType, namespace, key },
136
+ }, options);
137
+ return data.deleteMetafieldDefinition;
138
+ },
139
+ },
140
+ };
141
+ }
@@ -0,0 +1,106 @@
1
+ import type { Transport } from '@sixtyninecommerce/core';
2
+ import type { OrderPaymentPosition, PaidOrder } from '../payments/index.js';
3
+ import type { Call } from '../types.js';
4
+ import { type SurchargesModule } from './surcharges.js';
5
+ export type { PaidOrder } from '../payments/index.js';
6
+ /** The result of settling an order. */
7
+ export interface OrderSettlement {
8
+ order: PaidOrder;
9
+ position: OrderPaymentPosition;
10
+ /**
11
+ * What you asked for. Settling does NOT create a fulfilment: choosing a
12
+ * handler, a method and lines for a merchant is not the platform's decision
13
+ * to make, so the flag is reported back rather than acted on.
14
+ */
15
+ fulfilRequested: boolean;
16
+ }
17
+ /**
18
+ * Orders (spec §8.2, §12.2).
19
+ *
20
+ * `surcharges` is W5-1's module, unchanged. The three methods beside it are the
21
+ * deposit flow: hold an order while the rest of the money is arranged, read
22
+ * what it stands at, and settle it once the money is really there.
23
+ */
24
+ export interface OrdersModule {
25
+ surcharges: SurchargesModule;
26
+ /**
27
+ * Hold a placed order while the rest of its money is arranged. Needs
28
+ * `UpdateOrder`.
29
+ *
30
+ * Reachable only from `ArrangingPayment`, and only once something has been
31
+ * paid — unless the store's deposit policy allows a zero-deposit park, which
32
+ * is off unless a merchant chose it. Refusals carry the order's position:
33
+ * `DEPOSITS_NOT_ENABLED` (the store has not turned deposits on — the default,
34
+ * so this is the first one you will meet), `DEPOSIT_REQUIRED` (nothing paid,
35
+ * and the store does not allow that), `DEPOSIT_BELOW_MINIMUM`,
36
+ * `ORDER_NOT_PARKABLE` (wrong state), `PARK_REASON_TOO_LONG` (500
37
+ * characters).
38
+ *
39
+ * Parking does not RESERVE stock: the platform allocates at the payment edge,
40
+ * so an order parked while arranging payment usually holds no allocations
41
+ * yet. What it protects is the ORDER — the stock-allocation sweeper leaves a
42
+ * parked order alone deliberately, so an expiring allocation TTL cannot
43
+ * cancel an order a customer has already paid a deposit on, and allocations
44
+ * it does hold stay put while it waits.
45
+ */
46
+ park(orderId: string, options?: Call & {
47
+ reason?: string;
48
+ }): Promise<PaidOrder>;
49
+ /**
50
+ * End a hold: hand a parked order back to the state it was parked FROM, and
51
+ * clear its park reason. Needs `UpdateOrder`.
52
+ *
53
+ * The target is read from the ORDER'S OWN HISTORY, never chosen by the
54
+ * caller and never assumed, so un-parking reverses exactly the move that
55
+ * parked it. `PARK_ORIGIN_UNKNOWN` is the refusal when that history does not
56
+ * say — move such an order on explicitly with a state transition instead.
57
+ *
58
+ * There is no storefront equivalent today: a parked order's remainder is
59
+ * recorded WHERE IT STANDS with `payments.record`, and the payment that
60
+ * clears it settles, places and allocates the order without ever leaving
61
+ * `Parked` first. Use this when the order has to be EDITED, which is the one
62
+ * thing it cannot do while held.
63
+ *
64
+ * It moves no money and changes no payment. Other refusals:
65
+ * `ORDER_NOT_PARKED`, `UNPARK_TRANSITION_REFUSED` (the origin was known and
66
+ * another guard in the order process refused the way back — an order
67
+ * minimum, a stock check — with its own reason). All carry the order's
68
+ * position.
69
+ */
70
+ unpark(orderId: string, options?: Call): Promise<PaidOrder>;
71
+ /** What the order has actually been paid. Needs `ReadOrder`. */
72
+ paymentPosition(orderId: string, options?: Call): Promise<OrderPaymentPosition>;
73
+ /**
74
+ * Move a fully paid order to `PaymentSettled`. Needs `UpdateOrder`.
75
+ *
76
+ * Refuses `COVERAGE_INSUFFICIENT` while the order's SETTLED money does not
77
+ * cover its total, with the position attached. Settled, not covered: an
78
+ * authorization is permission to take money, not money, so an order whose
79
+ * remainder is merely authorized is settled from `ArrangingPayment` after the
80
+ * authorization is captured, like any other order. `ORDER_NOT_SETTLEABLE` is
81
+ * the other refusal — a state a settlement cannot be reached from, which
82
+ * paying more can never fix.
83
+ *
84
+ * Calling this on an order the platform ALREADY settled succeeds rather than
85
+ * refusing. Expect that to be the common case: the engine settles an order as
86
+ * soon as its settled payments cover its total, so recording the payment that
87
+ * clears a deposit usually settles the order on its own, and this call is then
88
+ * a confirmation.
89
+ *
90
+ * So do not wait on a settle CALL to learn that a deposit was paid off — in
91
+ * the ordinary flow nobody makes one. The close signal is the platform event
92
+ * `payment.partial_recorded` with `outstanding_after: 0`, on the payment that
93
+ * cleared the balance. That figure nets settled PLUS authorized money, so it
94
+ * means fully COVERED; "fully settled" is `settled >= total` on the position,
95
+ * which is what this method requires. For money recorded through
96
+ * `payments.record` the two coincide — it only creates settled payments.
97
+ */
98
+ settle(orderId: string, options?: Call & {
99
+ fulfil?: boolean;
100
+ }): Promise<OrderSettlement>;
101
+ }
102
+ export declare const PARK_ORDER_MUTATION = "\n mutation ParkOrder($orderId: ID!, $reason: String, $idempotencyKey: String) {\n parkOrder(orderId: $orderId, reason: $reason, idempotencyKey: $idempotencyKey) {\n id\n code\n state\n currencyCode\n total\n totalWithTax\n }\n }\n";
103
+ export declare const UNPARK_ORDER_MUTATION = "\n mutation UnparkOrder($orderId: ID!, $idempotencyKey: String) {\n unparkOrder(orderId: $orderId, idempotencyKey: $idempotencyKey) {\n id\n code\n state\n currencyCode\n total\n totalWithTax\n }\n }\n";
104
+ export declare const SETTLE_ORDER_MUTATION = "\n mutation SettleOrder($orderId: ID!, $fulfil: Boolean!, $idempotencyKey: String) {\n settleOrder(orderId: $orderId, fulfil: $fulfil, idempotencyKey: $idempotencyKey) {\n \n order {\n id\n code\n state\n currencyCode\n total\n totalWithTax\n }\n position {\n total\n covered\n settled\n authorized\n refunded\n outstanding\n overpaid\n availableToCharge\n }\n fulfilRequested\n\n }\n }\n";
105
+ export declare const ORDER_POSITION_QUERY = "\n query OrderPosition($orderId: ID!) {\n orderPaymentPosition(orderId: $orderId) {\n total\n covered\n settled\n authorized\n refunded\n outstanding\n overpaid\n availableToCharge\n }\n }\n";
106
+ export declare function createOrdersModule(transport: Transport): OrdersModule;
@@ -0,0 +1,115 @@
1
+ import { createSurchargesModule } from './surcharges.js';
2
+ const SETTLEMENT_FIELDS = /* GraphQL */ `
3
+ order {
4
+ id
5
+ code
6
+ state
7
+ currencyCode
8
+ total
9
+ totalWithTax
10
+ }
11
+ position {
12
+ total
13
+ covered
14
+ settled
15
+ authorized
16
+ refunded
17
+ outstanding
18
+ overpaid
19
+ availableToCharge
20
+ }
21
+ fulfilRequested
22
+ `;
23
+ export const PARK_ORDER_MUTATION = /* GraphQL */ `
24
+ mutation ParkOrder($orderId: ID!, $reason: String, $idempotencyKey: String) {
25
+ parkOrder(orderId: $orderId, reason: $reason, idempotencyKey: $idempotencyKey) {
26
+ id
27
+ code
28
+ state
29
+ currencyCode
30
+ total
31
+ totalWithTax
32
+ }
33
+ }
34
+ `;
35
+ export const UNPARK_ORDER_MUTATION = /* GraphQL */ `
36
+ mutation UnparkOrder($orderId: ID!, $idempotencyKey: String) {
37
+ unparkOrder(orderId: $orderId, idempotencyKey: $idempotencyKey) {
38
+ id
39
+ code
40
+ state
41
+ currencyCode
42
+ total
43
+ totalWithTax
44
+ }
45
+ }
46
+ `;
47
+ export const SETTLE_ORDER_MUTATION = /* GraphQL */ `
48
+ mutation SettleOrder($orderId: ID!, $fulfil: Boolean!, $idempotencyKey: String) {
49
+ settleOrder(orderId: $orderId, fulfil: $fulfil, idempotencyKey: $idempotencyKey) {
50
+ ${SETTLEMENT_FIELDS}
51
+ }
52
+ }
53
+ `;
54
+ export const ORDER_POSITION_QUERY = /* GraphQL */ `
55
+ query OrderPosition($orderId: ID!) {
56
+ orderPaymentPosition(orderId: $orderId) {
57
+ total
58
+ covered
59
+ settled
60
+ authorized
61
+ refunded
62
+ outstanding
63
+ overpaid
64
+ availableToCharge
65
+ }
66
+ }
67
+ `;
68
+ export function createOrdersModule(transport) {
69
+ return {
70
+ surcharges: createSurchargesModule(transport),
71
+ async park(orderId, options) {
72
+ const data = await transport.graphql({
73
+ api: 'admin',
74
+ query: PARK_ORDER_MUTATION,
75
+ // `undefined`, never an explicit null: `JSON.stringify` drops it, so an
76
+ // omitted option stays off the wire entirely rather than arriving as a
77
+ // value a future server could read as one.
78
+ variables: {
79
+ orderId,
80
+ reason: options?.reason,
81
+ idempotencyKey: options?.idempotencyKey,
82
+ },
83
+ }, options);
84
+ return data.parkOrder;
85
+ },
86
+ async unpark(orderId, options) {
87
+ const data = await transport.graphql({
88
+ api: 'admin',
89
+ query: UNPARK_ORDER_MUTATION,
90
+ // `undefined`, never an explicit null — see `park`.
91
+ variables: { orderId, idempotencyKey: options?.idempotencyKey },
92
+ }, options);
93
+ return data.unparkOrder;
94
+ },
95
+ async paymentPosition(orderId, options) {
96
+ const data = await transport.graphql({ api: 'admin', query: ORDER_POSITION_QUERY, variables: { orderId } }, options);
97
+ return data.orderPaymentPosition;
98
+ },
99
+ async settle(orderId, options) {
100
+ const data = await transport.graphql({
101
+ api: 'admin',
102
+ query: SETTLE_ORDER_MUTATION,
103
+ variables: {
104
+ // Explicit `false` rather than omitted: the argument is non-null on
105
+ // the wire, and "did the caller ask for fulfilment?" is recorded on
106
+ // the settlement event.
107
+ orderId,
108
+ fulfil: options?.fulfil ?? false,
109
+ idempotencyKey: options?.idempotencyKey,
110
+ },
111
+ }, options);
112
+ return data.settleOrder;
113
+ },
114
+ };
115
+ }