@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,121 @@
|
|
|
1
|
+
import type { Transport } from '@sixtyninecommerce/core';
|
|
2
|
+
import type { Call } from '../types.js';
|
|
3
|
+
/**
|
|
4
|
+
* A surcharge: an adjustment on a PLACED order that is not a line — a delivery
|
|
5
|
+
* charge, a negotiated discount, a handling fee.
|
|
6
|
+
*
|
|
7
|
+
* Money is integer MINOR UNITS of the order's currency, and `price` is NEGATIVE
|
|
8
|
+
* for a discount. `priceIncludesTax` says which basis the amount is stated in;
|
|
9
|
+
* there is no default, because guessing it is a 21% error.
|
|
10
|
+
*/
|
|
11
|
+
export interface SurchargeInput {
|
|
12
|
+
description: string;
|
|
13
|
+
sku?: string | null;
|
|
14
|
+
/** Minor units. Negative for a discount agreed after the order was placed. */
|
|
15
|
+
price: number;
|
|
16
|
+
/** Whether `price` is gross. Required: the engine will not guess a basis. */
|
|
17
|
+
priceIncludesTax: boolean;
|
|
18
|
+
/**
|
|
19
|
+
* Percentage (21 for 21%). Omit it for no tax line at all — and never send it
|
|
20
|
+
* together with `taxClass`, which is refused rather than silently resolved
|
|
21
|
+
* one way.
|
|
22
|
+
*/
|
|
23
|
+
taxRate?: number | null;
|
|
24
|
+
/**
|
|
25
|
+
* Label for the tax line. Refused together with `taxClass`: a class-priced
|
|
26
|
+
* line is named by the store's own tax setup — including names that are a
|
|
27
|
+
* policy statement, like `EU Reverse Charge (0%)` — and relabelling one would
|
|
28
|
+
* put on an invoice something the line is not.
|
|
29
|
+
*/
|
|
30
|
+
taxDescription?: string | null;
|
|
31
|
+
}
|
|
32
|
+
/** Options for a surcharge write. */
|
|
33
|
+
export interface SurchargeCall extends Call {
|
|
34
|
+
/**
|
|
35
|
+
* Price the surcharge from one of the store's tax classes (spec §8.6)
|
|
36
|
+
* instead of from an explicit `taxRate`.
|
|
37
|
+
*
|
|
38
|
+
* Resolved PER ORDER, which is the half of the platform's tax-class decision
|
|
39
|
+
* that is not write-time: a surcharge is money the platform composes, with no
|
|
40
|
+
* product variant behind it to have been priced already.
|
|
41
|
+
*
|
|
42
|
+
* **Which country, and which zone — they are two different questions.** The
|
|
43
|
+
* order's shipping COUNTRY picks the class's mapping (that is the store's own
|
|
44
|
+
* "what kind of thing is this" decision, one row per country). The order's
|
|
45
|
+
* tax ZONE picks the rate for the category that mapping names. The two can
|
|
46
|
+
* legitimately differ: a store only moves a country into its own zone by
|
|
47
|
+
* listing it in the tax-exclusive countries setting, so a Belgian order on a
|
|
48
|
+
* store that has not listed BE is still priced in the store's default zone
|
|
49
|
+
* while the BE mapping decides the category. Map a country and configure that
|
|
50
|
+
* zone's rates, or the write is refused rather than silently untaxed.
|
|
51
|
+
*
|
|
52
|
+
* **The order's own tax policy still wins.** A tax-removed order and an EU
|
|
53
|
+
* reverse charge override whatever the class resolved to, because the
|
|
54
|
+
* resolved rate is run through the same tax-line strategy the order's lines
|
|
55
|
+
* go through. The class chooses which rate is looked up, never what the line
|
|
56
|
+
* finally says. A per-line rate imported from an ERP is the one policy that
|
|
57
|
+
* does NOT carry over: it belongs to a line the ERP sent, never to a charge
|
|
58
|
+
* the merchant adds afterwards.
|
|
59
|
+
*
|
|
60
|
+
* Refusals: `TAX_CLASS_NOT_FOUND` (→ `NotFoundError`) for a key this store
|
|
61
|
+
* does not have; `TAX_CLASS_UNMAPPED` (→ `ValidationError`) when the class
|
|
62
|
+
* says nothing about the order's country and has no `*` fallback; and
|
|
63
|
+
* `TAX_CLASS_RATE_MISSING` (→ `ValidationError`) when it is mapped but that
|
|
64
|
+
* category has no rate in the order's zone. Sending it together with
|
|
65
|
+
* `input.taxRate` or `input.taxDescription` is refused as bad input.
|
|
66
|
+
*/
|
|
67
|
+
taxClass?: string;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* The order the mutation answers with, as much of it as this surface projects.
|
|
71
|
+
*
|
|
72
|
+
* The totals are re-derived and PERSISTED by every one of these calls, so the
|
|
73
|
+
* numbers here are the order's new state rather than a projection of the
|
|
74
|
+
* request.
|
|
75
|
+
*/
|
|
76
|
+
export interface SurchargedOrder {
|
|
77
|
+
id: string;
|
|
78
|
+
code: string;
|
|
79
|
+
currencyCode: string;
|
|
80
|
+
/** Minor units. */
|
|
81
|
+
total: number;
|
|
82
|
+
totalWithTax: number;
|
|
83
|
+
surcharges: Array<{
|
|
84
|
+
id: string;
|
|
85
|
+
description: string;
|
|
86
|
+
sku: string | null;
|
|
87
|
+
price: number;
|
|
88
|
+
priceWithTax: number;
|
|
89
|
+
taxRate: number;
|
|
90
|
+
}>;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Surcharges on a placed order.
|
|
94
|
+
*
|
|
95
|
+
* These exist beside Vendure's `modifyOrder`, which hard-requires a refund
|
|
96
|
+
* input for ANY downward price delta — so it cannot express "remove this
|
|
97
|
+
* discount" at all on an order with no payment to refund against. Needs
|
|
98
|
+
* `UpdateOrder`; refused once the order's invoice has been approved.
|
|
99
|
+
*/
|
|
100
|
+
export interface SurchargesModule {
|
|
101
|
+
/** Add a surcharge and re-total the order. */
|
|
102
|
+
add(orderId: string, input: SurchargeInput, options?: SurchargeCall): Promise<SurchargedOrder>;
|
|
103
|
+
/**
|
|
104
|
+
* Replace a surcharge in place and re-total the order. The surcharge keeps
|
|
105
|
+
* its id, so a delivery charge stays linked to the fulfillment that created
|
|
106
|
+
* it, and an omitted `sku` is left alone rather than cleared.
|
|
107
|
+
*
|
|
108
|
+
* TAX IS NOT LIKE `sku`: this is a whole replacement, so omitting BOTH
|
|
109
|
+
* `options.taxClass` and `input.taxRate` writes a surcharge with no tax line
|
|
110
|
+
* at all and lowers the order total. Re-send whichever of the two the
|
|
111
|
+
* surcharge should carry — a `taxClass` is then resolved afresh, so the rate
|
|
112
|
+
* that applies now is the one written.
|
|
113
|
+
*/
|
|
114
|
+
update(orderId: string, surchargeId: string, input: SurchargeInput, options?: SurchargeCall): Promise<SurchargedOrder>;
|
|
115
|
+
/** Remove a surcharge entirely and re-total the order. */
|
|
116
|
+
remove(orderId: string, surchargeId: string, options?: Call): Promise<SurchargedOrder>;
|
|
117
|
+
}
|
|
118
|
+
export declare const ADD_ORDER_SURCHARGE_MUTATION = "\n mutation AddOrderSurcharge(\n $orderId: ID!\n $input: SurchargeInput!\n $taxClass: String\n $idempotencyKey: String\n ) {\n addOrderSurcharge(\n orderId: $orderId\n input: $input\n taxClass: $taxClass\n idempotencyKey: $idempotencyKey\n ) {\n \n id\n code\n currencyCode\n total\n totalWithTax\n surcharges {\n id\n description\n sku\n price\n priceWithTax\n taxRate\n }\n\n }\n }\n";
|
|
119
|
+
export declare const UPDATE_ORDER_SURCHARGE_MUTATION = "\n mutation UpdateOrderSurcharge(\n $orderId: ID!\n $surchargeId: ID!\n $input: SurchargeInput!\n $taxClass: String\n $idempotencyKey: String\n ) {\n updateOrderSurcharge(\n orderId: $orderId\n surchargeId: $surchargeId\n input: $input\n taxClass: $taxClass\n idempotencyKey: $idempotencyKey\n ) {\n \n id\n code\n currencyCode\n total\n totalWithTax\n surcharges {\n id\n description\n sku\n price\n priceWithTax\n taxRate\n }\n\n }\n }\n";
|
|
120
|
+
export declare const REMOVE_ORDER_SURCHARGE_MUTATION = "\n mutation RemoveOrderSurcharge($orderId: ID!, $surchargeId: ID!, $idempotencyKey: String) {\n removeOrderSurcharge(\n orderId: $orderId\n surchargeId: $surchargeId\n idempotencyKey: $idempotencyKey\n ) {\n \n id\n code\n currencyCode\n total\n totalWithTax\n surcharges {\n id\n description\n sku\n price\n priceWithTax\n taxRate\n }\n\n }\n }\n";
|
|
121
|
+
export declare function createSurchargesModule(transport: Transport): SurchargesModule;
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
const SURCHARGED_ORDER_FIELDS = /* GraphQL */ `
|
|
2
|
+
id
|
|
3
|
+
code
|
|
4
|
+
currencyCode
|
|
5
|
+
total
|
|
6
|
+
totalWithTax
|
|
7
|
+
surcharges {
|
|
8
|
+
id
|
|
9
|
+
description
|
|
10
|
+
sku
|
|
11
|
+
price
|
|
12
|
+
priceWithTax
|
|
13
|
+
taxRate
|
|
14
|
+
}
|
|
15
|
+
`;
|
|
16
|
+
export const ADD_ORDER_SURCHARGE_MUTATION = /* GraphQL */ `
|
|
17
|
+
mutation AddOrderSurcharge(
|
|
18
|
+
$orderId: ID!
|
|
19
|
+
$input: SurchargeInput!
|
|
20
|
+
$taxClass: String
|
|
21
|
+
$idempotencyKey: String
|
|
22
|
+
) {
|
|
23
|
+
addOrderSurcharge(
|
|
24
|
+
orderId: $orderId
|
|
25
|
+
input: $input
|
|
26
|
+
taxClass: $taxClass
|
|
27
|
+
idempotencyKey: $idempotencyKey
|
|
28
|
+
) {
|
|
29
|
+
${SURCHARGED_ORDER_FIELDS}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
`;
|
|
33
|
+
export const UPDATE_ORDER_SURCHARGE_MUTATION = /* GraphQL */ `
|
|
34
|
+
mutation UpdateOrderSurcharge(
|
|
35
|
+
$orderId: ID!
|
|
36
|
+
$surchargeId: ID!
|
|
37
|
+
$input: SurchargeInput!
|
|
38
|
+
$taxClass: String
|
|
39
|
+
$idempotencyKey: String
|
|
40
|
+
) {
|
|
41
|
+
updateOrderSurcharge(
|
|
42
|
+
orderId: $orderId
|
|
43
|
+
surchargeId: $surchargeId
|
|
44
|
+
input: $input
|
|
45
|
+
taxClass: $taxClass
|
|
46
|
+
idempotencyKey: $idempotencyKey
|
|
47
|
+
) {
|
|
48
|
+
${SURCHARGED_ORDER_FIELDS}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
`;
|
|
52
|
+
export const REMOVE_ORDER_SURCHARGE_MUTATION = /* GraphQL */ `
|
|
53
|
+
mutation RemoveOrderSurcharge($orderId: ID!, $surchargeId: ID!, $idempotencyKey: String) {
|
|
54
|
+
removeOrderSurcharge(
|
|
55
|
+
orderId: $orderId
|
|
56
|
+
surchargeId: $surchargeId
|
|
57
|
+
idempotencyKey: $idempotencyKey
|
|
58
|
+
) {
|
|
59
|
+
${SURCHARGED_ORDER_FIELDS}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
`;
|
|
63
|
+
/**
|
|
64
|
+
* `undefined` rather than `null` for an absent tax class: a variable explicitly
|
|
65
|
+
* set to null is still a value the engine reads, and this argument's whole
|
|
66
|
+
* contract is "omit it and the legacy `taxRate` path is untouched".
|
|
67
|
+
*/
|
|
68
|
+
function taxClassVariable(options) {
|
|
69
|
+
const value = options?.taxClass;
|
|
70
|
+
return typeof value === 'string' && value.trim() !== '' ? value : undefined;
|
|
71
|
+
}
|
|
72
|
+
export function createSurchargesModule(transport) {
|
|
73
|
+
return {
|
|
74
|
+
async add(orderId, input, options) {
|
|
75
|
+
const data = await transport.graphql({
|
|
76
|
+
api: 'admin',
|
|
77
|
+
query: ADD_ORDER_SURCHARGE_MUTATION,
|
|
78
|
+
variables: {
|
|
79
|
+
orderId,
|
|
80
|
+
input,
|
|
81
|
+
taxClass: taxClassVariable(options),
|
|
82
|
+
idempotencyKey: options?.idempotencyKey,
|
|
83
|
+
},
|
|
84
|
+
}, options);
|
|
85
|
+
return data.addOrderSurcharge;
|
|
86
|
+
},
|
|
87
|
+
async update(orderId, surchargeId, input, options) {
|
|
88
|
+
const data = await transport.graphql({
|
|
89
|
+
api: 'admin',
|
|
90
|
+
query: UPDATE_ORDER_SURCHARGE_MUTATION,
|
|
91
|
+
variables: {
|
|
92
|
+
orderId,
|
|
93
|
+
surchargeId,
|
|
94
|
+
input,
|
|
95
|
+
taxClass: taxClassVariable(options),
|
|
96
|
+
idempotencyKey: options?.idempotencyKey,
|
|
97
|
+
},
|
|
98
|
+
}, options);
|
|
99
|
+
return data.updateOrderSurcharge;
|
|
100
|
+
},
|
|
101
|
+
async remove(orderId, surchargeId, options) {
|
|
102
|
+
const data = await transport.graphql({
|
|
103
|
+
api: 'admin',
|
|
104
|
+
query: REMOVE_ORDER_SURCHARGE_MUTATION,
|
|
105
|
+
variables: { orderId, surchargeId, idempotencyKey: options?.idempotencyKey },
|
|
106
|
+
}, options);
|
|
107
|
+
return data.removeOrderSurcharge;
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import type { Transport } from '@sixtyninecommerce/core';
|
|
2
|
+
import type { Call } from '../types.js';
|
|
3
|
+
import { type ProvidersModule } from './providers.js';
|
|
4
|
+
/**
|
|
5
|
+
* What an order has actually been paid, in minor units of its own currency.
|
|
6
|
+
*
|
|
7
|
+
* Derived from the order's payments on every read, never stored — a stored copy
|
|
8
|
+
* is a second source of truth that drifts the first time a refund lands.
|
|
9
|
+
*/
|
|
10
|
+
export interface OrderPaymentPosition {
|
|
11
|
+
/** The order's own gross total, carrying the order's rounding rule. */
|
|
12
|
+
total: number;
|
|
13
|
+
/** Settled plus authorized, less refunds: what is accounted for. */
|
|
14
|
+
covered: number;
|
|
15
|
+
/** Money actually taken, less refunds. This is what `orders.settle` requires. */
|
|
16
|
+
settled: number;
|
|
17
|
+
/** Money a payment handler may take and has not. */
|
|
18
|
+
authorized: number;
|
|
19
|
+
refunded: number;
|
|
20
|
+
/** `total - covered`, floored at zero. */
|
|
21
|
+
outstanding: number;
|
|
22
|
+
/**
|
|
23
|
+
* `covered - total`, floored at zero. Over-payment is refused at the door, so
|
|
24
|
+
* this is non-zero only for imported history, rows predating the guard, or a
|
|
25
|
+
* refund that landed after a second payment.
|
|
26
|
+
*/
|
|
27
|
+
overpaid: number;
|
|
28
|
+
/** What a further payment may take. */
|
|
29
|
+
availableToCharge: number;
|
|
30
|
+
}
|
|
31
|
+
/** Money the merchant HAS, recorded against an order. */
|
|
32
|
+
export interface RecordPaymentInput {
|
|
33
|
+
/**
|
|
34
|
+
* Minor units of the order's currency. Greater than zero, and at most the
|
|
35
|
+
* order's `outstanding` — an over-payment is refused rather than accepted,
|
|
36
|
+
* and the refusal carries the position so a till can correct itself without a
|
|
37
|
+
* second round trip.
|
|
38
|
+
*/
|
|
39
|
+
amount: number;
|
|
40
|
+
/** A payment method code configured on this channel. */
|
|
41
|
+
method: string;
|
|
42
|
+
/**
|
|
43
|
+
* Free-form detail stored on the payment, MINUS two keys the platform writes
|
|
44
|
+
* itself: `attribution` (the vouched-for staff, device and register refs) and
|
|
45
|
+
* `isDeposit`. Sending either is refused with `METADATA_KEY_RESERVED` rather
|
|
46
|
+
* than merged, so no caller can invent a till or a member of staff that every
|
|
47
|
+
* later report would believe.
|
|
48
|
+
*
|
|
49
|
+
* Anything you put under `public` is visible to the storefront — Vendure's
|
|
50
|
+
* Shop API exposes `metadata.public`. The platform never writes there.
|
|
51
|
+
*/
|
|
52
|
+
metadata?: Record<string, unknown>;
|
|
53
|
+
}
|
|
54
|
+
/** The order, as much of it as this surface projects. */
|
|
55
|
+
export interface PaidOrder {
|
|
56
|
+
id: string;
|
|
57
|
+
code: string;
|
|
58
|
+
state: string;
|
|
59
|
+
currencyCode: string;
|
|
60
|
+
total: number;
|
|
61
|
+
totalWithTax: number;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Payments: money against an order (spec §8.2), and the provider accounts
|
|
65
|
+
* it runs through (spec §8.9).
|
|
66
|
+
*
|
|
67
|
+
* `providers` is payment-provider onboarding, terminals and readiness. The
|
|
68
|
+
* rest of this namespace is the money itself.
|
|
69
|
+
*
|
|
70
|
+
* `record` writes money the merchant HAS, which lands the payment `Settled`.
|
|
71
|
+
* There is no payment state to choose: an AUTHORIZATION is a payment method
|
|
72
|
+
* handler's business — the gateway decides whether a card may be charged later
|
|
73
|
+
* — so one cannot be created here. Authorized payments that handlers DO create
|
|
74
|
+
* are counted by `position()` all the same, under `authorized`; only the write
|
|
75
|
+
* side is narrowed.
|
|
76
|
+
*
|
|
77
|
+
* Needs `UpdateOrder` to record, `ReadOrder` to read the position.
|
|
78
|
+
*/
|
|
79
|
+
export interface PaymentsModule {
|
|
80
|
+
/** Provider onboarding, terminals and readiness (spec §8.9). */
|
|
81
|
+
providers: ProvidersModule;
|
|
82
|
+
/**
|
|
83
|
+
* Record part (or all) of what an order owes, and return the order.
|
|
84
|
+
*
|
|
85
|
+
* `options.idempotencyKey` is honoured: the Admin API takes it as a mutation
|
|
86
|
+
* argument, so a retry under the same key returns the first call's order
|
|
87
|
+
* rather than taking the money twice. Use a fresh key per intended payment.
|
|
88
|
+
*
|
|
89
|
+
* Refusals: `AMOUNT_EXCEEDS_OUTSTANDING` (over-payment, with the position
|
|
90
|
+
* attached), `AMOUNT_NOT_POSITIVE`, `ORDER_NOT_RECORDABLE` (the order's state
|
|
91
|
+
* cannot take a payment), `DEPOSITS_NOT_ENABLED` — a PARTIAL payment on a
|
|
92
|
+
* store that has not turned deposits on; paying the whole outstanding balance
|
|
93
|
+
* is not a deposit and always works — `METADATA_KEY_RESERVED`,
|
|
94
|
+
* `PAYMENT_METHOD_NOT_FOUND`, `ORDER_NOT_FOUND`.
|
|
95
|
+
*/
|
|
96
|
+
record(orderId: string, input: RecordPaymentInput, options?: Call): Promise<PaidOrder>;
|
|
97
|
+
/** Read an order's payment position. */
|
|
98
|
+
position(orderId: string, options?: Call): Promise<OrderPaymentPosition>;
|
|
99
|
+
}
|
|
100
|
+
export declare const RECORD_ORDER_PAYMENT_MUTATION = "\n mutation RecordOrderPayment($orderId: ID!, $input: RecordPaymentInput!, $idempotencyKey: String) {\n recordOrderPayment(orderId: $orderId, input: $input, idempotencyKey: $idempotencyKey) {\n \n id\n code\n state\n currencyCode\n total\n totalWithTax\n\n }\n }\n";
|
|
101
|
+
export declare const ORDER_PAYMENT_POSITION_QUERY = "\n query OrderPaymentPosition($orderId: ID!) {\n orderPaymentPosition(orderId: $orderId) {\n \n total\n covered\n settled\n authorized\n refunded\n outstanding\n overpaid\n availableToCharge\n\n }\n }\n";
|
|
102
|
+
export declare function createPaymentsModule(transport: Transport): PaymentsModule;
|
|
103
|
+
export type { ProviderConnection, ProviderCredentialPath, ProviderOnboardingStart, ProviderReadiness, ProvidersModule, ProviderTerminal, ProviderTerminalsModule, ProvisionTerminalInput, ReadinessOptions, } from './providers.js';
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { createProvidersModule } from './providers.js';
|
|
2
|
+
const PAID_ORDER_FIELDS = /* GraphQL */ `
|
|
3
|
+
id
|
|
4
|
+
code
|
|
5
|
+
state
|
|
6
|
+
currencyCode
|
|
7
|
+
total
|
|
8
|
+
totalWithTax
|
|
9
|
+
`;
|
|
10
|
+
const POSITION_FIELDS = /* GraphQL */ `
|
|
11
|
+
total
|
|
12
|
+
covered
|
|
13
|
+
settled
|
|
14
|
+
authorized
|
|
15
|
+
refunded
|
|
16
|
+
outstanding
|
|
17
|
+
overpaid
|
|
18
|
+
availableToCharge
|
|
19
|
+
`;
|
|
20
|
+
export const RECORD_ORDER_PAYMENT_MUTATION = /* GraphQL */ `
|
|
21
|
+
mutation RecordOrderPayment($orderId: ID!, $input: RecordPaymentInput!, $idempotencyKey: String) {
|
|
22
|
+
recordOrderPayment(orderId: $orderId, input: $input, idempotencyKey: $idempotencyKey) {
|
|
23
|
+
${PAID_ORDER_FIELDS}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
`;
|
|
27
|
+
export const ORDER_PAYMENT_POSITION_QUERY = /* GraphQL */ `
|
|
28
|
+
query OrderPaymentPosition($orderId: ID!) {
|
|
29
|
+
orderPaymentPosition(orderId: $orderId) {
|
|
30
|
+
${POSITION_FIELDS}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
`;
|
|
34
|
+
export function createPaymentsModule(transport) {
|
|
35
|
+
return {
|
|
36
|
+
providers: createProvidersModule(transport),
|
|
37
|
+
async record(orderId, input, options) {
|
|
38
|
+
const data = await transport.graphql({
|
|
39
|
+
api: 'admin',
|
|
40
|
+
query: RECORD_ORDER_PAYMENT_MUTATION,
|
|
41
|
+
// Forwarded as a VARIABLE, not a header: the Admin API has no header
|
|
42
|
+
// for it, which is why the mutation takes the key as an argument.
|
|
43
|
+
// `undefined`, never an explicit null — `JSON.stringify` drops it, so
|
|
44
|
+
// an omitted key stays off the wire entirely rather than arriving as a
|
|
45
|
+
// value a future server could read as one.
|
|
46
|
+
variables: { orderId, input, idempotencyKey: options?.idempotencyKey },
|
|
47
|
+
}, options);
|
|
48
|
+
return data.recordOrderPayment;
|
|
49
|
+
},
|
|
50
|
+
async position(orderId, options) {
|
|
51
|
+
const data = await transport.graphql({ api: 'admin', query: ORDER_PAYMENT_POSITION_QUERY, variables: { orderId } }, options);
|
|
52
|
+
return data.orderPaymentPosition;
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
}
|
|
@@ -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;
|