@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.
Files changed (95) hide show
  1. package/dist/admin/permissions.d.ts +8 -0
  2. package/dist/admin/permissions.d.ts.map +1 -1
  3. package/dist/admin/permissions.js +1 -0
  4. package/dist/admin/permissions.js.map +1 -1
  5. package/dist/admin/router.d.ts +47 -5
  6. package/dist/admin/router.d.ts.map +1 -1
  7. package/dist/admin/router.js +942 -24
  8. package/dist/admin/router.js.map +1 -1
  9. package/dist/cart/store.d.ts +23 -0
  10. package/dist/cart/store.d.ts.map +1 -1
  11. package/dist/cart/store.js +49 -2
  12. package/dist/cart/store.js.map +1 -1
  13. package/dist/catalog/csv.d.ts +29 -0
  14. package/dist/catalog/csv.d.ts.map +1 -0
  15. package/dist/catalog/csv.js +396 -0
  16. package/dist/catalog/csv.js.map +1 -0
  17. package/dist/catalog/store.d.ts +24 -4
  18. package/dist/catalog/store.d.ts.map +1 -1
  19. package/dist/catalog/store.js +238 -11
  20. package/dist/catalog/store.js.map +1 -1
  21. package/dist/catalog/types.d.ts +92 -0
  22. package/dist/catalog/types.d.ts.map +1 -1
  23. package/dist/catalog/types.js +20 -0
  24. package/dist/catalog/types.js.map +1 -1
  25. package/dist/coupon/store.d.ts +63 -5
  26. package/dist/coupon/store.d.ts.map +1 -1
  27. package/dist/coupon/store.js +140 -7
  28. package/dist/coupon/store.js.map +1 -1
  29. package/dist/customer/store.d.ts +17 -0
  30. package/dist/customer/store.d.ts.map +1 -1
  31. package/dist/customer/store.js +26 -0
  32. package/dist/customer/store.js.map +1 -1
  33. package/dist/index.d.ts +18 -8
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js +9 -3
  36. package/dist/index.js.map +1 -1
  37. package/dist/invoice/credit-note.d.ts +45 -0
  38. package/dist/invoice/credit-note.d.ts.map +1 -0
  39. package/dist/invoice/credit-note.js +109 -0
  40. package/dist/invoice/credit-note.js.map +1 -0
  41. package/dist/invoice/sequence.d.ts +21 -0
  42. package/dist/invoice/sequence.d.ts.map +1 -0
  43. package/dist/invoice/sequence.js +64 -0
  44. package/dist/invoice/sequence.js.map +1 -0
  45. package/dist/invoice/store.d.ts +23 -1
  46. package/dist/invoice/store.d.ts.map +1 -1
  47. package/dist/invoice/store.js +23 -56
  48. package/dist/invoice/store.js.map +1 -1
  49. package/dist/order/csv.d.ts +24 -0
  50. package/dist/order/csv.d.ts.map +1 -0
  51. package/dist/order/csv.js +42 -0
  52. package/dist/order/csv.js.map +1 -0
  53. package/dist/order/notify.d.ts +65 -0
  54. package/dist/order/notify.d.ts.map +1 -0
  55. package/dist/order/notify.js +180 -0
  56. package/dist/order/notify.js.map +1 -0
  57. package/dist/order/store.d.ts +58 -1
  58. package/dist/order/store.d.ts.map +1 -1
  59. package/dist/order/store.js +145 -10
  60. package/dist/order/store.js.map +1 -1
  61. package/dist/order/types.d.ts +35 -1
  62. package/dist/order/types.d.ts.map +1 -1
  63. package/dist/order/types.js +2 -0
  64. package/dist/order/types.js.map +1 -1
  65. package/dist/payment/paypal.d.ts +9 -0
  66. package/dist/payment/paypal.d.ts.map +1 -0
  67. package/dist/payment/paypal.js +583 -0
  68. package/dist/payment/paypal.js.map +1 -0
  69. package/dist/payment/registry.d.ts +15 -6
  70. package/dist/payment/registry.d.ts.map +1 -1
  71. package/dist/payment/registry.js +17 -6
  72. package/dist/payment/registry.js.map +1 -1
  73. package/dist/payment/types.d.ts +6 -0
  74. package/dist/payment/types.d.ts.map +1 -1
  75. package/dist/shipping/store.d.ts +29 -3
  76. package/dist/shipping/store.d.ts.map +1 -1
  77. package/dist/shipping/store.js +68 -4
  78. package/dist/shipping/store.js.map +1 -1
  79. package/dist/subscription/renewal-notifier.d.ts +17 -0
  80. package/dist/subscription/renewal-notifier.d.ts.map +1 -0
  81. package/dist/subscription/renewal-notifier.js +33 -0
  82. package/dist/subscription/renewal-notifier.js.map +1 -0
  83. package/dist/subscription/store.d.ts +126 -3
  84. package/dist/subscription/store.d.ts.map +1 -1
  85. package/dist/subscription/store.js +348 -8
  86. package/dist/subscription/store.js.map +1 -1
  87. package/dist/tables.d.ts +17 -0
  88. package/dist/tables.d.ts.map +1 -1
  89. package/dist/tables.js +273 -0
  90. package/dist/tables.js.map +1 -1
  91. package/dist/tax/store.d.ts +20 -0
  92. package/dist/tax/store.d.ts.map +1 -1
  93. package/dist/tax/store.js +51 -7
  94. package/dist/tax/store.js.map +1 -1
  95. 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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"}
@@ -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":"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"}
1
+ {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/order/store.ts"],"names":[],"mappings":"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"}
@@ -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
- return {
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 check = await dependencies.coupons.check(cart.couponCode, totals.subtotalMinor, cart.currency);
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}, ${at}, ${at}, ${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
- let statement = sql `select * from ${orders}`;
209
- if (options?.status !== undefined && options.customerId !== undefined) {
210
- statement = sql `${statement} where status = ${options.status} and customer_id = ${options.customerId}`;
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
- else if (options?.status !== undefined) {
213
- statement = sql `${statement} where status = ${options.status}`;
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
- else if (options?.customerId !== undefined) {
216
- statement = sql `${statement} where customer_id = ${options.customerId}`;
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 {