@cogenta/commerce 0.2.0 → 0.4.3
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/dist/admin/permissions.d.ts +8 -0
- package/dist/admin/permissions.d.ts.map +1 -1
- package/dist/admin/permissions.js +1 -0
- package/dist/admin/permissions.js.map +1 -1
- package/dist/admin/router.d.ts +47 -5
- package/dist/admin/router.d.ts.map +1 -1
- package/dist/admin/router.js +942 -24
- package/dist/admin/router.js.map +1 -1
- package/dist/cart/store.d.ts +23 -0
- package/dist/cart/store.d.ts.map +1 -1
- package/dist/cart/store.js +49 -2
- package/dist/cart/store.js.map +1 -1
- package/dist/catalog/csv.d.ts +29 -0
- package/dist/catalog/csv.d.ts.map +1 -0
- package/dist/catalog/csv.js +396 -0
- package/dist/catalog/csv.js.map +1 -0
- package/dist/catalog/store.d.ts +24 -4
- package/dist/catalog/store.d.ts.map +1 -1
- package/dist/catalog/store.js +238 -11
- package/dist/catalog/store.js.map +1 -1
- package/dist/catalog/types.d.ts +92 -0
- package/dist/catalog/types.d.ts.map +1 -1
- package/dist/catalog/types.js +20 -0
- package/dist/catalog/types.js.map +1 -1
- package/dist/coupon/store.d.ts +63 -5
- package/dist/coupon/store.d.ts.map +1 -1
- package/dist/coupon/store.js +140 -7
- package/dist/coupon/store.js.map +1 -1
- package/dist/customer/store.d.ts +17 -0
- package/dist/customer/store.d.ts.map +1 -1
- package/dist/customer/store.js +26 -0
- package/dist/customer/store.js.map +1 -1
- package/dist/index.d.ts +18 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -3
- package/dist/index.js.map +1 -1
- package/dist/invoice/credit-note.d.ts +45 -0
- package/dist/invoice/credit-note.d.ts.map +1 -0
- package/dist/invoice/credit-note.js +109 -0
- package/dist/invoice/credit-note.js.map +1 -0
- package/dist/invoice/sequence.d.ts +21 -0
- package/dist/invoice/sequence.d.ts.map +1 -0
- package/dist/invoice/sequence.js +64 -0
- package/dist/invoice/sequence.js.map +1 -0
- package/dist/invoice/store.d.ts +23 -1
- package/dist/invoice/store.d.ts.map +1 -1
- package/dist/invoice/store.js +23 -56
- package/dist/invoice/store.js.map +1 -1
- package/dist/order/csv.d.ts +24 -0
- package/dist/order/csv.d.ts.map +1 -0
- package/dist/order/csv.js +42 -0
- package/dist/order/csv.js.map +1 -0
- package/dist/order/notify.d.ts +65 -0
- package/dist/order/notify.d.ts.map +1 -0
- package/dist/order/notify.js +180 -0
- package/dist/order/notify.js.map +1 -0
- package/dist/order/store.d.ts +58 -1
- package/dist/order/store.d.ts.map +1 -1
- package/dist/order/store.js +145 -10
- package/dist/order/store.js.map +1 -1
- package/dist/order/types.d.ts +35 -1
- package/dist/order/types.d.ts.map +1 -1
- package/dist/order/types.js +2 -0
- package/dist/order/types.js.map +1 -1
- package/dist/payment/paypal.d.ts +9 -0
- package/dist/payment/paypal.d.ts.map +1 -0
- package/dist/payment/paypal.js +583 -0
- package/dist/payment/paypal.js.map +1 -0
- package/dist/payment/registry.d.ts +15 -6
- package/dist/payment/registry.d.ts.map +1 -1
- package/dist/payment/registry.js +17 -6
- package/dist/payment/registry.js.map +1 -1
- package/dist/payment/types.d.ts +6 -0
- package/dist/payment/types.d.ts.map +1 -1
- package/dist/shipping/store.d.ts +29 -3
- package/dist/shipping/store.d.ts.map +1 -1
- package/dist/shipping/store.js +68 -4
- package/dist/shipping/store.js.map +1 -1
- package/dist/subscription/renewal-notifier.d.ts +17 -0
- package/dist/subscription/renewal-notifier.d.ts.map +1 -0
- package/dist/subscription/renewal-notifier.js +33 -0
- package/dist/subscription/renewal-notifier.js.map +1 -0
- package/dist/subscription/store.d.ts +126 -3
- package/dist/subscription/store.d.ts.map +1 -1
- package/dist/subscription/store.js +348 -8
- package/dist/subscription/store.js.map +1 -1
- package/dist/tables.d.ts +17 -0
- package/dist/tables.d.ts.map +1 -1
- package/dist/tables.js +273 -0
- package/dist/tables.js.map +1 -1
- package/dist/tax/store.d.ts +20 -0
- package/dist/tax/store.d.ts.map +1 -1
- package/dist/tax/store.js +51 -7
- package/dist/tax/store.js.map +1 -1
- package/package.json +3 -2
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { type EmailTransport, type ReportChannelMessage } from '@cogenta/channels';
|
|
2
|
+
import { type DatabaseHandle } from '@cogenta/core';
|
|
3
|
+
import type { OrderStore } from './store.js';
|
|
4
|
+
import type { Order } from './types.js';
|
|
5
|
+
/**
|
|
6
|
+
* Transactional order e-mail (fiche 52 task 2): "réutiliser l'adaptateur
|
|
7
|
+
* e-mail existant de `@cogenta/channels`... modèles éditables, file avec
|
|
8
|
+
* reprise, journal visible sur la commande."
|
|
9
|
+
*
|
|
10
|
+
* Reused, never reinvented — the same discipline `@cogenta/forms`'s
|
|
11
|
+
* `notify.ts` already applies: this file builds `ReportChannelMessage`s and
|
|
12
|
+
* hands them to `createEmailAdapter`; it never writes its own MIME or
|
|
13
|
+
* subject/body HTML. `ReportChannelMessage` (not `AlertChannelMessage`) is
|
|
14
|
+
* the fit: a real subject line and structured key figures for a customer
|
|
15
|
+
* reading their own order, not an operator's "action attendue" framing.
|
|
16
|
+
*
|
|
17
|
+
* A confirmation and a shipment notice are the two kinds this fiche's
|
|
18
|
+
* acceptance criteria name ("commande → confirmation e-mail → expédition →
|
|
19
|
+
* notification"). The queue is persisted (`cogenta_commerce_order_emails`)
|
|
20
|
+
* rather than fire-and-forget like forms' notifier, because this fiche
|
|
21
|
+
* explicitly asks for "une file avec reprise" — a transient SMTP/file-write
|
|
22
|
+
* failure must not silently lose an order confirmation.
|
|
23
|
+
*/
|
|
24
|
+
export declare const ORDER_EMAIL_KINDS: readonly ['confirmation', 'shipment'];
|
|
25
|
+
export type OrderEmailKind = (typeof ORDER_EMAIL_KINDS)[number];
|
|
26
|
+
export type OrderEmailStatus = 'pending' | 'sent' | 'failed';
|
|
27
|
+
export interface OrderEmailRecord {
|
|
28
|
+
readonly id: string;
|
|
29
|
+
readonly orderId: string;
|
|
30
|
+
readonly kind: OrderEmailKind;
|
|
31
|
+
readonly toEmail: string;
|
|
32
|
+
readonly status: OrderEmailStatus;
|
|
33
|
+
readonly attempts: number;
|
|
34
|
+
readonly lastError: string | null;
|
|
35
|
+
readonly createdAt: string;
|
|
36
|
+
readonly sentAt: string | null;
|
|
37
|
+
}
|
|
38
|
+
export interface OrderEmailQueueDependencies {
|
|
39
|
+
readonly orders: OrderStore;
|
|
40
|
+
readonly transport: EmailTransport;
|
|
41
|
+
}
|
|
42
|
+
export interface OrderEmailQueue {
|
|
43
|
+
/** Queues one e-mail for the next `flushDue()`. Never sends synchronously. */
|
|
44
|
+
enqueue(orderId: string, kind: OrderEmailKind): Promise<OrderEmailRecord>;
|
|
45
|
+
listForOrder(orderId: string): Promise<readonly OrderEmailRecord[]>;
|
|
46
|
+
/**
|
|
47
|
+
* Sends every `pending` e-mail, up to `limit`. A failure is retried on the
|
|
48
|
+
* next call until `MAX_ATTEMPTS`, after which it becomes terminally
|
|
49
|
+
* `failed` — visible on `listForOrder` and, either way, noted on the
|
|
50
|
+
* order's own history (`OrderStore.record`), which is this fiche's "journal
|
|
51
|
+
* visible sur la commande".
|
|
52
|
+
*/
|
|
53
|
+
flushDue(limit?: number): Promise<{
|
|
54
|
+
readonly sent: number;
|
|
55
|
+
readonly failed: number;
|
|
56
|
+
}>;
|
|
57
|
+
}
|
|
58
|
+
/** How many delivery attempts before an e-mail is given up on for good. */
|
|
59
|
+
export declare const MAX_ATTEMPTS = 5;
|
|
60
|
+
/** Exported for testability — the exact message a confirmation renders as. */
|
|
61
|
+
export declare function buildConfirmationMessage(order: Order): ReportChannelMessage;
|
|
62
|
+
/** Exported for testability — the exact message a shipment notice renders as. */
|
|
63
|
+
export declare function buildShipmentMessage(order: Order): ReportChannelMessage;
|
|
64
|
+
export declare function createOrderEmailQueue(db: DatabaseHandle, dependencies: OrderEmailQueueDependencies, now?: () => number): OrderEmailQueue;
|
|
65
|
+
//# sourceMappingURL=notify.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"notify.d.ts","sourceRoot":"","sources":["../../src/order/notify.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,cAAc,EACnB,KAAK,oBAAoB,EAC1B,MAAM,mBAAmB,CAAA;AAC1B,OAAO,EAAgB,KAAK,cAAc,EAA0B,MAAM,eAAe,CAAA;AAIzF,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AAC5C,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,YAAY,CAAA;AAEvC;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,iBAAiB,YAAI,cAAc,EAAE,UAAU,CAAU,CAAA;AACtE,MAAM,MAAM,cAAc,GAAG,CAAC,OAAO,iBAAiB,CAAC,CAAC,MAAM,CAAC,CAAA;AAE/D,MAAM,MAAM,gBAAgB,GAAG,SAAS,GAAG,MAAM,GAAG,QAAQ,CAAA;AAE5D,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;IACnB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,IAAI,EAAE,cAAc,CAAA;IAC7B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,MAAM,EAAE,gBAAgB,CAAA;IACjC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACjC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;CAC/B;AAED,MAAM,WAAW,2BAA2B;IAC1C,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAA;IAC3B,QAAQ,CAAC,SAAS,EAAE,cAAc,CAAA;CACnC;AAED,MAAM,WAAW,eAAe;IAC9B,8EAA8E;IAC9E,OAAO,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,cAAc,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAAA;IACzE,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,gBAAgB,EAAE,CAAC,CAAA;IACnE;;;;;;OAMG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;CACtF;AAED,2EAA2E;AAC3E,eAAO,MAAM,YAAY,IAAI,CAAA;AA2C7B,8EAA8E;AAC9E,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,KAAK,GAAG,oBAAoB,CAgB3E;AAED,iFAAiF;AACjF,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,KAAK,GAAG,oBAAoB,CAmBvE;AAMD,wBAAgB,qBAAqB,CACnC,EAAE,EAAE,cAAc,EAClB,YAAY,EAAE,2BAA2B,EACzC,GAAG,GAAE,MAAM,MAAiB,GAC3B,eAAe,CAgGjB"}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { createEmailAdapter, } from '@cogenta/channels';
|
|
2
|
+
import { CogentaError, identifier, newId, sql } from '@cogenta/core';
|
|
3
|
+
import { formatMoney } from '../money.js';
|
|
4
|
+
import { toInt, toNullableText, toText } from '../rows.js';
|
|
5
|
+
import { TABLES } from '../tables.js';
|
|
6
|
+
/**
|
|
7
|
+
* Transactional order e-mail (fiche 52 task 2): "réutiliser l'adaptateur
|
|
8
|
+
* e-mail existant de `@cogenta/channels`... modèles éditables, file avec
|
|
9
|
+
* reprise, journal visible sur la commande."
|
|
10
|
+
*
|
|
11
|
+
* Reused, never reinvented — the same discipline `@cogenta/forms`'s
|
|
12
|
+
* `notify.ts` already applies: this file builds `ReportChannelMessage`s and
|
|
13
|
+
* hands them to `createEmailAdapter`; it never writes its own MIME or
|
|
14
|
+
* subject/body HTML. `ReportChannelMessage` (not `AlertChannelMessage`) is
|
|
15
|
+
* the fit: a real subject line and structured key figures for a customer
|
|
16
|
+
* reading their own order, not an operator's "action attendue" framing.
|
|
17
|
+
*
|
|
18
|
+
* A confirmation and a shipment notice are the two kinds this fiche's
|
|
19
|
+
* acceptance criteria name ("commande → confirmation e-mail → expédition →
|
|
20
|
+
* notification"). The queue is persisted (`cogenta_commerce_order_emails`)
|
|
21
|
+
* rather than fire-and-forget like forms' notifier, because this fiche
|
|
22
|
+
* explicitly asks for "une file avec reprise" — a transient SMTP/file-write
|
|
23
|
+
* failure must not silently lose an order confirmation.
|
|
24
|
+
*/
|
|
25
|
+
export const ORDER_EMAIL_KINDS = ['confirmation', 'shipment'];
|
|
26
|
+
/** How many delivery attempts before an e-mail is given up on for good. */
|
|
27
|
+
export const MAX_ATTEMPTS = 5;
|
|
28
|
+
function decode(row) {
|
|
29
|
+
return {
|
|
30
|
+
id: toText(row.id, 'order_email.id'),
|
|
31
|
+
orderId: toText(row.order_id, 'order_email.order_id'),
|
|
32
|
+
kind: toText(row.kind, 'order_email.kind'),
|
|
33
|
+
toEmail: toText(row.to_email, 'order_email.to_email'),
|
|
34
|
+
status: toText(row.status, 'order_email.status'),
|
|
35
|
+
attempts: toInt(row.attempts, 'order_email.attempts'),
|
|
36
|
+
lastError: toNullableText(row.last_error),
|
|
37
|
+
createdAt: toText(row.created_at, 'order_email.created_at'),
|
|
38
|
+
sentAt: toNullableText(row.sent_at),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
function money(order, amountMinor) {
|
|
42
|
+
return formatMoney({ amountMinor, currency: order.currency });
|
|
43
|
+
}
|
|
44
|
+
function addressLines(order) {
|
|
45
|
+
const lines = [
|
|
46
|
+
order.shippingRecipient,
|
|
47
|
+
order.shippingAddressLine1,
|
|
48
|
+
order.shippingAddressLine2,
|
|
49
|
+
[order.shippingPostalCode, order.shippingCity].filter((part) => part !== null).join(' '),
|
|
50
|
+
order.shippingCountry,
|
|
51
|
+
].filter((line) => line !== null && line.trim() !== '');
|
|
52
|
+
return lines.length === 0 ? 'No delivery address on file.' : lines.join('\n');
|
|
53
|
+
}
|
|
54
|
+
/** Exported for testability — the exact message a confirmation renders as. */
|
|
55
|
+
export function buildConfirmationMessage(order) {
|
|
56
|
+
return {
|
|
57
|
+
level: 'report',
|
|
58
|
+
title: `Order confirmation — ${order.reference}`,
|
|
59
|
+
keyFigures: [
|
|
60
|
+
{ label: 'Reference', value: order.reference },
|
|
61
|
+
{ label: 'Total', value: money(order, order.totalMinor) },
|
|
62
|
+
],
|
|
63
|
+
sections: [
|
|
64
|
+
{
|
|
65
|
+
heading: 'Items',
|
|
66
|
+
body: order.lines.map((line) => `${line.quantity} × ${line.title}`).join('\n'),
|
|
67
|
+
},
|
|
68
|
+
{ heading: 'Delivery address', body: addressLines(order) },
|
|
69
|
+
],
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
/** Exported for testability — the exact message a shipment notice renders as. */
|
|
73
|
+
export function buildShipmentMessage(order) {
|
|
74
|
+
return {
|
|
75
|
+
level: 'report',
|
|
76
|
+
title: `Your order has shipped — ${order.reference}`,
|
|
77
|
+
keyFigures: [
|
|
78
|
+
{ label: 'Reference', value: order.reference },
|
|
79
|
+
{ label: 'Carrier', value: order.trackingCarrier ?? 'Unspecified' },
|
|
80
|
+
{ label: 'Tracking number', value: order.trackingNumber ?? 'Unspecified' },
|
|
81
|
+
],
|
|
82
|
+
sections: [
|
|
83
|
+
{
|
|
84
|
+
heading: 'Delivery address',
|
|
85
|
+
body: addressLines(order),
|
|
86
|
+
},
|
|
87
|
+
...(order.trackingUrl === null
|
|
88
|
+
? []
|
|
89
|
+
: [{ heading: 'Track your parcel', body: order.trackingUrl }]),
|
|
90
|
+
],
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
function buildMessage(order, kind) {
|
|
94
|
+
return kind === 'confirmation' ? buildConfirmationMessage(order) : buildShipmentMessage(order);
|
|
95
|
+
}
|
|
96
|
+
export function createOrderEmailQueue(db, dependencies, now = Date.now) {
|
|
97
|
+
const d = db.dialect;
|
|
98
|
+
const table = identifier(TABLES.orderEmails, d);
|
|
99
|
+
const stamp = () => new Date(now()).toISOString();
|
|
100
|
+
const adapter = createEmailAdapter({ transport: dependencies.transport });
|
|
101
|
+
async function read(id) {
|
|
102
|
+
const result = await db.query(sql `select * from ${table} where id = ${id}`);
|
|
103
|
+
const row = result.rows[0];
|
|
104
|
+
return row === undefined ? null : decode(row);
|
|
105
|
+
}
|
|
106
|
+
return {
|
|
107
|
+
enqueue: async (orderId, kind) => {
|
|
108
|
+
const order = await dependencies.orders.read(orderId);
|
|
109
|
+
if (order === null) {
|
|
110
|
+
throw new CogentaError({
|
|
111
|
+
code: 'COMMERCE_ORDER_NOT_FOUND',
|
|
112
|
+
message: 'This order does not exist.',
|
|
113
|
+
hint: 'Check the order reference.',
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
const id = newId(now);
|
|
117
|
+
const at = stamp();
|
|
118
|
+
await db.query(sql `
|
|
119
|
+
insert into ${table} (id, order_id, kind, to_email, status, attempts, last_error, created_at, sent_at)
|
|
120
|
+
values (${id}, ${orderId}, ${kind}, ${order.email}, ${'pending'}, ${0}, ${null}, ${at}, ${null})`);
|
|
121
|
+
const created = await read(id);
|
|
122
|
+
if (created === null) {
|
|
123
|
+
throw new CogentaError({
|
|
124
|
+
code: 'INTERNAL',
|
|
125
|
+
message: 'The queued e-mail was not stored.',
|
|
126
|
+
hint: 'Check that the commerce tables exist (ensureCommerceTables).',
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
return created;
|
|
130
|
+
},
|
|
131
|
+
listForOrder: async (orderId) => {
|
|
132
|
+
const result = await db.query(sql `select * from ${table} where order_id = ${orderId} order by created_at asc`);
|
|
133
|
+
return result.rows.map(decode);
|
|
134
|
+
},
|
|
135
|
+
flushDue: async (limit = 25) => {
|
|
136
|
+
const due = await db.query(sql `select * from ${table} where status = ${'pending'} order by created_at asc limit ${limit}`);
|
|
137
|
+
let sent = 0;
|
|
138
|
+
let failed = 0;
|
|
139
|
+
for (const row of due.rows.map(decode)) {
|
|
140
|
+
const order = await dependencies.orders.read(row.orderId);
|
|
141
|
+
if (order === null) {
|
|
142
|
+
// The order it was for is gone — nothing left to describe or notify
|
|
143
|
+
// about. Terminal, not retried.
|
|
144
|
+
await db.query(sql `
|
|
145
|
+
update ${table} set status = ${'failed'}, attempts = attempts + 1,
|
|
146
|
+
last_error = ${'The order no longer exists.'}
|
|
147
|
+
where id = ${row.id}`);
|
|
148
|
+
failed += 1;
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
try {
|
|
152
|
+
await adapter.send({ id: row.toEmail }, buildMessage(order, row.kind));
|
|
153
|
+
await db.query(sql `
|
|
154
|
+
update ${table} set status = ${'sent'}, attempts = attempts + 1, sent_at = ${stamp()}, last_error = ${null}
|
|
155
|
+
where id = ${row.id}`);
|
|
156
|
+
await dependencies.orders.record(order.id, 'note', {
|
|
157
|
+
note: `${row.kind === 'confirmation' ? 'Confirmation' : 'Shipment notification'} e-mail sent to ${row.toEmail}.`,
|
|
158
|
+
});
|
|
159
|
+
sent += 1;
|
|
160
|
+
}
|
|
161
|
+
catch (error) {
|
|
162
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
163
|
+
const attempts = row.attempts + 1;
|
|
164
|
+
const givenUp = attempts >= MAX_ATTEMPTS;
|
|
165
|
+
await db.query(sql `
|
|
166
|
+
update ${table} set status = ${givenUp ? 'failed' : 'pending'}, attempts = ${attempts}, last_error = ${message}
|
|
167
|
+
where id = ${row.id}`);
|
|
168
|
+
if (givenUp) {
|
|
169
|
+
await dependencies.orders.record(order.id, 'note', {
|
|
170
|
+
note: `${row.kind === 'confirmation' ? 'Confirmation' : 'Shipment notification'} e-mail to ${row.toEmail} failed after ${String(attempts)} attempts: ${message}`,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
failed += 1;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return { sent, failed };
|
|
177
|
+
},
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
//# sourceMappingURL=notify.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"notify.js","sourceRoot":"","sources":["../../src/order/notify.ts"],"names":[],"mappings":"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"}
|
package/dist/order/store.d.ts
CHANGED
|
@@ -4,12 +4,34 @@ import type { CatalogStore } from '../catalog/store.js';
|
|
|
4
4
|
import type { StockShortfall } from '../catalog/types.js';
|
|
5
5
|
import type { CouponStore } from '../coupon/store.js';
|
|
6
6
|
import type { CustomerStore } from '../customer/store.js';
|
|
7
|
-
import type { Order, OrderEvent, OrderEventKind, OrderStatus } from './types.js';
|
|
7
|
+
import type { Order, OrderEvent, OrderEventKind, OrderStatus, OrderTracking, ShippingAddress } from './types.js';
|
|
8
8
|
export interface PlaceOrderInput {
|
|
9
9
|
readonly cartId: string;
|
|
10
10
|
readonly email: string;
|
|
11
11
|
readonly customerName?: string | null;
|
|
12
12
|
readonly actorId?: string | null;
|
|
13
|
+
/** The delivery address (fiche 52 task 1). Absent for digital-only goods. */
|
|
14
|
+
readonly shippingAddress?: ShippingAddress | null;
|
|
15
|
+
}
|
|
16
|
+
export interface PlaceManualOrderLineInput {
|
|
17
|
+
readonly variantId: string;
|
|
18
|
+
readonly quantity: number;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* A manual order — one a shopkeeper types in themselves (fiche 52 task 5):
|
|
22
|
+
* a phone order, a trade-show sale, a correction for a customer who paid by
|
|
23
|
+
* some other means entirely. Deliberately **not** a second placement path:
|
|
24
|
+
* it opens a real cart, adds the real lines to it, and calls the exact same
|
|
25
|
+
* `place()` this file already has, so stock-taking, coupon redemption and
|
|
26
|
+
* pricing are never duplicated — only reused.
|
|
27
|
+
*/
|
|
28
|
+
export interface PlaceManualOrderInput {
|
|
29
|
+
readonly email: string;
|
|
30
|
+
readonly customerName?: string | null;
|
|
31
|
+
readonly currency: string;
|
|
32
|
+
readonly lines: readonly PlaceManualOrderLineInput[];
|
|
33
|
+
readonly shippingAddress?: ShippingAddress | null;
|
|
34
|
+
readonly actorId?: string | null;
|
|
13
35
|
}
|
|
14
36
|
/**
|
|
15
37
|
* Placing an order either produced one, or explains exactly what stopped it.
|
|
@@ -40,6 +62,17 @@ export interface OrderStoreDependencies {
|
|
|
40
62
|
export interface OrderListOptions {
|
|
41
63
|
readonly status?: OrderStatus;
|
|
42
64
|
readonly customerId?: string;
|
|
65
|
+
/**
|
|
66
|
+
* A substring of the reference or the email, folded to lower case (fiche 36
|
|
67
|
+
* task 4 — the admin's global search finds an order "by number and email").
|
|
68
|
+
* Mirrors `CustomerStore.list`'s own `search` option, the established
|
|
69
|
+
* pattern for this kind of filter in this package.
|
|
70
|
+
*/
|
|
71
|
+
readonly search?: string;
|
|
72
|
+
/** Placed at or after this instant (inclusive), ISO 8601 — fiche 52 task 7. */
|
|
73
|
+
readonly placedFrom?: string;
|
|
74
|
+
/** Placed at or before this instant (inclusive), ISO 8601 — fiche 52 task 7. */
|
|
75
|
+
readonly placedTo?: string;
|
|
43
76
|
readonly limit?: number;
|
|
44
77
|
readonly offset?: number;
|
|
45
78
|
}
|
|
@@ -55,6 +88,8 @@ export interface OrderStore {
|
|
|
55
88
|
* sale that did not occur.
|
|
56
89
|
*/
|
|
57
90
|
place(input: PlaceOrderInput): Promise<PlaceOrderOutcome>;
|
|
91
|
+
/** A shopkeeper-entered order (fiche 52 task 5) — see `PlaceManualOrderInput`. */
|
|
92
|
+
placeManual(input: PlaceManualOrderInput): Promise<PlaceOrderOutcome>;
|
|
58
93
|
read(id: string): Promise<Order | null>;
|
|
59
94
|
readByReference(reference: string): Promise<Order | null>;
|
|
60
95
|
list(options?: OrderListOptions): Promise<readonly Order[]>;
|
|
@@ -69,6 +104,28 @@ export interface OrderStore {
|
|
|
69
104
|
readonly note?: string;
|
|
70
105
|
readonly actorId?: string | null;
|
|
71
106
|
}, tx?: SqlExecutor): Promise<void>;
|
|
107
|
+
/**
|
|
108
|
+
* Corrects the e-mail and/or delivery address (fiche 52 task 5, "modification
|
|
109
|
+
* pré-paiement"). Only while `pending` — once an order is `paid`, whatever a
|
|
110
|
+
* courier or an invoice already refers to must not shift under it; a
|
|
111
|
+
* shopkeeper who needs to redirect a shipped parcel uses `setTracking`
|
|
112
|
+
* instead, or cancels and re-places the order.
|
|
113
|
+
*/
|
|
114
|
+
update(id: string, patch: {
|
|
115
|
+
readonly email?: string;
|
|
116
|
+
readonly shippingAddress?: ShippingAddress | null;
|
|
117
|
+
}, options?: {
|
|
118
|
+
readonly actorId?: string | null;
|
|
119
|
+
}): Promise<Order>;
|
|
120
|
+
/**
|
|
121
|
+
* Records how a shipment actually went (fiche 52 task 4). Setting tracking
|
|
122
|
+
* on a `paid` order also transitions it to `shipped` — attaching a tracking
|
|
123
|
+
* number *is* what "we shipped it" means here — while a `shipped` order
|
|
124
|
+
* just gets its tracking corrected in place.
|
|
125
|
+
*/
|
|
126
|
+
setTracking(id: string, tracking: OrderTracking, options?: {
|
|
127
|
+
readonly actorId?: string | null;
|
|
128
|
+
}): Promise<Order>;
|
|
72
129
|
}
|
|
73
130
|
export declare function referenceFrom(id: string): string;
|
|
74
131
|
export declare function createOrderStore(db: DatabaseHandle, dependencies: OrderStoreDependencies, now?: () => number): OrderStore;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/order/store.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,cAAc,EAInB,KAAK,WAAW,
|
|
1
|
+
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/order/store.ts"],"names":[],"mappings":"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"}
|
package/dist/order/store.js
CHANGED
|
@@ -70,6 +70,16 @@ export function createOrderStore(db, dependencies, now = Date.now) {
|
|
|
70
70
|
shippingRegion: toNullableText(row.shipping_region),
|
|
71
71
|
shippingMethodId: toNullableText(row.shipping_method_id),
|
|
72
72
|
shippingMethodLabel: toNullableText(row.shipping_method_label),
|
|
73
|
+
shippingAddressLine1: toNullableText(row.shipping_address_line1),
|
|
74
|
+
shippingAddressLine2: toNullableText(row.shipping_address_line2),
|
|
75
|
+
shippingCity: toNullableText(row.shipping_city),
|
|
76
|
+
shippingPostalCode: toNullableText(row.shipping_postal_code),
|
|
77
|
+
shippingRecipient: toNullableText(row.shipping_recipient),
|
|
78
|
+
shippingPhone: toNullableText(row.shipping_phone),
|
|
79
|
+
trackingCarrier: toNullableText(row.tracking_carrier),
|
|
80
|
+
trackingNumber: toNullableText(row.tracking_number),
|
|
81
|
+
trackingUrl: toNullableText(row.tracking_url),
|
|
82
|
+
shippedAt: toNullableText(row.shipped_at),
|
|
73
83
|
subscriptionId: toNullableText(row.subscription_id),
|
|
74
84
|
lines: await linesOf(id, executor),
|
|
75
85
|
placedAt: toText(row.placed_at, 'order.placed_at'),
|
|
@@ -99,7 +109,7 @@ export function createOrderStore(db, dependencies, now = Date.now) {
|
|
|
99
109
|
${fields.fromStatus ?? null}, ${fields.toStatus ?? null},
|
|
100
110
|
${fields.actorId ?? null}, ${fields.note ?? null})`);
|
|
101
111
|
}
|
|
102
|
-
|
|
112
|
+
const store = {
|
|
103
113
|
place: async (input) => {
|
|
104
114
|
const priced = await dependencies.carts.price(input.cartId);
|
|
105
115
|
const { cart, totals } = priced;
|
|
@@ -118,7 +128,13 @@ export function createOrderStore(db, dependencies, now = Date.now) {
|
|
|
118
128
|
// one is the one that decides, because a coupon can expire or be
|
|
119
129
|
// exhausted between adding it and paying.
|
|
120
130
|
if (cart.couponCode !== null) {
|
|
121
|
-
const
|
|
131
|
+
const productIds = [];
|
|
132
|
+
for (const line of cart.lines) {
|
|
133
|
+
const variant = await dependencies.catalog.readVariant(line.variantId);
|
|
134
|
+
if (variant !== null)
|
|
135
|
+
productIds.push(variant.productId);
|
|
136
|
+
}
|
|
137
|
+
const check = await dependencies.coupons.check(cart.couponCode, totals.subtotalMinor, cart.currency, { customerId: customer.id, productIds });
|
|
122
138
|
if (check.kind !== 'ok') {
|
|
123
139
|
return { kind: 'coupon_refused', reason: couponRefusal(check).message };
|
|
124
140
|
}
|
|
@@ -152,17 +168,25 @@ export function createOrderStore(db, dependencies, now = Date.now) {
|
|
|
152
168
|
});
|
|
153
169
|
}
|
|
154
170
|
}
|
|
171
|
+
const address = input.shippingAddress ?? null;
|
|
155
172
|
await tx.query(sql `
|
|
156
173
|
insert into ${orders} (id, reference, customer_id, email, status, currency,
|
|
157
174
|
subtotal_minor, discount_minor, shipping_minor, tax_minor, total_minor,
|
|
158
175
|
coupon_code, shipping_country, shipping_region,
|
|
159
176
|
shipping_method_id, shipping_method_label,
|
|
177
|
+
shipping_address_line1, shipping_address_line2, shipping_city,
|
|
178
|
+
shipping_postal_code, shipping_recipient, shipping_phone,
|
|
179
|
+
tracking_carrier, tracking_number, tracking_url, shipped_at,
|
|
160
180
|
placed_at, updated_at, subscription_id)
|
|
161
181
|
values (${id}, ${reference}, ${customer.id}, ${customer.email}, ${'pending'}, ${cart.currency},
|
|
162
182
|
${totals.subtotalMinor}, ${totals.discountMinor}, ${totals.shippingMinor},
|
|
163
183
|
${totals.taxMinor}, ${totals.totalMinor},
|
|
164
184
|
${cart.couponCode}, ${cart.shippingCountry}, ${cart.shippingRegion},
|
|
165
|
-
${cart.shippingMethodId}, ${null},
|
|
185
|
+
${cart.shippingMethodId}, ${null},
|
|
186
|
+
${address?.line1 ?? null}, ${address?.line2 ?? null}, ${address?.city ?? null},
|
|
187
|
+
${address?.postalCode ?? null}, ${address?.recipient ?? null}, ${address?.phone ?? null},
|
|
188
|
+
${null}, ${null}, ${null}, ${null},
|
|
189
|
+
${at}, ${at}, ${null})`);
|
|
166
190
|
for (const [position, line] of totals.lines.entries()) {
|
|
167
191
|
await tx.query(sql `
|
|
168
192
|
insert into ${orderLines} (id, order_id, variant_id, sku, title, quantity,
|
|
@@ -198,6 +222,25 @@ export function createOrderStore(db, dependencies, now = Date.now) {
|
|
|
198
222
|
throw error;
|
|
199
223
|
}
|
|
200
224
|
},
|
|
225
|
+
placeManual: async (input) => {
|
|
226
|
+
if (input.lines.length === 0)
|
|
227
|
+
return { kind: 'empty' };
|
|
228
|
+
const customer = await dependencies.customers.ensure(input.email, input.customerName);
|
|
229
|
+
const cart = await dependencies.carts.open({
|
|
230
|
+
currency: input.currency,
|
|
231
|
+
customerId: customer.id,
|
|
232
|
+
});
|
|
233
|
+
for (const line of input.lines) {
|
|
234
|
+
await dependencies.carts.addLine(cart.id, line.variantId, line.quantity);
|
|
235
|
+
}
|
|
236
|
+
return store.place({
|
|
237
|
+
cartId: cart.id,
|
|
238
|
+
email: input.email,
|
|
239
|
+
...(input.customerName === undefined ? {} : { customerName: input.customerName }),
|
|
240
|
+
...(input.actorId === undefined ? {} : { actorId: input.actorId }),
|
|
241
|
+
...(input.shippingAddress === undefined ? {} : { shippingAddress: input.shippingAddress }),
|
|
242
|
+
});
|
|
243
|
+
},
|
|
201
244
|
read: async (id) => read(id),
|
|
202
245
|
readByReference: async (reference) => {
|
|
203
246
|
const result = await db.query(sql `select * from ${orders} where reference = ${reference.trim().toUpperCase()}`);
|
|
@@ -205,16 +248,26 @@ export function createOrderStore(db, dependencies, now = Date.now) {
|
|
|
205
248
|
return row === undefined ? null : decode(row);
|
|
206
249
|
},
|
|
207
250
|
list: async (options) => {
|
|
208
|
-
|
|
209
|
-
if (options?.status !== undefined
|
|
210
|
-
|
|
251
|
+
const conditions = [];
|
|
252
|
+
if (options?.status !== undefined)
|
|
253
|
+
conditions.push(sql `status = ${options.status}`);
|
|
254
|
+
if (options?.customerId !== undefined) {
|
|
255
|
+
conditions.push(sql `customer_id = ${options.customerId}`);
|
|
211
256
|
}
|
|
212
|
-
|
|
213
|
-
|
|
257
|
+
const search = options?.search?.trim().toLowerCase();
|
|
258
|
+
if (search !== undefined && search !== '') {
|
|
259
|
+
conditions.push(sql `(lower(reference) like ${`%${search}%`} or lower(email) like ${`%${search}%`})`);
|
|
214
260
|
}
|
|
215
|
-
|
|
216
|
-
|
|
261
|
+
if (options?.placedFrom !== undefined) {
|
|
262
|
+
conditions.push(sql `placed_at >= ${options.placedFrom}`);
|
|
217
263
|
}
|
|
264
|
+
if (options?.placedTo !== undefined) {
|
|
265
|
+
conditions.push(sql `placed_at <= ${options.placedTo}`);
|
|
266
|
+
}
|
|
267
|
+
let statement = sql `select * from ${orders}`;
|
|
268
|
+
conditions.forEach((condition, index) => {
|
|
269
|
+
statement = sql `${statement} ${index === 0 ? sql `where` : sql `and`} ${condition}`;
|
|
270
|
+
});
|
|
218
271
|
statement = sql `${statement} order by placed_at desc, id desc limit ${limit(options?.limit ?? 100)} offset ${limit(options?.offset ?? 0)}`;
|
|
219
272
|
const result = await db.query(statement);
|
|
220
273
|
const decoded = [];
|
|
@@ -287,7 +340,89 @@ export function createOrderStore(db, dependencies, now = Date.now) {
|
|
|
287
340
|
note: options?.note ?? null,
|
|
288
341
|
});
|
|
289
342
|
},
|
|
343
|
+
update: async (id, patch, options) => {
|
|
344
|
+
const current = await load(id);
|
|
345
|
+
if (current.status !== 'pending') {
|
|
346
|
+
throw new CogentaError({
|
|
347
|
+
code: 'COMMERCE_ORDER_LOCKED',
|
|
348
|
+
message: `This order is ${current.status}, and its e-mail and delivery address can no longer be edited.`,
|
|
349
|
+
hint: 'Only a pending order — not yet paid — can be corrected here. Cancel and re-place it instead.',
|
|
350
|
+
details: { orderId: id, status: current.status },
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
const address = patch.shippingAddress;
|
|
354
|
+
const at = stamp();
|
|
355
|
+
await db.query(sql `
|
|
356
|
+
update ${orders}
|
|
357
|
+
set email = ${patch.email ?? current.email},
|
|
358
|
+
shipping_address_line1 = ${address === undefined ? current.shippingAddressLine1 : (address?.line1 ?? null)},
|
|
359
|
+
shipping_address_line2 = ${address === undefined ? current.shippingAddressLine2 : (address?.line2 ?? null)},
|
|
360
|
+
shipping_city = ${address === undefined ? current.shippingCity : (address?.city ?? null)},
|
|
361
|
+
shipping_postal_code = ${address === undefined ? current.shippingPostalCode : (address?.postalCode ?? null)},
|
|
362
|
+
shipping_recipient = ${address === undefined ? current.shippingRecipient : (address?.recipient ?? null)},
|
|
363
|
+
shipping_phone = ${address === undefined ? current.shippingPhone : (address?.phone ?? null)},
|
|
364
|
+
updated_at = ${at}
|
|
365
|
+
where id = ${id}`);
|
|
366
|
+
await appendEvent(db, id, 'address_updated', {
|
|
367
|
+
actorId: options?.actorId ?? null,
|
|
368
|
+
note: 'Order details corrected before payment.',
|
|
369
|
+
});
|
|
370
|
+
return load(id);
|
|
371
|
+
},
|
|
372
|
+
setTracking: async (id, tracking, options) => {
|
|
373
|
+
const current = await load(id);
|
|
374
|
+
if (current.status !== 'paid' && current.status !== 'shipped') {
|
|
375
|
+
throw new CogentaError({
|
|
376
|
+
code: 'COMMERCE_TRACKING_INVALID',
|
|
377
|
+
message: `Tracking can only be recorded once an order is paid, and this one is ${current.status}.`,
|
|
378
|
+
hint: 'Settle the payment first — an order cannot ship before it is paid for.',
|
|
379
|
+
details: { orderId: id, status: current.status },
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
if (tracking.carrier.trim() === '' || tracking.number.trim() === '') {
|
|
383
|
+
throw new CogentaError({
|
|
384
|
+
code: 'COMMERCE_TRACKING_INVALID',
|
|
385
|
+
message: 'A carrier and a tracking number are both required.',
|
|
386
|
+
hint: 'Fill in both fields, or wait until the shipment has a real tracking number.',
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
return db.transaction(async (tx) => {
|
|
390
|
+
const wasPaid = current.status === 'paid';
|
|
391
|
+
const at = stamp();
|
|
392
|
+
await tx.query(sql `
|
|
393
|
+
update ${orders}
|
|
394
|
+
set tracking_carrier = ${tracking.carrier},
|
|
395
|
+
tracking_number = ${tracking.number},
|
|
396
|
+
tracking_url = ${tracking.url ?? null},
|
|
397
|
+
shipped_at = ${current.shippedAt ?? at},
|
|
398
|
+
status = ${wasPaid ? 'shipped' : current.status},
|
|
399
|
+
updated_at = ${at}
|
|
400
|
+
where id = ${id}`);
|
|
401
|
+
await appendEvent(tx, id, 'tracking_added', {
|
|
402
|
+
actorId: options?.actorId ?? null,
|
|
403
|
+
note: `Tracking added: ${tracking.carrier} ${tracking.number}.`,
|
|
404
|
+
});
|
|
405
|
+
if (wasPaid) {
|
|
406
|
+
await appendEvent(tx, id, 'status_changed', {
|
|
407
|
+
fromStatus: 'paid',
|
|
408
|
+
toStatus: 'shipped',
|
|
409
|
+
actorId: options?.actorId ?? null,
|
|
410
|
+
note: 'Shipped, tracking attached.',
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
const updated = await read(id, tx);
|
|
414
|
+
if (updated === null) {
|
|
415
|
+
throw new CogentaError({
|
|
416
|
+
code: 'COMMERCE_ORDER_NOT_FOUND',
|
|
417
|
+
message: 'This order disappeared while it was being updated.',
|
|
418
|
+
hint: 'Refresh the order list.',
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
return updated;
|
|
422
|
+
}, { immediate: true });
|
|
423
|
+
},
|
|
290
424
|
};
|
|
425
|
+
return store;
|
|
291
426
|
}
|
|
292
427
|
/** Internal. Unwinds the placement transaction with a caller-facing outcome. */
|
|
293
428
|
class PlacementRefused extends Error {
|