@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,281 @@
1
+ import { type Page, type Transport } from '@sixtyninecommerce/core';
2
+ import type { Call } from '../types.js';
3
+ /**
4
+ * Which credential a store is actually paying through.
5
+ *
6
+ * `connect` — an account the store onboarded through the platform.
7
+ * `legacy` — the channel's own per-provider API key, as it has always worked.
8
+ * Supported indefinitely: a store moves when it chooses to, and both paths
9
+ * can be fully ready.
10
+ * `none` — neither, so nothing can be charged.
11
+ */
12
+ export type ProviderCredentialPath = 'connect' | 'legacy' | 'none';
13
+ /** A payment provider's account, as the platform holds it for one seller. */
14
+ export interface ProviderConnection {
15
+ id: string;
16
+ /**
17
+ * The legal entity this account belongs to, resolved from the channel's
18
+ * seller. Several stores of one seller SHARE a connection — the money lands
19
+ * in one place and the merchant onboards once.
20
+ */
21
+ sellerId: string;
22
+ /** Provider key — `test`, `mollie`, `stripe`. */
23
+ provider: string;
24
+ /** `pending` while onboarding is in flight, then `connected` or `disconnected`. */
25
+ status: string;
26
+ /**
27
+ * The account's PUBLIC id at the provider. Null while pending. Never a
28
+ * secret: credentials stay with the provider package that obtained them.
29
+ */
30
+ externalId: string | null;
31
+ scopes: string[];
32
+ /** Payment methods active on the account, as the provider reports them. */
33
+ activeMethods: string[];
34
+ /**
35
+ * `test` or `live`, as the provider reported it when the account was
36
+ * connected. REPORTED, not enforced: the rule that a sandbox store may hold
37
+ * only a `test` connection needs a way to identify a sandbox store, which
38
+ * does not exist yet. The mode is recorded now so that rule has something to
39
+ * act on the day it lands.
40
+ */
41
+ mode: string;
42
+ connectedAt: string | null;
43
+ /**
44
+ * Set when the connection ended. The row STAYS — which account a store was
45
+ * paid through outlives the connection, and it is the first thing anyone asks
46
+ * after a dispute.
47
+ */
48
+ disconnectedAt: string | null;
49
+ createdAt: string;
50
+ updatedAt: string;
51
+ }
52
+ /** A card terminal registered against a connection. Terminals are per Location. */
53
+ export interface ProviderTerminal {
54
+ id: string;
55
+ /** The terminal's id at the provider — the identity a re-provision reuses. */
56
+ externalId: string;
57
+ label: string;
58
+ /** The stock location it serves, or null while it is unassigned. */
59
+ stockLocationId: string | null;
60
+ status: string;
61
+ lastSeenAt: string | null;
62
+ createdAt: string;
63
+ updatedAt: string;
64
+ }
65
+ /** Where to send the merchant, and the state to hand back on return. */
66
+ export interface ProviderOnboardingStart {
67
+ url: string;
68
+ /** Opaque and single-use. Pass it to {@link ProvidersModule.completeOnboarding}. */
69
+ state: string;
70
+ mode: string;
71
+ }
72
+ /**
73
+ * Independently verifiable evidence that this store can be paid through this
74
+ * provider — never a UI flag. Each signal is a row, the provider's own answer,
75
+ * a count, or one piece of arithmetic on the order.
76
+ */
77
+ export interface ProviderReadiness {
78
+ provider: string;
79
+ /** Which credential this verdict is about. Both `connect` and `legacy` can be ready. */
80
+ path: ProviderCredentialPath;
81
+ /** `test` or `live` when there is a connection; null on the legacy path. */
82
+ mode: string | null;
83
+ connected: boolean;
84
+ /**
85
+ * The software for this provider is loaded in the deployment you are talking
86
+ * to.
87
+ *
88
+ * False only for a store that IS connected through a provider package the
89
+ * deployment no longer runs. Nothing about the credential is known then, and
90
+ * it is not the merchant's to fix — show the blocker, do not offer a
91
+ * reconnect. A provider the server has never heard of is refused instead.
92
+ */
93
+ adapterAvailable: boolean;
94
+ /**
95
+ * The provider ANSWERED when the server asked about this account.
96
+ *
97
+ * False means the call did not come back — an outage, a timeout — so nothing
98
+ * else here about the credential was verified and `blockers` holds that one
99
+ * sentence. Retry; do NOT send the merchant to reconnect on it. Always true
100
+ * on the `legacy` path and with no credential, where there is no probe.
101
+ */
102
+ providerReachable: boolean;
103
+ /**
104
+ * On the `connect` path: the provider itself accepts the stored credential
105
+ * RIGHT NOW. On the `legacy` path it means only that the store holds a key on
106
+ * an enabled method — the server never reads that key here, so a legacy key
107
+ * the provider has since revoked still reads `true`.
108
+ */
109
+ credentialUsable: boolean;
110
+ methodActive: boolean;
111
+ terminalProvisioned: boolean;
112
+ /**
113
+ * A registered terminal serves the `locationId` you asked about — and `false`
114
+ * when you asked about none, since there is no location for it to be true of.
115
+ * Same for `orderPayable`. Neither produces a blocker or moves `overall`
116
+ * outside its own question, so do not render either as a problem unless you
117
+ * asked the question it answers.
118
+ */
119
+ terminalUsableForLocation: boolean;
120
+ /**
121
+ * The provider can take PART of an order's total. Reported, never gating: it
122
+ * answers your question (may I take a deposit), not whether the store can be
123
+ * paid at all.
124
+ */
125
+ partialSupported: boolean;
126
+ orderPayable: boolean;
127
+ /**
128
+ * The answer to "can this store be paid through this provider" FOR THE
129
+ * QUESTION ASKED. Signals outside it do not gate: a web-only store with no
130
+ * terminal is ready, because you did not ask about a location.
131
+ */
132
+ overall: boolean;
133
+ /**
134
+ * The server's evidence for anything short of a clean pass, one entry per
135
+ * failing gate, in the platform's own words. Never a sentence this SDK
136
+ * composed, and never a blocker for something you did not ask about.
137
+ */
138
+ blockers: string[];
139
+ }
140
+ export interface ProvisionTerminalInput {
141
+ /** What the merchant calls it — "Counter 1". At most 120 characters. */
142
+ label: string;
143
+ /** Assign it to this stock location as it is created. Must be in this channel. */
144
+ stockLocationId?: string | null;
145
+ }
146
+ export interface ReadinessOptions extends Call {
147
+ /** Ask about a specific order: `orderPayable` then gates the verdict. */
148
+ orderId?: string;
149
+ /** Ask about a counter sale: the terminal signals then gate the verdict. */
150
+ locationId?: string;
151
+ }
152
+ /** Terminals of one provider. */
153
+ export interface ProviderTerminalsModule {
154
+ /**
155
+ * The terminals this platform holds. A pure read of stored rows; see
156
+ * {@link refresh} for reconciling with the provider.
157
+ *
158
+ * A `Page` because every list in this SDK is one (spec §9.2), not because
159
+ * this query pages: it answers the complete array.
160
+ */
161
+ list(provider: string, options?: Call): Promise<Page<ProviderTerminal>>;
162
+ /**
163
+ * Reconcile with the provider: new readers are recorded, known ones updated,
164
+ * and one the provider no longer reports is marked `inactive` so it stops
165
+ * counting towards readiness. Rows are never deleted — a row is the evidence
166
+ * that a reader existed and where it stood.
167
+ *
168
+ * A WRITE (`UpdatePaymentMethod`), because it changes what the platform holds.
169
+ */
170
+ refresh(provider: string, options?: Call): Promise<Page<ProviderTerminal>>;
171
+ /**
172
+ * Ask the provider for a terminal.
173
+ *
174
+ * Provisioning the same reader twice is the SAME reader — the provider's id
175
+ * is the identity — so this is safe to retry even without a key. Send
176
+ * `idempotencyKey` to make the whole mutation replay instead.
177
+ */
178
+ provision(provider: string, input: ProvisionTerminalInput, options?: Call): Promise<ProviderTerminal>;
179
+ /** Put a terminal on a location. */
180
+ assign(terminalId: string, locationId: string, options?: Call): Promise<ProviderTerminal>;
181
+ /**
182
+ * Take a terminal off its location. The row stays: the reader still exists.
183
+ *
184
+ * This is the ONLY way to unassign through this SDK — `assign` requires a
185
+ * location id. (The underlying mutation accepts a null `locationId` too; the
186
+ * SDK keeps the two intents as two methods so neither call has a meaningless
187
+ * argument.)
188
+ */
189
+ unassign(terminalId: string, options?: Call): Promise<ProviderTerminal>;
190
+ }
191
+ /**
192
+ * Payment-provider onboarding, terminals and readiness (headless spec §8.9).
193
+ *
194
+ * ## A connection belongs to the SELLER, not to the store
195
+ *
196
+ * A PSP account belongs to the legal entity that gets paid, so several stores of
197
+ * one seller share one connection: onboard once, and the money lands in one
198
+ * place. A store whose channel has no seller is refused with `SELLER_REQUIRED`
199
+ * rather than given a connection keyed on nothing.
200
+ *
201
+ * ## Two credential paths, both first-class
202
+ *
203
+ * A store on a raw per-channel API key is supported, not broken — there is no
204
+ * migration, and it moves when it chooses to. {@link readiness} reports which
205
+ * path it is on and treats both equally in `overall`. The per-channel key
206
+ * surface is deprecated in the schema and supported in the code; those are
207
+ * different statements and both are deliberate.
208
+ *
209
+ * ## Readiness is evidence, not a flag
210
+ *
211
+ * Every signal is something you could verify yourself, and `blockers` are the
212
+ * server's words. Ask the question you are about to act on: pass `locationId`
213
+ * for a counter sale and `orderId` for a payment, and signals outside that
214
+ * question are reported without gating `overall` — a web-only store has no
215
+ * terminal and is not broken.
216
+ *
217
+ * **Coded refusals**, delivered as GraphQL errors with `extensions.code`:
218
+ * `SELLER_REQUIRED`, `PAYMENT_PROVIDER_NOT_REGISTERED`, `PROVIDER_NOT_CONNECTED`,
219
+ * `PROVIDER_ALREADY_CONNECTED` (→ `ConflictError`; disconnect first rather than
220
+ * silently re-pointing a store's money), `ONBOARDING_STATE_INVALID`,
221
+ * `PROVIDER_CREDENTIAL_REJECTED`, `TERMINAL_NOT_FOUND` (→ `NotFoundError`),
222
+ * `TERMINAL_REJECTED`, `LIVE_CONNECTION_REFUSED`, and the `INVALID_*` input
223
+ * refusals (→ `ValidationError`). Assigning a terminal to a location of another
224
+ * channel answers `LOCATION_NOT_IN_CHANNEL`, the same code the rest of the
225
+ * platform uses for that one rule.
226
+ */
227
+ export interface ProvidersModule {
228
+ /**
229
+ * Every connection of this store's seller, including disconnected ones: a
230
+ * seller that WAS connected and is not any more gives a different answer to
231
+ * one that never connected at all, and support needs to tell them apart.
232
+ */
233
+ list(options?: Call): Promise<Page<ProviderConnection>>;
234
+ /** One provider's connection, or null when this seller has never connected it. */
235
+ status(provider: string, options?: Call): Promise<ProviderConnection | null>;
236
+ /**
237
+ * Begin connecting this seller's account. Send the merchant to the `url`, and
238
+ * hand the `state` back to {@link completeOnboarding} when they return.
239
+ *
240
+ * Refused with `PROVIDER_ALREADY_CONNECTED` while a live connection exists.
241
+ *
242
+ * This is the one call here that takes NO `idempotencyKey`, and it needs
243
+ * none: a repeat replaces the pending attempt rather than creating a second
244
+ * account, and it answers with a FRESH url and state — which is what a
245
+ * merchant who lost the first link needs. Storing the answer for a day would
246
+ * re-serve a link that may be single-use and, once the onboarding is
247
+ * finished, one that can no longer be completed.
248
+ */
249
+ startOnboarding(provider: string, input: {
250
+ returnUrl: string;
251
+ }, options?: Call): Promise<ProviderOnboardingStart>;
252
+ /**
253
+ * Finish what {@link startOnboarding} began. The `state` is single-use, so a
254
+ * replayed callback cannot re-complete an onboarding.
255
+ */
256
+ completeOnboarding(provider: string, input: {
257
+ state: string;
258
+ code: string;
259
+ }, options?: Call): Promise<ProviderConnection>;
260
+ /** Activate one payment method on the connected account (`ideal`, `pointofsale`, …). */
261
+ activateMethod(provider: string, method: string, options?: Call): Promise<ProviderConnection>;
262
+ /** Can this store be paid through this provider, and if not, what is missing? */
263
+ readiness(provider: string, options?: ReadinessOptions): Promise<ProviderReadiness>;
264
+ /**
265
+ * End the connection at the provider. False when there was nothing to
266
+ * disconnect. The row stays, marked disconnected.
267
+ */
268
+ disconnect(provider: string, options?: Call): Promise<boolean>;
269
+ terminals: ProviderTerminalsModule;
270
+ }
271
+ export declare const PROVIDER_CONNECTIONS_QUERY = "\n query ProviderConnections {\n providerConnections { \n id\n sellerId\n provider\n status\n externalId\n scopes\n activeMethods\n mode\n connectedAt\n disconnectedAt\n createdAt\n updatedAt\n }\n }\n";
272
+ export declare const PROVIDER_READINESS_QUERY = "\n query PaymentProviderReadiness($provider: String!, $orderId: ID, $locationId: ID) {\n paymentProviderReadiness(provider: $provider, orderId: $orderId, locationId: $locationId) {\n \n provider\n path\n mode\n connected\n adapterAvailable\n providerReachable\n credentialUsable\n methodActive\n terminalProvisioned\n terminalUsableForLocation\n partialSupported\n orderPayable\n overall\n blockers\n\n }\n }\n";
273
+ export declare const PROVIDER_TERMINALS_QUERY = "\n query ProviderTerminals($provider: String!) {\n providerTerminals(provider: $provider) { \n id\n externalId\n label\n stockLocationId\n status\n lastSeenAt\n createdAt\n updatedAt\n }\n }\n";
274
+ export declare const START_ONBOARDING_MUTATION = "\n mutation StartPaymentProviderOnboarding($provider: String!, $returnUrl: String!) {\n startPaymentProviderOnboarding(provider: $provider, returnUrl: $returnUrl) {\n url\n state\n mode\n }\n }\n";
275
+ export declare const COMPLETE_ONBOARDING_MUTATION = "\n mutation CompletePaymentProviderOnboarding($provider: String!, $state: String!, $code: String!, $idempotencyKey: String) {\n completePaymentProviderOnboarding(provider: $provider, state: $state, code: $code, idempotencyKey: $idempotencyKey) {\n \n id\n sellerId\n provider\n status\n externalId\n scopes\n activeMethods\n mode\n connectedAt\n disconnectedAt\n createdAt\n updatedAt\n\n }\n }\n";
276
+ export declare const ACTIVATE_METHOD_MUTATION = "\n mutation ActivatePaymentProviderMethod($provider: String!, $method: String!) {\n activatePaymentProviderMethod(provider: $provider, method: $method) { \n id\n sellerId\n provider\n status\n externalId\n scopes\n activeMethods\n mode\n connectedAt\n disconnectedAt\n createdAt\n updatedAt\n }\n }\n";
277
+ export declare const REFRESH_TERMINALS_MUTATION = "\n mutation RefreshProviderTerminals($provider: String!) {\n refreshProviderTerminals(provider: $provider) { \n id\n externalId\n label\n stockLocationId\n status\n lastSeenAt\n createdAt\n updatedAt\n }\n }\n";
278
+ export declare const PROVISION_TERMINAL_MUTATION = "\n mutation ProvisionProviderTerminal($provider: String!, $input: ProvisionTerminalInput!, $idempotencyKey: String) {\n provisionProviderTerminal(provider: $provider, input: $input, idempotencyKey: $idempotencyKey) {\n \n id\n externalId\n label\n stockLocationId\n status\n lastSeenAt\n createdAt\n updatedAt\n\n }\n }\n";
279
+ export declare const ASSIGN_TERMINAL_MUTATION = "\n mutation AssignProviderTerminal($terminalId: ID!, $locationId: ID) {\n assignProviderTerminal(terminalId: $terminalId, locationId: $locationId) { \n id\n externalId\n label\n stockLocationId\n status\n lastSeenAt\n createdAt\n updatedAt\n }\n }\n";
280
+ export declare const DISCONNECT_PROVIDER_MUTATION = "\n mutation DisconnectPaymentProvider($provider: String!) {\n disconnectPaymentProvider(provider: $provider)\n }\n";
281
+ export declare function createProvidersModule(transport: Transport): ProvidersModule;
@@ -0,0 +1,205 @@
1
+ import { createPage } from '@sixtyninecommerce/core';
2
+ const CONNECTION_FIELDS = /* GraphQL */ `
3
+ id
4
+ sellerId
5
+ provider
6
+ status
7
+ externalId
8
+ scopes
9
+ activeMethods
10
+ mode
11
+ connectedAt
12
+ disconnectedAt
13
+ createdAt
14
+ updatedAt
15
+ `;
16
+ const TERMINAL_FIELDS = /* GraphQL */ `
17
+ id
18
+ externalId
19
+ label
20
+ stockLocationId
21
+ status
22
+ lastSeenAt
23
+ createdAt
24
+ updatedAt
25
+ `;
26
+ const READINESS_FIELDS = /* GraphQL */ `
27
+ provider
28
+ path
29
+ mode
30
+ connected
31
+ adapterAvailable
32
+ providerReachable
33
+ credentialUsable
34
+ methodActive
35
+ terminalProvisioned
36
+ terminalUsableForLocation
37
+ partialSupported
38
+ orderPayable
39
+ overall
40
+ blockers
41
+ `;
42
+ export const PROVIDER_CONNECTIONS_QUERY = /* GraphQL */ `
43
+ query ProviderConnections {
44
+ providerConnections { ${CONNECTION_FIELDS} }
45
+ }
46
+ `;
47
+ export const PROVIDER_READINESS_QUERY = /* GraphQL */ `
48
+ query PaymentProviderReadiness($provider: String!, $orderId: ID, $locationId: ID) {
49
+ paymentProviderReadiness(provider: $provider, orderId: $orderId, locationId: $locationId) {
50
+ ${READINESS_FIELDS}
51
+ }
52
+ }
53
+ `;
54
+ export const PROVIDER_TERMINALS_QUERY = /* GraphQL */ `
55
+ query ProviderTerminals($provider: String!) {
56
+ providerTerminals(provider: $provider) { ${TERMINAL_FIELDS} }
57
+ }
58
+ `;
59
+ export const START_ONBOARDING_MUTATION = /* GraphQL */ `
60
+ mutation StartPaymentProviderOnboarding($provider: String!, $returnUrl: String!) {
61
+ startPaymentProviderOnboarding(provider: $provider, returnUrl: $returnUrl) {
62
+ url
63
+ state
64
+ mode
65
+ }
66
+ }
67
+ `;
68
+ export const COMPLETE_ONBOARDING_MUTATION = /* GraphQL */ `
69
+ mutation CompletePaymentProviderOnboarding($provider: String!, $state: String!, $code: String!, $idempotencyKey: String) {
70
+ completePaymentProviderOnboarding(provider: $provider, state: $state, code: $code, idempotencyKey: $idempotencyKey) {
71
+ ${CONNECTION_FIELDS}
72
+ }
73
+ }
74
+ `;
75
+ export const ACTIVATE_METHOD_MUTATION = /* GraphQL */ `
76
+ mutation ActivatePaymentProviderMethod($provider: String!, $method: String!) {
77
+ activatePaymentProviderMethod(provider: $provider, method: $method) { ${CONNECTION_FIELDS} }
78
+ }
79
+ `;
80
+ export const REFRESH_TERMINALS_MUTATION = /* GraphQL */ `
81
+ mutation RefreshProviderTerminals($provider: String!) {
82
+ refreshProviderTerminals(provider: $provider) { ${TERMINAL_FIELDS} }
83
+ }
84
+ `;
85
+ export const PROVISION_TERMINAL_MUTATION = /* GraphQL */ `
86
+ mutation ProvisionProviderTerminal($provider: String!, $input: ProvisionTerminalInput!, $idempotencyKey: String) {
87
+ provisionProviderTerminal(provider: $provider, input: $input, idempotencyKey: $idempotencyKey) {
88
+ ${TERMINAL_FIELDS}
89
+ }
90
+ }
91
+ `;
92
+ export const ASSIGN_TERMINAL_MUTATION = /* GraphQL */ `
93
+ mutation AssignProviderTerminal($terminalId: ID!, $locationId: ID) {
94
+ assignProviderTerminal(terminalId: $terminalId, locationId: $locationId) { ${TERMINAL_FIELDS} }
95
+ }
96
+ `;
97
+ export const DISCONNECT_PROVIDER_MUTATION = /* GraphQL */ `
98
+ mutation DisconnectPaymentProvider($provider: String!) {
99
+ disconnectPaymentProvider(provider: $provider)
100
+ }
101
+ `;
102
+ /** A complete array behind the SDK's page shape (spec §9.2). */
103
+ function wholeList(items, what) {
104
+ return createPage(items, { nextCursor: null, hasNext: false, total: items.length }, () => {
105
+ throw new Error(`${what} returns a complete list; there is no next page`);
106
+ });
107
+ }
108
+ export function createProvidersModule(transport) {
109
+ // Hoisted rather than reached through `this`: `const { status } =
110
+ // admin.payments.providers` is a thing consumers do, and a method that needs
111
+ // its receiver would throw for them. No other module in this SDK uses `this`.
112
+ const listConnections = async (options) => {
113
+ const data = await transport.graphql({ api: 'admin', query: PROVIDER_CONNECTIONS_QUERY }, options);
114
+ return wholeList(data.providerConnections, 'providerConnections');
115
+ };
116
+ return {
117
+ list: listConnections,
118
+ async status(provider, options) {
119
+ // Filtered client-side from the complete list rather than added as a
120
+ // second query: the list is small (one row per provider per seller), and
121
+ // one server surface is one thing to keep correct.
122
+ const page = await listConnections(options);
123
+ return page.items.find((row) => row.provider === provider) ?? null;
124
+ },
125
+ async startOnboarding(provider, input, options) {
126
+ const data = await transport.graphql({
127
+ api: 'admin',
128
+ query: START_ONBOARDING_MUTATION,
129
+ variables: { provider, returnUrl: input.returnUrl },
130
+ }, options);
131
+ return data.startPaymentProviderOnboarding;
132
+ },
133
+ async completeOnboarding(provider, input, options) {
134
+ const data = await transport.graphql({
135
+ api: 'admin',
136
+ query: COMPLETE_ONBOARDING_MUTATION,
137
+ variables: {
138
+ provider,
139
+ state: input.state,
140
+ code: input.code,
141
+ idempotencyKey: options?.idempotencyKey ?? null,
142
+ },
143
+ }, options);
144
+ return data.completePaymentProviderOnboarding;
145
+ },
146
+ async activateMethod(provider, method, options) {
147
+ const data = await transport.graphql({ api: 'admin', query: ACTIVATE_METHOD_MUTATION, variables: { provider, method } }, options);
148
+ return data.activatePaymentProviderMethod;
149
+ },
150
+ async readiness(provider, options) {
151
+ const data = await transport.graphql({
152
+ api: 'admin',
153
+ query: PROVIDER_READINESS_QUERY,
154
+ variables: {
155
+ provider,
156
+ orderId: options?.orderId ?? null,
157
+ locationId: options?.locationId ?? null,
158
+ },
159
+ }, options);
160
+ return data.paymentProviderReadiness;
161
+ },
162
+ async disconnect(provider, options) {
163
+ const data = await transport.graphql({ api: 'admin', query: DISCONNECT_PROVIDER_MUTATION, variables: { provider } }, options);
164
+ return data.disconnectPaymentProvider;
165
+ },
166
+ terminals: {
167
+ async list(provider, options) {
168
+ const data = await transport.graphql({ api: 'admin', query: PROVIDER_TERMINALS_QUERY, variables: { provider } }, options);
169
+ return wholeList(data.providerTerminals, 'providerTerminals');
170
+ },
171
+ async refresh(provider, options) {
172
+ const data = await transport.graphql({ api: 'admin', query: REFRESH_TERMINALS_MUTATION, variables: { provider } }, options);
173
+ return wholeList(data.refreshProviderTerminals, 'refreshProviderTerminals');
174
+ },
175
+ async provision(provider, input, options) {
176
+ const data = await transport.graphql({
177
+ api: 'admin',
178
+ query: PROVISION_TERMINAL_MUTATION,
179
+ variables: {
180
+ provider,
181
+ input: { label: input.label, stockLocationId: input.stockLocationId ?? null },
182
+ idempotencyKey: options?.idempotencyKey ?? null,
183
+ },
184
+ }, options);
185
+ return data.provisionProviderTerminal;
186
+ },
187
+ async assign(terminalId, locationId, options) {
188
+ const data = await transport.graphql({ api: 'admin', query: ASSIGN_TERMINAL_MUTATION, variables: { terminalId, locationId } }, options);
189
+ return data.assignProviderTerminal;
190
+ },
191
+ async unassign(terminalId, options) {
192
+ const data = await transport.graphql({
193
+ api: 'admin',
194
+ query: ASSIGN_TERMINAL_MUTATION,
195
+ // Explicit null rather than an omitted variable. The server reads
196
+ // BOTH as "take it off the counter" — there is no "leave it"
197
+ // spelling — so this is about the request being self-describing in
198
+ // a proxy log, not about changing what the server does.
199
+ variables: { terminalId, locationId: null },
200
+ }, options);
201
+ return data.assignProviderTerminal;
202
+ },
203
+ },
204
+ };
205
+ }
@@ -1,6 +1,15 @@
1
1
  import { type Transport } from '@sixtyninecommerce/core';
