@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.
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 +103 -0
  28. package/dist/payments/index.js +55 -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,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
- }
@@ -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, and ignored on a GraphQL one —
6
- * the Admin API has no header for it, so an `apiTokens.*` write is never
7
- * deduplicated and a repeat is a second write.
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`),