@sixtyninecommerce/admin 0.6.0 → 0.7.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 +103 -0
- package/dist/payments/index.js +55 -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,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
|
-
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { type Page, type Transport } from '@sixtyninecommerce/core';
|
|
2
|
+
import type { Call } from '../types.js';
|
|
3
|
+
/**
|
|
4
|
+
* The mapping used when no mapping names the country the money is taxed in.
|
|
5
|
+
*
|
|
6
|
+
* A real row, not an absence: it is set and removed through the same two
|
|
7
|
+
* methods as any country's, so "this class says nothing about Germany" and
|
|
8
|
+
* "this class charges Germany the fallback" are different states rather than
|
|
9
|
+
* the same one read two ways.
|
|
10
|
+
*/
|
|
11
|
+
export declare const TAX_CLASS_FALLBACK_COUNTRY = "*";
|
|
12
|
+
/** As much of a Vendure tax category as a mapping projects. */
|
|
13
|
+
export interface TaxClassTaxCategory {
|
|
14
|
+
id: string;
|
|
15
|
+
name: string;
|
|
16
|
+
/** True for a category the store's own tax setup treats as the default. */
|
|
17
|
+
isDefault: boolean;
|
|
18
|
+
}
|
|
19
|
+
/** One country's answer for one class. */
|
|
20
|
+
export interface TaxClassMapping {
|
|
21
|
+
/** ISO 3166-1 alpha-2, upper case — or `*`, the fallback. */
|
|
22
|
+
countryCode: string;
|
|
23
|
+
taxCategoryId: string;
|
|
24
|
+
taxCategory: TaxClassTaxCategory;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* A store's own name for how something is taxed — `standard`, `reduced`,
|
|
28
|
+
* `zero-rated` (spec §8.6).
|
|
29
|
+
*
|
|
30
|
+
* Tax categories and their rates are platform-wide, shared by every store on
|
|
31
|
+
* the platform. The class is this store's indirection in front of them: the
|
|
32
|
+
* store owns the key and the label, and the mappings say which global category
|
|
33
|
+
* prices it in which country.
|
|
34
|
+
*/
|
|
35
|
+
export interface TaxClass {
|
|
36
|
+
id: string;
|
|
37
|
+
/**
|
|
38
|
+
* Kebab-case handle, unique in this store. The identity: it is what
|
|
39
|
+
* `ProductVariant.customFields.taxClassKey` carries, what a surcharge names,
|
|
40
|
+
* and what `tax_class.changed` puts on the event bus.
|
|
41
|
+
*/
|
|
42
|
+
key: string;
|
|
43
|
+
label: string;
|
|
44
|
+
mappings: TaxClassMapping[];
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Create a class, or re-label the one that already carries the key.
|
|
48
|
+
*
|
|
49
|
+
* There is NO rename. The key is stored on product variants and held by every
|
|
50
|
+
* consumer that has seen a `tax_class.changed`, so a merchant who wants a
|
|
51
|
+
* different key creates one and deletes the old — in that order, because the
|
|
52
|
+
* delete is refused while variants still carry it.
|
|
53
|
+
*/
|
|
54
|
+
export interface UpsertTaxClassInput {
|
|
55
|
+
/** Kebab-case, 1–64 characters (`INVALID_TAX_CLASS_KEY` otherwise). */
|
|
56
|
+
key: string;
|
|
57
|
+
label: string;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* The store's tax classes and the country mappings that price them (spec §8.6).
|
|
61
|
+
*
|
|
62
|
+
* Reading needs `ReadSettings`, writing `UpdateSettings` — deliberately NOT the
|
|
63
|
+
* tax-category permissions, which govern the platform-wide table every store
|
|
64
|
+
* shares. Everything is scoped to the channel the client is on: another store's
|
|
65
|
+
* class is invisible to `get`, unwritable by `mappings.set`, and reported as
|
|
66
|
+
* absent by `delete`.
|
|
67
|
+
*
|
|
68
|
+
* **Coded refusals**, delivered as GraphQL errors with `extensions.code`:
|
|
69
|
+
* `TAX_CLASS_NOT_FOUND` (→ `NotFoundError`), which is also the answer for a
|
|
70
|
+
* class belonging to another store; `TAX_CLASS_KEY_TAKEN` (→ `ConflictError`),
|
|
71
|
+
* which a caller meets only by losing a race with a concurrent create;
|
|
72
|
+
* `TAX_CLASS_IN_USE` (→ `ConflictError`) on a delete while product variants
|
|
73
|
+
* still carry the key; `TAX_CLASS_UNMAPPED` (→ `ValidationError`) when
|
|
74
|
+
* something asks a class to price money in a country it says nothing about, and
|
|
75
|
+
* `TAX_CLASS_RATE_MISSING` (→ `ValidationError`) when it IS mapped but that
|
|
76
|
+
* category has no rate in the zone the money is taxed in;
|
|
77
|
+
* `TAX_CATEGORY_NOT_FOUND` (→ `NotFoundError`); and
|
|
78
|
+
* `INVALID_TAX_CLASS_KEY` / `INVALID_TAX_CLASS_LABEL` /
|
|
79
|
+
* `INVALID_TAX_CLASS_COUNTRY` (→ `ValidationError`).
|
|
80
|
+
*/
|
|
81
|
+
export interface TaxClassesModule {
|
|
82
|
+
/**
|
|
83
|
+
* Every class of this store, by key.
|
|
84
|
+
*
|
|
85
|
+
* A `Page` because every list in this SDK is one (spec §9.2), not because
|
|
86
|
+
* this query pages: `taxClasses` answers the complete array, so the page is
|
|
87
|
+
* always the last one and `total` is its length. If the endpoint ever grows a
|
|
88
|
+
* cursor, consumers walking it with `.next()` or `for await` keep working.
|
|
89
|
+
*/
|
|
90
|
+
list(options?: Call): Promise<Page<TaxClass>>;
|
|
91
|
+
/**
|
|
92
|
+
* One class, or null when this store has none with that key — including a key
|
|
93
|
+
* that belongs to another store. A key that is not kebab-case at all is a
|
|
94
|
+
* different answer: it is refused with `INVALID_TAX_CLASS_KEY` rather than
|
|
95
|
+
* reported as absent.
|
|
96
|
+
*/
|
|
97
|
+
get(key: string, options?: Call): Promise<TaxClass | null>;
|
|
98
|
+
upsert(input: UpsertTaxClassInput, options?: Call): Promise<TaxClass>;
|
|
99
|
+
/**
|
|
100
|
+
* True when a class was removed, false when there was none — so a repeated
|
|
101
|
+
* delete is not an error. A malformed key is still refused with
|
|
102
|
+
* `INVALID_TAX_CLASS_KEY`, and a class product variants still carry with
|
|
103
|
+
* `TAX_CLASS_IN_USE`.
|
|
104
|
+
*/
|
|
105
|
+
delete(key: string, options?: Call): Promise<boolean>;
|
|
106
|
+
}
|
|
107
|
+
/** The country → tax category table of one class. */
|
|
108
|
+
export interface TaxMappingsModule {
|
|
109
|
+
/**
|
|
110
|
+
* Point one country — or {@link TAX_CLASS_FALLBACK_COUNTRY} — at a tax
|
|
111
|
+
* category, replacing that country's mapping if it had one. Country codes are
|
|
112
|
+
* stored upper case; `nl` and `NL` are the same mapping.
|
|
113
|
+
*/
|
|
114
|
+
set(key: string, countryCode: string, taxCategoryId: string, options?: Call): Promise<TaxClass>;
|
|
115
|
+
/** Drop one country's mapping. Silent when there was none. */
|
|
116
|
+
remove(key: string, countryCode: string, options?: Call): Promise<TaxClass>;
|
|
117
|
+
}
|
|
118
|
+
export interface TaxModule {
|
|
119
|
+
classes: TaxClassesModule;
|
|
120
|
+
mappings: TaxMappingsModule;
|
|
121
|
+
}
|
|
122
|
+
export declare const TAX_CLASSES_QUERY = "\n query TaxClasses {\n taxClasses { \n id\n key\n label\n mappings {\n countryCode\n taxCategoryId\n taxCategory {\n id\n name\n isDefault\n }\n }\n }\n }\n";
|
|
123
|
+
export declare const TAX_CLASS_QUERY = "\n query TaxClass($key: String!) {\n taxClass(key: $key) { \n id\n key\n label\n mappings {\n countryCode\n taxCategoryId\n taxCategory {\n id\n name\n isDefault\n }\n }\n }\n }\n";
|
|
124
|
+
export declare const UPSERT_TAX_CLASS_MUTATION = "\n mutation UpsertTaxClass($input: UpsertTaxClassInput!) {\n upsertTaxClass(input: $input) { \n id\n key\n label\n mappings {\n countryCode\n taxCategoryId\n taxCategory {\n id\n name\n isDefault\n }\n }\n }\n }\n";
|
|
125
|
+
export declare const DELETE_TAX_CLASS_MUTATION = "\n mutation DeleteTaxClass($key: String!) {\n deleteTaxClass(key: $key)\n }\n";
|
|
126
|
+
export declare const SET_TAX_CLASS_MAPPING_MUTATION = "\n mutation SetTaxClassMapping($key: String!, $countryCode: String!, $taxCategoryId: ID!) {\n setTaxClassMapping(key: $key, countryCode: $countryCode, taxCategoryId: $taxCategoryId) {\n \n id\n key\n label\n mappings {\n countryCode\n taxCategoryId\n taxCategory {\n id\n name\n isDefault\n }\n }\n\n }\n }\n";
|
|
127
|
+
export declare const REMOVE_TAX_CLASS_MAPPING_MUTATION = "\n mutation RemoveTaxClassMapping($key: String!, $countryCode: String!) {\n removeTaxClassMapping(key: $key, countryCode: $countryCode) { \n id\n key\n label\n mappings {\n countryCode\n taxCategoryId\n taxCategory {\n id\n name\n isDefault\n }\n }\n }\n }\n";
|
|
128
|
+
export declare function createTaxModule(transport: Transport): TaxModule;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { createPage } from '@sixtyninecommerce/core';
|
|
2
|
+
/**
|
|
3
|
+
* The mapping used when no mapping names the country the money is taxed in.
|
|
4
|
+
*
|
|
5
|
+
* A real row, not an absence: it is set and removed through the same two
|
|
6
|
+
* methods as any country's, so "this class says nothing about Germany" and
|
|
7
|
+
* "this class charges Germany the fallback" are different states rather than
|
|
8
|
+
* the same one read two ways.
|
|
9
|
+
*/
|
|
10
|
+
export const TAX_CLASS_FALLBACK_COUNTRY = '*';
|
|
11
|
+
const TAX_CLASS_FIELDS = /* GraphQL */ `
|
|
12
|
+
id
|
|
13
|
+
key
|
|
14
|
+
label
|
|
15
|
+
mappings {
|
|
16
|
+
countryCode
|
|
17
|
+
taxCategoryId
|
|
18
|
+
taxCategory {
|
|
19
|
+
id
|
|
20
|
+
name
|
|
21
|
+
isDefault
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
`;
|
|
25
|
+
export const TAX_CLASSES_QUERY = /* GraphQL */ `
|
|
26
|
+
query TaxClasses {
|
|
27
|
+
taxClasses { ${TAX_CLASS_FIELDS} }
|
|
28
|
+
}
|
|
29
|
+
`;
|
|
30
|
+
export const TAX_CLASS_QUERY = /* GraphQL */ `
|
|
31
|
+
query TaxClass($key: String!) {
|
|
32
|
+
taxClass(key: $key) { ${TAX_CLASS_FIELDS} }
|
|
33
|
+
}
|
|
34
|
+
`;
|
|
35
|
+
export const UPSERT_TAX_CLASS_MUTATION = /* GraphQL */ `
|
|
36
|
+
mutation UpsertTaxClass($input: UpsertTaxClassInput!) {
|
|
37
|
+
upsertTaxClass(input: $input) { ${TAX_CLASS_FIELDS} }
|
|
38
|
+
}
|
|
39
|
+
`;
|
|
40
|
+
export const DELETE_TAX_CLASS_MUTATION = /* GraphQL */ `
|
|
41
|
+
mutation DeleteTaxClass($key: String!) {
|
|
42
|
+
deleteTaxClass(key: $key)
|
|
43
|
+
}
|
|
44
|
+
`;
|
|
45
|
+
export const SET_TAX_CLASS_MAPPING_MUTATION = /* GraphQL */ `
|
|
46
|
+
mutation SetTaxClassMapping($key: String!, $countryCode: String!, $taxCategoryId: ID!) {
|
|
47
|
+
setTaxClassMapping(key: $key, countryCode: $countryCode, taxCategoryId: $taxCategoryId) {
|
|
48
|
+
${TAX_CLASS_FIELDS}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
`;
|
|
52
|
+
export const REMOVE_TAX_CLASS_MAPPING_MUTATION = /* GraphQL */ `
|
|
53
|
+
mutation RemoveTaxClassMapping($key: String!, $countryCode: String!) {
|
|
54
|
+
removeTaxClassMapping(key: $key, countryCode: $countryCode) { ${TAX_CLASS_FIELDS} }
|
|
55
|
+
}
|
|
56
|
+
`;
|
|
57
|
+
export function createTaxModule(transport) {
|
|
58
|
+
return {
|
|
59
|
+
classes: {
|
|
60
|
+
async list(options) {
|
|
61
|
+
const data = await transport.graphql({ api: 'admin', query: TAX_CLASSES_QUERY }, options);
|
|
62
|
+
const items = data.taxClasses;
|
|
63
|
+
return createPage(items, { nextCursor: null, hasNext: false, total: items.length }, () => {
|
|
64
|
+
// Unreachable while `hasNext` is false — `Page.next()` short-circuits
|
|
65
|
+
// — and a rejection rather than a silent re-fetch if that changes.
|
|
66
|
+
throw new Error('taxClasses returns a complete list; there is no next page');
|
|
67
|
+
});
|
|
68
|
+
},
|
|
69
|
+
async get(key, options) {
|
|
70
|
+
const data = await transport.graphql({ api: 'admin', query: TAX_CLASS_QUERY, variables: { key } }, options);
|
|
71
|
+
return data.taxClass;
|
|
72
|
+
},
|
|
73
|
+
async upsert(input, options) {
|
|
74
|
+
const data = await transport.graphql({ api: 'admin', query: UPSERT_TAX_CLASS_MUTATION, variables: { input } }, options);
|
|
75
|
+
return data.upsertTaxClass;
|
|
76
|
+
},
|
|
77
|
+
async delete(key, options) {
|
|
78
|
+
const data = await transport.graphql({ api: 'admin', query: DELETE_TAX_CLASS_MUTATION, variables: { key } }, options);
|
|
79
|
+
return data.deleteTaxClass;
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
mappings: {
|
|
83
|
+
async set(key, countryCode, taxCategoryId, options) {
|
|
84
|
+
const data = await transport.graphql({
|
|
85
|
+
api: 'admin',
|
|
86
|
+
query: SET_TAX_CLASS_MAPPING_MUTATION,
|
|
87
|
+
variables: { key, countryCode, taxCategoryId },
|
|
88
|
+
}, options);
|
|
89
|
+
return data.setTaxClassMapping;
|
|
90
|
+
},
|
|
91
|
+
async remove(key, countryCode, options) {
|
|
92
|
+
const data = await transport.graphql({
|
|
93
|
+
api: 'admin',
|
|
94
|
+
query: REMOVE_TAX_CLASS_MAPPING_MUTATION,
|
|
95
|
+
variables: { key, countryCode },
|
|
96
|
+
}, options);
|
|
97
|
+
return data.removeTaxClassMapping;
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
}
|
|
@@ -68,6 +68,21 @@ export interface CapabilityManifest {
|
|
|
68
68
|
* apps work has not written.
|
|
69
69
|
*/
|
|
70
70
|
installedApps: never[];
|
|
71
|
+
/**
|
|
72
|
+
* The `aud` an app assertion must carry to be exchanged at this deployment —
|
|
73
|
+
* its public Vendure origin. Copy it VERBATIM into the claim; re-deriving it
|
|
74
|
+
* from a URL you hold is the drift the platform compares against as a plain
|
|
75
|
+
* string. `createAppCredential` already does this for you, so this member is
|
|
76
|
+
* for a caller signing assertions by hand.
|
|
77
|
+
*
|
|
78
|
+
* Camelised from the wire's `assertion_audience`, like every other member
|
|
79
|
+
* here. `null` means the plugins service cannot state one — which does NOT
|
|
80
|
+
* mean the exchange accepts an assertion without `aud`, since the two run as
|
|
81
|
+
* separate deployment resources; ask the operator rather than omitting it.
|
|
82
|
+
*
|
|
83
|
+
* Optional because a platform older than this field simply does not send it.
|
|
84
|
+
*/
|
|
85
|
+
assertionAudience?: string | null;
|
|
71
86
|
/** A member added after this SDK release arrives camelised and untouched. */
|
|
72
87
|
[extra: string]: unknown;
|
|
73
88
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -2,9 +2,26 @@ import type { ChannelInfo, TenantContext, TenantScope } from '@sixtyninecommerce
|
|
|
2
2
|
/** Per-call options every admin method accepts (spec §9.2). */
|
|
3
3
|
export interface Call {
|
|
4
4
|
/**
|
|
5
|
-
* Sent as `Idempotency-Key` on a REST write
|
|
6
|
-
* the Admin API has no header for it
|
|
7
|
-
*
|
|
5
|
+
* Sent as `Idempotency-Key` on a REST write. On a GraphQL one it is sent as
|
|
6
|
+
* an ARGUMENT — the Admin API has no header for it — and only the mutations
|
|
7
|
+
* that declare one act on it. Today those are the money mutations
|
|
8
|
+
* `payments.record`, `orders.park`, `orders.settle` and all three
|
|
9
|
+
* `orders.surcharges.*`, plus `payments.providers.completeOnboarding` and
|
|
10
|
+
* `payments.providers.terminals.provision`. Repeating one under the same key
|
|
11
|
+
* returns the first call's result instead of moving the money again; a
|
|
12
|
+
* DIFFERENT request under the same key is refused with
|
|
13
|
+
* `IDEMPOTENCY_CONFLICT`.
|
|
14
|
+
*
|
|
15
|
+
* Two deliberate exceptions on that list. `customers.erase` takes a key but
|
|
16
|
+
* RECORDS it for provenance rather than keying on it: that mutation is
|
|
17
|
+
* idempotent on the customer itself, which is the stronger key. And
|
|
18
|
+
* `payments.providers.startOnboarding` takes none at all, because its
|
|
19
|
+
* response carries a link that can be single-use and a stored response is
|
|
20
|
+
* re-served for 24 hours.
|
|
21
|
+
*
|
|
22
|
+
* Everything else — `apiTokens.*` and the rest of the GraphQL surface —
|
|
23
|
+
* still sends the key and acts on nothing, so a repeat there is a second
|
|
24
|
+
* write.
|
|
8
25
|
*
|
|
9
26
|
* On REST, three of this SDK's writes are deduplicated by the service itself:
|
|
10
27
|
* `content.forms.upsertBySlug` (`PUT /forms/by-slug/:slug`),
|