kempo-commerce 0.1.0 → 0.1.2
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/admin/components/Settings.js +6 -1
- package/package.json +4 -4
- package/public/api/my/orders/[id]/GET.js +16 -16
- package/public/api/my/orders/[id]/cancel/POST.js +22 -22
- package/public/api/orders/[id]/guest/GET.js +13 -13
- package/public/api/orders/[id]/guest/cancel/POST.js +16 -16
- package/public/components/Checkout.js +669 -669
- package/public/components/checkoutSections.js +76 -76
- package/public/sdk.js +130 -130
- package/sdk.js +66 -66
- package/server/utils/checkout/tax.js +115 -115
|
@@ -1,76 +1,76 @@
|
|
|
1
|
-
/*
|
|
2
|
-
How another extension puts something on the checkout page, served at /commerce/components/checkoutSections.js.
|
|
3
|
-
|
|
4
|
-
The left side of the checkout is an accordion, one panel open at a time. Contact, billing, credits and payment
|
|
5
|
-
are the checkout's own panels. An extension adds a panel of its own, such as a shipping address, or puts an
|
|
6
|
-
element under the order summary:
|
|
7
|
-
|
|
8
|
-
```javascript
|
|
9
|
-
import { registerCheckoutSection } from '/commerce/components/checkoutSections.js';
|
|
10
|
-
|
|
11
|
-
const TAG = 'k-shipping-address';
|
|
12
|
-
customElements.define(TAG, ShippingAddress);
|
|
13
|
-
registerCheckoutSection(TAG, { title: 'Shipping address', priority: 300 });
|
|
14
|
-
```
|
|
15
|
-
|
|
16
|
-
Options:
|
|
17
|
-
|
|
18
|
-
title the panel's heading (required for a panel)
|
|
19
|
-
priority where the panel goes, highest first. The checkout's own are contact 500, billing 400,
|
|
20
|
-
credits 200 and payment 100, so 300 lands between billing and credits
|
|
21
|
-
id names the panel (default: the tag). contact, billing, credits and payment are reserved
|
|
22
|
-
where 'panel' (default) for an accordion panel, or 'summary' for an element under the order
|
|
23
|
-
summary, above the button that places the order
|
|
24
|
-
|
|
25
|
-
The checkout opens a panel while it is not done and closes it once it is, and opens the next panel when one
|
|
26
|
-
becomes done. An element says when its panel is done with a `complete` property (`get complete(){ ... }`,
|
|
27
|
-
true when the customer has filled in what it asks for). The checkout looks again only when something on the
|
|
28
|
-
page changes, so an element calls `checkout.setData(key, value)` whenever what it holds changes, which is
|
|
29
|
-
what makes the checkout look at it. Without a `complete`, its panel stays as the customer left it.
|
|
30
|
-
|
|
31
|
-
Each element is created by the checkout and handed `checkout`: the page itself. What it can use:
|
|
32
|
-
|
|
33
|
-
checkout.quote the server's current answer (cart, totals, tenders, plan). A Lit element is asked to
|
|
34
|
-
draw again whenever the checkout does, so it can read this in `render`
|
|
35
|
-
checkout.form the contact and billing details typed so far
|
|
36
|
-
checkout.reprice() asks the server for the totals again, which is what an element calls after it
|
|
37
|
-
changed something the totals depend on (it applied a coupon to the cart)
|
|
38
|
-
checkout.setData(key, value) a value sent with the order as `data`, which the `checkout:validate` guard
|
|
39
|
-
on the server receives
|
|
40
|
-
|
|
41
|
-
An element can also keep the order from being placed with a `blocker` property (`get blocker(){ ... }`): a sentence for
|
|
42
|
-
the customer saying why, or '' when nothing stops it. While it is not empty the button is disabled and the sentence is
|
|
43
|
-
shown under it. The server must refuse the order too (in `checkout:validate`), as this is only the page being clear.
|
|
44
|
-
|
|
45
|
-
The server side of a section is the extension's own: money it takes off goes through the cart's
|
|
46
|
-
adjustments, and what it needs to check goes in the `checkout:validate` guard.
|
|
47
|
-
|
|
48
|
-
The registry lives in this module, so every script that imports it shares one.
|
|
49
|
-
*/
|
|
50
|
-
|
|
51
|
-
const RESERVED = ['contact', 'billing', 'credits', 'payment'];
|
|
52
|
-
|
|
53
|
-
const sections = new Map();
|
|
54
|
-
|
|
55
|
-
const changed = () => window.dispatchEvent(new CustomEvent('checkout-sections-changed'));
|
|
56
|
-
|
|
57
|
-
export const registerCheckoutSection = (tag, { id = tag, title = '', priority = 0, where = 'panel' } = {}) => {
|
|
58
|
-
if(typeof tag !== 'string' || !tag.includes('-')) throw new Error('A checkout section is a custom element tag, such as k-shipping-address');
|
|
59
|
-
if(!['panel', 'summary'].includes(where)) throw new Error('A checkout section goes in a "panel" or in the "summary"');
|
|
60
|
-
if(where === 'panel' && !title) throw new Error('A checkout panel needs a title');
|
|
61
|
-
if(where === 'panel' && RESERVED.includes(id)) throw new Error(`"${id}" is the checkout's own panel; choose another id`);
|
|
62
|
-
if(where === 'panel' && [...sections.values()].some(section => section.where === 'panel' && section.id === id && section.tag !== tag)){
|
|
63
|
-
throw new Error(`Another checkout panel already has the id "${id}"`);
|
|
64
|
-
}
|
|
65
|
-
sections.set(tag, { tag, id, title, priority, where });
|
|
66
|
-
changed();
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
export const unregisterCheckoutSection = tag => {
|
|
70
|
-
if(sections.delete(tag)) changed();
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
/* The registered sections for a place (`panel` or `summary`), highest priority first. */
|
|
74
|
-
export const checkoutSections = where => [...sections.values()]
|
|
75
|
-
.filter(section => section.where === where)
|
|
76
|
-
.sort((a, b) => b.priority - a.priority || a.tag.localeCompare(b.tag));
|
|
1
|
+
/*
|
|
2
|
+
How another extension puts something on the checkout page, served at /commerce/components/checkoutSections.js.
|
|
3
|
+
|
|
4
|
+
The left side of the checkout is an accordion, one panel open at a time. Contact, billing, credits and payment
|
|
5
|
+
are the checkout's own panels. An extension adds a panel of its own, such as a shipping address, or puts an
|
|
6
|
+
element under the order summary:
|
|
7
|
+
|
|
8
|
+
```javascript
|
|
9
|
+
import { registerCheckoutSection } from '/commerce/components/checkoutSections.js';
|
|
10
|
+
|
|
11
|
+
const TAG = 'k-shipping-address';
|
|
12
|
+
customElements.define(TAG, ShippingAddress);
|
|
13
|
+
registerCheckoutSection(TAG, { title: 'Shipping address', priority: 300 });
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Options:
|
|
17
|
+
|
|
18
|
+
title the panel's heading (required for a panel)
|
|
19
|
+
priority where the panel goes, highest first. The checkout's own are contact 500, billing 400,
|
|
20
|
+
credits 200 and payment 100, so 300 lands between billing and credits
|
|
21
|
+
id names the panel (default: the tag). contact, billing, credits and payment are reserved
|
|
22
|
+
where 'panel' (default) for an accordion panel, or 'summary' for an element under the order
|
|
23
|
+
summary, above the button that places the order
|
|
24
|
+
|
|
25
|
+
The checkout opens a panel while it is not done and closes it once it is, and opens the next panel when one
|
|
26
|
+
becomes done. An element says when its panel is done with a `complete` property (`get complete(){ ... }`,
|
|
27
|
+
true when the customer has filled in what it asks for). The checkout looks again only when something on the
|
|
28
|
+
page changes, so an element calls `checkout.setData(key, value)` whenever what it holds changes, which is
|
|
29
|
+
what makes the checkout look at it. Without a `complete`, its panel stays as the customer left it.
|
|
30
|
+
|
|
31
|
+
Each element is created by the checkout and handed `checkout`: the page itself. What it can use:
|
|
32
|
+
|
|
33
|
+
checkout.quote the server's current answer (cart, totals, tenders, plan). A Lit element is asked to
|
|
34
|
+
draw again whenever the checkout does, so it can read this in `render`
|
|
35
|
+
checkout.form the contact and billing details typed so far
|
|
36
|
+
checkout.reprice() asks the server for the totals again, which is what an element calls after it
|
|
37
|
+
changed something the totals depend on (it applied a coupon to the cart)
|
|
38
|
+
checkout.setData(key, value) a value sent with the order as `data`, which the `checkout:validate` guard
|
|
39
|
+
on the server receives
|
|
40
|
+
|
|
41
|
+
An element can also keep the order from being placed with a `blocker` property (`get blocker(){ ... }`): a sentence for
|
|
42
|
+
the customer saying why, or '' when nothing stops it. While it is not empty the button is disabled and the sentence is
|
|
43
|
+
shown under it. The server must refuse the order too (in `checkout:validate`), as this is only the page being clear.
|
|
44
|
+
|
|
45
|
+
The server side of a section is the extension's own: money it takes off goes through the cart's
|
|
46
|
+
adjustments, and what it needs to check goes in the `checkout:validate` guard.
|
|
47
|
+
|
|
48
|
+
The registry lives in this module, so every script that imports it shares one.
|
|
49
|
+
*/
|
|
50
|
+
|
|
51
|
+
const RESERVED = ['contact', 'billing', 'credits', 'payment'];
|
|
52
|
+
|
|
53
|
+
const sections = new Map();
|
|
54
|
+
|
|
55
|
+
const changed = () => window.dispatchEvent(new CustomEvent('checkout-sections-changed'));
|
|
56
|
+
|
|
57
|
+
export const registerCheckoutSection = (tag, { id = tag, title = '', priority = 0, where = 'panel' } = {}) => {
|
|
58
|
+
if(typeof tag !== 'string' || !tag.includes('-')) throw new Error('A checkout section is a custom element tag, such as k-shipping-address');
|
|
59
|
+
if(!['panel', 'summary'].includes(where)) throw new Error('A checkout section goes in a "panel" or in the "summary"');
|
|
60
|
+
if(where === 'panel' && !title) throw new Error('A checkout panel needs a title');
|
|
61
|
+
if(where === 'panel' && RESERVED.includes(id)) throw new Error(`"${id}" is the checkout's own panel; choose another id`);
|
|
62
|
+
if(where === 'panel' && [...sections.values()].some(section => section.where === 'panel' && section.id === id && section.tag !== tag)){
|
|
63
|
+
throw new Error(`Another checkout panel already has the id "${id}"`);
|
|
64
|
+
}
|
|
65
|
+
sections.set(tag, { tag, id, title, priority, where });
|
|
66
|
+
changed();
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export const unregisterCheckoutSection = tag => {
|
|
70
|
+
if(sections.delete(tag)) changed();
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/* The registered sections for a place (`panel` or `summary`), highest priority first. */
|
|
74
|
+
export const checkoutSections = where => [...sections.values()]
|
|
75
|
+
.filter(section => section.where === where)
|
|
76
|
+
.sort((a, b) => b.priority - a.priority || a.tag.localeCompare(b.tag));
|
package/public/sdk.js
CHANGED
|
@@ -1,130 +1,130 @@
|
|
|
1
|
-
/*
|
|
2
|
-
Browser client, served at /commerce/sdk.js.
|
|
3
|
-
|
|
4
|
-
Every call returns an `[error, data]` tuple like the rest of kempo. An error carries the extra fields
|
|
5
|
-
a route sent with it — the `notices` that block a cart, the real `totals` when the total changed,
|
|
6
|
-
`account` rules, `retryAfter` — so a page can say something more useful than "no".
|
|
7
|
-
|
|
8
|
-
The browser names a product and a quantity, never a price: every amount comes from the server.
|
|
9
|
-
|
|
10
|
-
When anything changes the cart, call `announceCartChanged(source, cart)`. It fires a bubbling,
|
|
11
|
-
composed `cart-changed` event from `source` (the mini-cart, the cart button and the cart page all
|
|
12
|
-
listen) and tells the other tabs through a BroadcastChannel, so a cart open in two tabs never shows
|
|
13
|
-
two different things. Methods cause actions; events only say that something happened.
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
const BASE = '/commerce/api';
|
|
17
|
-
|
|
18
|
-
const request = async (path, options = {}) => {
|
|
19
|
-
try {
|
|
20
|
-
const response = await fetch(`${BASE}${path}`, { credentials: 'same-origin', ...options });
|
|
21
|
-
const data = await response.json().catch(() => ({}));
|
|
22
|
-
if(!response.ok){
|
|
23
|
-
const { error, ...details } = data;
|
|
24
|
-
return [{ code: response.status, msg: error || response.statusText, ...details }, null];
|
|
25
|
-
}
|
|
26
|
-
return [null, data];
|
|
27
|
-
} catch(error) {
|
|
28
|
-
return [{ code: 0, msg: error.message }, null];
|
|
29
|
-
}
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
const send = (method, body) => ({
|
|
33
|
-
method,
|
|
34
|
-
headers: { 'Content-Type': 'application/json' },
|
|
35
|
-
body: JSON.stringify(body ?? {}),
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
const query = params => {
|
|
39
|
-
const search = new URLSearchParams();
|
|
40
|
-
for(const [name, value] of Object.entries(params || {})){
|
|
41
|
-
if(value !== undefined && value !== null && value !== '') search.set(name, value);
|
|
42
|
-
}
|
|
43
|
-
const string = search.toString();
|
|
44
|
-
return string ? `?${string}` : '';
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
/*
|
|
48
|
-
An id for one checkout, sent as its idempotency key so a double click, a reload or a second tab
|
|
49
|
-
places the order once. A page keeps it for as long as the same checkout is going on.
|
|
50
|
-
*/
|
|
51
|
-
export const newKey = () => crypto.randomUUID();
|
|
52
|
-
|
|
53
|
-
/*
|
|
54
|
-
Cart
|
|
55
|
-
*/
|
|
56
|
-
|
|
57
|
-
export const getConfig = () => request('/config');
|
|
58
|
-
export const getRegions = () => request('/regions');
|
|
59
|
-
export const getCart = () => request('/cart');
|
|
60
|
-
export const addToCart = ({ productId, quantity = 1, selections = {}, data = {} }) => request('/cart/lines', send('POST', { productId, quantity, selections, data }));
|
|
61
|
-
export const updateLine = (lineId, quantity) => request(`/cart/lines/${encodeURIComponent(lineId)}`, send('PATCH', { quantity }));
|
|
62
|
-
export const removeLine = lineId => request(`/cart/lines/${encodeURIComponent(lineId)}`, { method: 'DELETE' });
|
|
63
|
-
export const clearCart = () => request('/cart', { method: 'DELETE' });
|
|
64
|
-
|
|
65
|
-
/*
|
|
66
|
-
Checkout
|
|
67
|
-
*/
|
|
68
|
-
|
|
69
|
-
export const startCheckout = () => request('/checkout');
|
|
70
|
-
export const validateCheckout = ({ billing, data, tenders }) => request('/checkout/validate', send('POST', { billing, data, tenders }));
|
|
71
|
-
export const placeOrder = ({ checkoutKey, billing, data, tenders, account, expectedTotal }) => request('/checkout/place', send('POST', { checkoutKey, billing, data, tenders, account, expectedTotal }));
|
|
72
|
-
|
|
73
|
-
/* Asks the server to check with the processor: the browser's word that a payment worked is never believed. */
|
|
74
|
-
export const confirmPayment = (orderId, key) => request(`/checkout/orders/${encodeURIComponent(orderId)}/confirm`, send('POST', { key }));
|
|
75
|
-
|
|
76
|
-
/*
|
|
77
|
-
Orders
|
|
78
|
-
*/
|
|
79
|
-
|
|
80
|
-
export const getMyOrders = (params = {}) => request(`/my/orders${query(params)}`);
|
|
81
|
-
export const getMyOrder = id => request(`/my/orders/${encodeURIComponent(id)}`);
|
|
82
|
-
export const cancelOrder = id => request(`/my/orders/${encodeURIComponent(id)}/cancel`, send('POST'));
|
|
83
|
-
|
|
84
|
-
/* A guest's order, reached with the key in their link. */
|
|
85
|
-
export const getOrderByKey = (id, key) => request(`/orders/${encodeURIComponent(id)}/guest${query({ key })}`);
|
|
86
|
-
export const cancelOrderByKey = (id, key) => request(`/orders/${encodeURIComponent(id)}/guest/cancel`, send('POST', { key }));
|
|
87
|
-
|
|
88
|
-
/*
|
|
89
|
-
The shop's side (each needs its own permission)
|
|
90
|
-
*/
|
|
91
|
-
|
|
92
|
-
export const listOrders = (params = {}) => request(`/orders${query(params)}`);
|
|
93
|
-
export const getOrder = id => request(`/orders/${encodeURIComponent(id)}`);
|
|
94
|
-
export const setOrderStatus = (id, status, note) => request(`/orders/${encodeURIComponent(id)}/status`, send('PATCH', { status, note }));
|
|
95
|
-
export const addNote = (id, body, visibleToCustomer) => request(`/orders/${encodeURIComponent(id)}/notes`, send('POST', { body, visibleToCustomer }));
|
|
96
|
-
export const contactCustomer = (id, subject, body) => request(`/orders/${encodeURIComponent(id)}/contact`, send('POST', { subject, body }));
|
|
97
|
-
export const setFulfillment = (id, lineId, fulfillment) => request(`/orders/${encodeURIComponent(id)}/lines/${encodeURIComponent(lineId)}`, send('PATCH', { fulfillment }));
|
|
98
|
-
export const cancelUnpaid = (id, reason) => request(`/orders/${encodeURIComponent(id)}/cancel`, send('POST', { reason }));
|
|
99
|
-
export const markPaid = (id, note) => request(`/orders/${encodeURIComponent(id)}/mark-paid`, send('POST', { note }));
|
|
100
|
-
export const previewRefund = options => request('/refunds/preview', send('POST', options));
|
|
101
|
-
export const refundOrder = (id, options) => request(`/orders/${encodeURIComponent(id)}/refund`, send('POST', options));
|
|
102
|
-
export const finishRefund = refundId => request(`/refunds/${encodeURIComponent(refundId)}/finish`, send('POST'));
|
|
103
|
-
export const listCarts = (params = {}) => request(`/carts${query(params)}`);
|
|
104
|
-
export const getSummary = () => request('/summary');
|
|
105
|
-
export const getSettings = () => request('/settings');
|
|
106
|
-
export const saveSettings = values => request('/settings', send('PUT', values));
|
|
107
|
-
|
|
108
|
-
/*
|
|
109
|
-
Telling everyone the cart changed
|
|
110
|
-
*/
|
|
111
|
-
|
|
112
|
-
const CHANNEL = 'kempo-commerce-cart';
|
|
113
|
-
const channel = 'BroadcastChannel' in window ? new BroadcastChannel(CHANNEL) : null;
|
|
114
|
-
|
|
115
|
-
export const announceCartChanged = (source, cart) => {
|
|
116
|
-
(source || window).dispatchEvent(new CustomEvent('cart-changed', { detail: { cart }, bubbles: true, composed: true }));
|
|
117
|
-
channel?.postMessage({ type: 'cart-changed' });
|
|
118
|
-
};
|
|
119
|
-
|
|
120
|
-
/* Runs `callback` when the cart changed in this tab (any component) or another. Returns a function that stops listening. */
|
|
121
|
-
export const onCartChanged = callback => {
|
|
122
|
-
const here = event => callback(event.detail?.cart ?? null);
|
|
123
|
-
const elsewhere = () => callback(null);
|
|
124
|
-
window.addEventListener('cart-changed', here);
|
|
125
|
-
channel?.addEventListener('message', elsewhere);
|
|
126
|
-
return () => {
|
|
127
|
-
window.removeEventListener('cart-changed', here);
|
|
128
|
-
channel?.removeEventListener('message', elsewhere);
|
|
129
|
-
};
|
|
130
|
-
};
|
|
1
|
+
/*
|
|
2
|
+
Browser client, served at /commerce/sdk.js.
|
|
3
|
+
|
|
4
|
+
Every call returns an `[error, data]` tuple like the rest of kempo. An error carries the extra fields
|
|
5
|
+
a route sent with it — the `notices` that block a cart, the real `totals` when the total changed,
|
|
6
|
+
`account` rules, `retryAfter` — so a page can say something more useful than "no".
|
|
7
|
+
|
|
8
|
+
The browser names a product and a quantity, never a price: every amount comes from the server.
|
|
9
|
+
|
|
10
|
+
When anything changes the cart, call `announceCartChanged(source, cart)`. It fires a bubbling,
|
|
11
|
+
composed `cart-changed` event from `source` (the mini-cart, the cart button and the cart page all
|
|
12
|
+
listen) and tells the other tabs through a BroadcastChannel, so a cart open in two tabs never shows
|
|
13
|
+
two different things. Methods cause actions; events only say that something happened.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const BASE = '/commerce/api';
|
|
17
|
+
|
|
18
|
+
const request = async (path, options = {}) => {
|
|
19
|
+
try {
|
|
20
|
+
const response = await fetch(`${BASE}${path}`, { credentials: 'same-origin', ...options });
|
|
21
|
+
const data = await response.json().catch(() => ({}));
|
|
22
|
+
if(!response.ok){
|
|
23
|
+
const { error, ...details } = data;
|
|
24
|
+
return [{ code: response.status, msg: error || response.statusText, ...details }, null];
|
|
25
|
+
}
|
|
26
|
+
return [null, data];
|
|
27
|
+
} catch(error) {
|
|
28
|
+
return [{ code: 0, msg: error.message }, null];
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const send = (method, body) => ({
|
|
33
|
+
method,
|
|
34
|
+
headers: { 'Content-Type': 'application/json' },
|
|
35
|
+
body: JSON.stringify(body ?? {}),
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const query = params => {
|
|
39
|
+
const search = new URLSearchParams();
|
|
40
|
+
for(const [name, value] of Object.entries(params || {})){
|
|
41
|
+
if(value !== undefined && value !== null && value !== '') search.set(name, value);
|
|
42
|
+
}
|
|
43
|
+
const string = search.toString();
|
|
44
|
+
return string ? `?${string}` : '';
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/*
|
|
48
|
+
An id for one checkout, sent as its idempotency key so a double click, a reload or a second tab
|
|
49
|
+
places the order once. A page keeps it for as long as the same checkout is going on.
|
|
50
|
+
*/
|
|
51
|
+
export const newKey = () => crypto.randomUUID();
|
|
52
|
+
|
|
53
|
+
/*
|
|
54
|
+
Cart
|
|
55
|
+
*/
|
|
56
|
+
|
|
57
|
+
export const getConfig = () => request('/config');
|
|
58
|
+
export const getRegions = () => request('/regions');
|
|
59
|
+
export const getCart = () => request('/cart');
|
|
60
|
+
export const addToCart = ({ productId, quantity = 1, selections = {}, data = {} }) => request('/cart/lines', send('POST', { productId, quantity, selections, data }));
|
|
61
|
+
export const updateLine = (lineId, quantity) => request(`/cart/lines/${encodeURIComponent(lineId)}`, send('PATCH', { quantity }));
|
|
62
|
+
export const removeLine = lineId => request(`/cart/lines/${encodeURIComponent(lineId)}`, { method: 'DELETE' });
|
|
63
|
+
export const clearCart = () => request('/cart', { method: 'DELETE' });
|
|
64
|
+
|
|
65
|
+
/*
|
|
66
|
+
Checkout
|
|
67
|
+
*/
|
|
68
|
+
|
|
69
|
+
export const startCheckout = () => request('/checkout');
|
|
70
|
+
export const validateCheckout = ({ billing, data, tenders }) => request('/checkout/validate', send('POST', { billing, data, tenders }));
|
|
71
|
+
export const placeOrder = ({ checkoutKey, billing, data, tenders, account, expectedTotal }) => request('/checkout/place', send('POST', { checkoutKey, billing, data, tenders, account, expectedTotal }));
|
|
72
|
+
|
|
73
|
+
/* Asks the server to check with the processor: the browser's word that a payment worked is never believed. */
|
|
74
|
+
export const confirmPayment = (orderId, key) => request(`/checkout/orders/${encodeURIComponent(orderId)}/confirm`, send('POST', { key }));
|
|
75
|
+
|
|
76
|
+
/*
|
|
77
|
+
Orders
|
|
78
|
+
*/
|
|
79
|
+
|
|
80
|
+
export const getMyOrders = (params = {}) => request(`/my/orders${query(params)}`);
|
|
81
|
+
export const getMyOrder = id => request(`/my/orders/${encodeURIComponent(id)}`);
|
|
82
|
+
export const cancelOrder = id => request(`/my/orders/${encodeURIComponent(id)}/cancel`, send('POST'));
|
|
83
|
+
|
|
84
|
+
/* A guest's order, reached with the key in their link. */
|
|
85
|
+
export const getOrderByKey = (id, key) => request(`/orders/${encodeURIComponent(id)}/guest${query({ key })}`);
|
|
86
|
+
export const cancelOrderByKey = (id, key) => request(`/orders/${encodeURIComponent(id)}/guest/cancel`, send('POST', { key }));
|
|
87
|
+
|
|
88
|
+
/*
|
|
89
|
+
The shop's side (each needs its own permission)
|
|
90
|
+
*/
|
|
91
|
+
|
|
92
|
+
export const listOrders = (params = {}) => request(`/orders${query(params)}`);
|
|
93
|
+
export const getOrder = id => request(`/orders/${encodeURIComponent(id)}`);
|
|
94
|
+
export const setOrderStatus = (id, status, note) => request(`/orders/${encodeURIComponent(id)}/status`, send('PATCH', { status, note }));
|
|
95
|
+
export const addNote = (id, body, visibleToCustomer) => request(`/orders/${encodeURIComponent(id)}/notes`, send('POST', { body, visibleToCustomer }));
|
|
96
|
+
export const contactCustomer = (id, subject, body) => request(`/orders/${encodeURIComponent(id)}/contact`, send('POST', { subject, body }));
|
|
97
|
+
export const setFulfillment = (id, lineId, fulfillment) => request(`/orders/${encodeURIComponent(id)}/lines/${encodeURIComponent(lineId)}`, send('PATCH', { fulfillment }));
|
|
98
|
+
export const cancelUnpaid = (id, reason) => request(`/orders/${encodeURIComponent(id)}/cancel`, send('POST', { reason }));
|
|
99
|
+
export const markPaid = (id, note) => request(`/orders/${encodeURIComponent(id)}/mark-paid`, send('POST', { note }));
|
|
100
|
+
export const previewRefund = options => request('/refunds/preview', send('POST', options));
|
|
101
|
+
export const refundOrder = (id, options) => request(`/orders/${encodeURIComponent(id)}/refund`, send('POST', options));
|
|
102
|
+
export const finishRefund = refundId => request(`/refunds/${encodeURIComponent(refundId)}/finish`, send('POST'));
|
|
103
|
+
export const listCarts = (params = {}) => request(`/carts${query(params)}`);
|
|
104
|
+
export const getSummary = () => request('/summary');
|
|
105
|
+
export const getSettings = () => request('/settings');
|
|
106
|
+
export const saveSettings = values => request('/settings', send('PUT', values));
|
|
107
|
+
|
|
108
|
+
/*
|
|
109
|
+
Telling everyone the cart changed
|
|
110
|
+
*/
|
|
111
|
+
|
|
112
|
+
const CHANNEL = 'kempo-commerce-cart';
|
|
113
|
+
const channel = 'BroadcastChannel' in window ? new BroadcastChannel(CHANNEL) : null;
|
|
114
|
+
|
|
115
|
+
export const announceCartChanged = (source, cart) => {
|
|
116
|
+
(source || window).dispatchEvent(new CustomEvent('cart-changed', { detail: { cart }, bubbles: true, composed: true }));
|
|
117
|
+
channel?.postMessage({ type: 'cart-changed' });
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
/* Runs `callback` when the cart changed in this tab (any component) or another. Returns a function that stops listening. */
|
|
121
|
+
export const onCartChanged = callback => {
|
|
122
|
+
const here = event => callback(event.detail?.cart ?? null);
|
|
123
|
+
const elsewhere = () => callback(null);
|
|
124
|
+
window.addEventListener('cart-changed', here);
|
|
125
|
+
channel?.addEventListener('message', elsewhere);
|
|
126
|
+
return () => {
|
|
127
|
+
window.removeEventListener('cart-changed', here);
|
|
128
|
+
channel?.removeEventListener('message', elsewhere);
|
|
129
|
+
};
|
|
130
|
+
};
|
package/sdk.js
CHANGED
|
@@ -1,66 +1,66 @@
|
|
|
1
|
-
/*
|
|
2
|
-
Server-side entry point — what shipping, coupons, digital delivery and every other extension that
|
|
3
|
-
builds on a store imports.
|
|
4
|
-
|
|
5
|
-
```js
|
|
6
|
-
import { getCart, addToCart, placeOrder, getOrder, setLineFulfillment, registerTender } from 'kempo-commerce/sdk';
|
|
7
|
-
|
|
8
|
-
// Something was shipped: mark the line, and the order completes when no line is waiting.
|
|
9
|
-
await setLineFulfillment(order.id, line.id, 'done', { actor: 'kempo-shipping', data: { shipmentId } });
|
|
10
|
-
```
|
|
11
|
-
|
|
12
|
-
Every function returns `[error, data]` with `error = { code, msg }`, and none throws. These are data
|
|
13
|
-
operations with **no permission checks of their own**: the routes under public/api decide who may
|
|
14
|
-
do what, and anything calling in here is server-side code that has already decided it is allowed.
|
|
15
|
-
Everything that changes an order takes an optional `{ actor }` (your extension's name), which is
|
|
16
|
-
written to the order's history.
|
|
17
|
-
|
|
18
|
-
Extending checkout is done with hooks (see README.md for every one): `cart:before_add` to refuse or
|
|
19
|
-
change an add, `totals:fees` to add shipping, `checkout:fields` and `checkout:validate` to collect
|
|
20
|
-
and check a delivery address, `checkout:tenders` to offer a way to pay, `order:paid` to do the work
|
|
21
|
-
once money has arrived, `order:line_fulfillment` to say which lines need sending, `refund:issued` to
|
|
22
|
-
put things back.
|
|
23
|
-
|
|
24
|
-
The model is WooCommerce's: carts for visitors and customers, an order with a status and a history,
|
|
25
|
-
guest checkout, and refunds issued by an administrator. Discounts, coupons, shipping, categories and
|
|
26
|
-
the catalog itself are other extensions' business.
|
|
27
|
-
*/
|
|
28
|
-
|
|
29
|
-
export {
|
|
30
|
-
resolveCart,
|
|
31
|
-
getCart,
|
|
32
|
-
addToCart,
|
|
33
|
-
updateCartLine,
|
|
34
|
-
removeCartLine,
|
|
35
|
-
clearCart,
|
|
36
|
-
mergeCarts,
|
|
37
|
-
setCartData,
|
|
38
|
-
listCarts,
|
|
39
|
-
} from './server/utils/carts/cart.js';
|
|
40
|
-
|
|
41
|
-
export { computeTotals, collectAdjustments } from './server/utils/totals.js';
|
|
42
|
-
export { resolveUnitDiscounts, resolveDiscountRows } from './server/utils/discounts.js';
|
|
43
|
-
|
|
44
|
-
export { startCheckout, accountRules } from './server/utils/checkout/quote.js';
|
|
45
|
-
export { placeOrder } from './server/utils/checkout/place.js';
|
|
46
|
-
export { completeOrder, markPaid } from './server/utils/checkout/complete.js';
|
|
47
|
-
export { failOrder, cancelUnpaidOrder } from './server/utils/checkout/unwind.js';
|
|
48
|
-
export { registerTender, unregisterTender, listTenders, getTender } from './server/utils/checkout/tenders.js';
|
|
49
|
-
|
|
50
|
-
export { getOrder, getOrderByKey, listOrders, forCustomer, STATUSES as orderStatuses } from './server/utils/orders/read.js';
|
|
51
|
-
export { setOrderStatus } from './server/utils/orders/status.js';
|
|
52
|
-
export { setLineFulfillment } from './server/utils/orders/fulfillment.js';
|
|
53
|
-
export { addOrderNote } from './server/utils/orders/notes.js';
|
|
54
|
-
export { setOrderData, addOrderEvent } from './server/utils/orders/data.js';
|
|
55
|
-
export { contactCustomer } from './server/utils/orders/contact.js';
|
|
56
|
-
export { getOrderEvents } from './server/utils/orders/events.js';
|
|
57
|
-
export { linkGuestOrders } from './server/utils/orders/link.js';
|
|
58
|
-
export { formatOrderNumber } from './server/utils/orders/number.js';
|
|
59
|
-
export { sendOrderEmail } from './server/utils/orders/emails.js';
|
|
60
|
-
|
|
61
|
-
export { readConfig, publicConfig, OWNER } from './server/utils/config/settings.js';
|
|
62
|
-
export { EVENTS } from './server/utils/events.js';
|
|
63
|
-
export { runSweep } from './server/utils/sweeps.js';
|
|
64
|
-
|
|
65
|
-
export { previewRefund } from './server/utils/refunds/preview.js';
|
|
66
|
-
export { refundOrder, finishRefund, getRefund } from './server/utils/refunds/issue.js';
|
|
1
|
+
/*
|
|
2
|
+
Server-side entry point — what shipping, coupons, digital delivery and every other extension that
|
|
3
|
+
builds on a store imports.
|
|
4
|
+
|
|
5
|
+
```js
|
|
6
|
+
import { getCart, addToCart, placeOrder, getOrder, setLineFulfillment, registerTender } from 'kempo-commerce/sdk';
|
|
7
|
+
|
|
8
|
+
// Something was shipped: mark the line, and the order completes when no line is waiting.
|
|
9
|
+
await setLineFulfillment(order.id, line.id, 'done', { actor: 'kempo-shipping', data: { shipmentId } });
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Every function returns `[error, data]` with `error = { code, msg }`, and none throws. These are data
|
|
13
|
+
operations with **no permission checks of their own**: the routes under public/api decide who may
|
|
14
|
+
do what, and anything calling in here is server-side code that has already decided it is allowed.
|
|
15
|
+
Everything that changes an order takes an optional `{ actor }` (your extension's name), which is
|
|
16
|
+
written to the order's history.
|
|
17
|
+
|
|
18
|
+
Extending checkout is done with hooks (see README.md for every one): `cart:before_add` to refuse or
|
|
19
|
+
change an add, `totals:fees` to add shipping, `checkout:fields` and `checkout:validate` to collect
|
|
20
|
+
and check a delivery address, `checkout:tenders` to offer a way to pay, `order:paid` to do the work
|
|
21
|
+
once money has arrived, `order:line_fulfillment` to say which lines need sending, `refund:issued` to
|
|
22
|
+
put things back.
|
|
23
|
+
|
|
24
|
+
The model is WooCommerce's: carts for visitors and customers, an order with a status and a history,
|
|
25
|
+
guest checkout, and refunds issued by an administrator. Discounts, coupons, shipping, categories and
|
|
26
|
+
the catalog itself are other extensions' business.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
export {
|
|
30
|
+
resolveCart,
|
|
31
|
+
getCart,
|
|
32
|
+
addToCart,
|
|
33
|
+
updateCartLine,
|
|
34
|
+
removeCartLine,
|
|
35
|
+
clearCart,
|
|
36
|
+
mergeCarts,
|
|
37
|
+
setCartData,
|
|
38
|
+
listCarts,
|
|
39
|
+
} from './server/utils/carts/cart.js';
|
|
40
|
+
|
|
41
|
+
export { computeTotals, collectAdjustments } from './server/utils/totals.js';
|
|
42
|
+
export { resolveUnitDiscounts, resolveDiscountRows } from './server/utils/discounts.js';
|
|
43
|
+
|
|
44
|
+
export { startCheckout, accountRules } from './server/utils/checkout/quote.js';
|
|
45
|
+
export { placeOrder } from './server/utils/checkout/place.js';
|
|
46
|
+
export { completeOrder, markPaid } from './server/utils/checkout/complete.js';
|
|
47
|
+
export { failOrder, cancelUnpaidOrder } from './server/utils/checkout/unwind.js';
|
|
48
|
+
export { registerTender, unregisterTender, listTenders, getTender } from './server/utils/checkout/tenders.js';
|
|
49
|
+
|
|
50
|
+
export { getOrder, getOrderByKey, listOrders, forCustomer, STATUSES as orderStatuses } from './server/utils/orders/read.js';
|
|
51
|
+
export { setOrderStatus } from './server/utils/orders/status.js';
|
|
52
|
+
export { setLineFulfillment } from './server/utils/orders/fulfillment.js';
|
|
53
|
+
export { addOrderNote } from './server/utils/orders/notes.js';
|
|
54
|
+
export { setOrderData, addOrderEvent } from './server/utils/orders/data.js';
|
|
55
|
+
export { contactCustomer } from './server/utils/orders/contact.js';
|
|
56
|
+
export { getOrderEvents } from './server/utils/orders/events.js';
|
|
57
|
+
export { linkGuestOrders } from './server/utils/orders/link.js';
|
|
58
|
+
export { formatOrderNumber } from './server/utils/orders/number.js';
|
|
59
|
+
export { sendOrderEmail } from './server/utils/orders/emails.js';
|
|
60
|
+
|
|
61
|
+
export { readConfig, publicConfig, OWNER } from './server/utils/config/settings.js';
|
|
62
|
+
export { EVENTS } from './server/utils/events.js';
|
|
63
|
+
export { runSweep } from './server/utils/sweeps.js';
|
|
64
|
+
|
|
65
|
+
export { previewRefund } from './server/utils/refunds/preview.js';
|
|
66
|
+
export { refundOrder, finishRefund, getRefund } from './server/utils/refunds/issue.js';
|