@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.
- package/README.md +359 -2
- package/dist/apps/assertion.d.ts +30 -3
- package/dist/apps/assertion.js +40 -3
- package/dist/apps/config.d.ts +39 -0
- package/dist/apps/config.js +22 -0
- package/dist/apps/credential.d.ts +5 -0
- package/dist/apps/credential.js +55 -4
- package/dist/apps/index.d.ts +11 -0
- package/dist/client.d.ts +54 -1
- package/dist/client.js +34 -1
- package/dist/custom-objects/index.d.ts +66 -0
- package/dist/custom-objects/index.js +48 -0
- package/dist/customers/index.d.ts +225 -0
- package/dist/customers/index.js +279 -0
- package/dist/documents/index.d.ts +432 -0
- package/dist/documents/index.js +289 -0
- package/dist/index.d.ts +34 -2
- package/dist/index.js +16 -1
- package/dist/jobs/index.d.ts +143 -0
- package/dist/jobs/index.js +132 -0
- package/dist/metafields/index.d.ts +291 -0
- package/dist/metafields/index.js +141 -0
- package/dist/orders/index.d.ts +106 -0
- package/dist/orders/index.js +115 -0
- package/dist/orders/surcharges.d.ts +121 -0
- package/dist/orders/surcharges.js +110 -0
- package/dist/payments/index.d.ts +244 -0
- package/dist/payments/index.js +103 -0
- package/dist/payments/providers.d.ts +281 -0
- package/dist/payments/providers.js +205 -0
- package/dist/platform/provision-store.d.ts +9 -26
- package/dist/platform/provision-store.js +8 -66
- package/dist/tax/index.d.ts +128 -0
- package/dist/tax/index.js +101 -0
- package/dist/tenant/capabilities.d.ts +15 -0
- package/dist/types.d.ts +20 -3
- package/dist/uploads/index.d.ts +146 -0
- package/dist/uploads/index.js +72 -0
- 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
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
}
|