2
2
  import type { AdminClientConfig } from '../client.js';
3
+ import { type JobHandle } from '../jobs/index.js';
3
4
  import type { Call } from '../types.js';
5
+ /**
6
+ * `JobHandle` and `WaitOptions` LIVE in `../jobs` now — every long-running
7
+ * operation returns one, not just this resource (spec §6.11). They are
8
+ * re-exported here so that every consumer written against
9
+ * `platform/provision-store` keeps compiling: the type is the same type, not a
10
+ * copy.
11
+ */
12
+ export type { JobHandle, WaitOptions } from '../jobs/index.js';
4
13
  /**
5
14
  * Which objects a new store is provisioned with.
6
15
  *
@@ -108,32 +117,6 @@ export interface ProvisionRun {
108
117
  completedAt: string | null;
109
118
  [extra: string]: unknown;
110
119
  }
111
- export interface WaitOptions {
112
- /** How often to poll. Default 2000 ms; anything below 250 ms is raised to it. */
113
- intervalMs?: number;
114
- /** Give up after this long. Default 300000 ms (5 minutes). */
115
- timeoutMs?: number;
116
- signal?: AbortSignal;
117
- }
118
- /**
119
- * A long-running operation in flight (spec §6.11). `runId` is the receipt: it
120
- * outlives this process, so a caller that cannot wait can hand it on and poll
121
- * `provisionRun(runId)` later.
122
- */
123
- export interface JobHandle<T> {
124
- runId: string;
125
- /**
126
- * Poll until the run reaches a terminal status.
127
- *
128
- * Resolves with the result on success. Throws `PROVISION_RUN_COMPENSATED`
129
- * when the run failed and the platform undid everything it had built (no
130
- * store, no leftovers — retrying is safe), and `PROVISION_RUN_FAILED` when
131
- * the cleanup ITSELF could not finish, whose message names what is left
132
- * behind for an operator. `PROVISION_RUN_TIMEOUT` means only that waiting
133
- * stopped: the run is still going, and {@link runId} still resolves it.
134
- */
135
- wait(options?: WaitOptions): Promise<T>;
136
- }
137
120
  export interface PlatformModule {
138
121
  /**
139
122
  * Build a whole store: seller, channel, zones, shipping methods, stock
@@ -1,4 +1,5 @@
1
1
  import { BusinessRuleError, TransportError } from '@sixtyninecommerce/core';
2
+ import { pollUntilTerminal } from '../jobs/index.js';
2
3
  import { requireHumanSession } from './guard.js';
3
4
  /** Every profile, for a caller that renders a picker. */
4
5
  export const PROVISION_PROFILES = [
@@ -9,22 +10,6 @@ export const PROVISION_PROFILES = [
9
10
  'full',
10
11
  ];
11
12
  const TERMINAL = new Set(['succeeded', 'failed', 'compensated']);
12
- const DEFAULT_INTERVAL_MS = 2_000;
13
- const MIN_INTERVAL_MS = 250;
14
- const DEFAULT_TIMEOUT_MS = 300_000;
15
- /**
16
- * Consecutive failed READS tolerated while waiting. A poll is a read of a run
17
- * that is still building a store: one 503 from a rolling restart must not
18
- * report a failure for work that is going fine. Reset by any successful read,
19
- * so only a sustained outage gives up — and a refusal that is ABOUT the run
20
- * (not found, no permission) is not retried at all.
21
- */
22
- const MAX_CONSECUTIVE_POLL_FAILURES = 3;
23
- /** A read that failed for a reason polling again cannot fix. */
24
- function isTerminalPollError(error) {
25
- const status = error?.status;
26
- return typeof status === 'number' && status >= 400 && status < 500 && status !== 429;
27
- }
28
13
  export function createPlatformModule(transport, config) {
29
14
  async function provisionRun(runId, options) {
30
15
  requireHumanSession(config);
@@ -42,39 +27,13 @@ export function createPlatformModule(transport, config) {
42
27
  return {
43
28
  runId,
44
29
  async wait(waitOptions) {
45
- const intervalMs = Math.max(waitOptions?.intervalMs ?? DEFAULT_INTERVAL_MS, MIN_INTERVAL_MS);
46
- const deadline = Date.now() + (waitOptions?.timeoutMs ?? DEFAULT_TIMEOUT_MS);
47
- let consecutiveFailures = 0;
48
- let lastSeen;
49
- for (;;) {
50
- let run;
51
- try {
52
- run = await provisionRun(runId, {
53
- ...options,
54
- ...(waitOptions?.signal ? { signal: waitOptions.signal } : {}),
55
- });
56
- consecutiveFailures = 0;
57
- }
58
- catch (error) {
59
- if (waitOptions?.signal?.aborted === true)
60
- throw error;
61
- consecutiveFailures += 1;
62
- if (isTerminalPollError(error) ||
63
- consecutiveFailures > MAX_CONSECUTIVE_POLL_FAILURES) {
64
- throw error;
65
- }
66
- if (Date.now() >= deadline)
67
- throw timedOut(runId, lastSeen);
68
- await delay(intervalMs, waitOptions?.signal);
69
- continue;
70
- }
71
- lastSeen = run;
72
- if (TERMINAL.has(run.status))
73
- return resultOf(run);
74
- if (Date.now() >= deadline)
75
- throw timedOut(runId, run);
76
- await delay(intervalMs, waitOptions?.signal);
77
- }
30
+ const run = await pollUntilTerminal({
31
+ read: (signal) => provisionRun(runId, { ...options, ...(signal ? { signal } : {}) }),
32
+ isTerminal: (candidate) => TERMINAL.has(candidate.status),
33
+ onTimeout: (lastSeen) => timedOut(runId, lastSeen),
34
+ ...waitOptions,
35
+ });
36
+ return resultOf(run);
78
37
  },
79
38
  };
80
39
  },
@@ -121,20 +80,3 @@ function timedOut(runId, run) {
121
80
  retryable: true,
122
81
  });
123
82
  }
124
- function delay(ms, signal) {
125
- return new Promise((resolve, reject) => {
126
- if (signal?.aborted) {
127
- reject(signal.reason instanceof Error ? signal.reason : new Error('Aborted'));
128
- return;
129
- }
130
- const timer = setTimeout(() => {
131
- signal?.removeEventListener('abort', onAbort);
132
- resolve();
133
- }, ms);
134
- function onAbort() {
135
- clearTimeout(timer);
136
- reject(signal?.reason instanceof Error ? signal.reason : new Error('Aborted'));
137
- }
138
- signal?.addEventListener('abort', onAbort, { once: true });
139
- });
140
- }