@comity/order 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +73 -0
  3. package/dist/cjs/contracts/address-snapshot.js +3 -0
  4. package/dist/cjs/contracts/contact.js +3 -0
  5. package/dist/cjs/contracts/customer-snapshot.js +3 -0
  6. package/dist/cjs/contracts/item.js +3 -0
  7. package/dist/cjs/contracts/order-repository.js +3 -0
  8. package/dist/cjs/contracts/order.js +3 -0
  9. package/dist/cjs/contracts/payment-snapshot.js +3 -0
  10. package/dist/cjs/domain/order-transitions.js +52 -0
  11. package/dist/cjs/entities/order.js +459 -0
  12. package/dist/cjs/errors/index.js +6 -0
  13. package/dist/cjs/errors/order.js +39 -0
  14. package/dist/cjs/index.js +8 -0
  15. package/dist/cjs/repositories/index.js +6 -0
  16. package/dist/cjs/repositories/memory.js +69 -0
  17. package/dist/cjs/value-objects/order-id.js +70 -0
  18. package/dist/esm/contracts/address-snapshot.js +2 -0
  19. package/dist/esm/contracts/contact.js +2 -0
  20. package/dist/esm/contracts/customer-snapshot.js +2 -0
  21. package/dist/esm/contracts/item.js +2 -0
  22. package/dist/esm/contracts/order-repository.js +2 -0
  23. package/dist/esm/contracts/order.js +2 -0
  24. package/dist/esm/contracts/payment-snapshot.js +2 -0
  25. package/dist/esm/domain/order-transitions.js +48 -0
  26. package/dist/esm/entities/order.js +455 -0
  27. package/dist/esm/errors/index.js +2 -0
  28. package/dist/esm/errors/order.js +35 -0
  29. package/dist/esm/index.js +3 -0
  30. package/dist/esm/repositories/index.js +2 -0
  31. package/dist/esm/repositories/memory.js +65 -0
  32. package/dist/esm/value-objects/order-id.js +66 -0
  33. package/dist/types/contracts/address-snapshot.d.ts +35 -0
  34. package/dist/types/contracts/contact.d.ts +14 -0
  35. package/dist/types/contracts/customer-snapshot.d.ts +24 -0
  36. package/dist/types/contracts/item.d.ts +100 -0
  37. package/dist/types/contracts/order-repository.d.ts +82 -0
  38. package/dist/types/contracts/order.d.ts +108 -0
  39. package/dist/types/contracts/payment-snapshot.d.ts +36 -0
  40. package/dist/types/domain/order-transitions.d.ts +22 -0
  41. package/dist/types/entities/order.d.ts +168 -0
  42. package/dist/types/errors/index.d.ts +2 -0
  43. package/dist/types/errors/order.d.ts +44 -0
  44. package/dist/types/index.d.ts +9 -0
  45. package/dist/types/repositories/index.d.ts +1 -0
  46. package/dist/types/repositories/memory.d.ts +26 -0
  47. package/dist/types/value-objects/order-id.d.ts +50 -0
  48. package/package.json +102 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Filippo Bovo and contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,73 @@
1
+ # @comity/order
2
+
3
+ Order domain contracts for Comity commerce modules.
4
+
5
+ ---
6
+
7
+ ## Purpose
8
+
9
+ Defines order contracts and models consumed by commerce and storefront modules. Unifies cart and order into a single domain where a cart is an order in draft status. Provides repositories, item models, and structured errors without binding to a specific commerce backend.
10
+
11
+ ---
12
+
13
+ ## Scope
14
+
15
+ This package:
16
+
17
+ - ✅ defines the order entity with lifecycle and item mutations
18
+ - ✅ defines order, item, and product contracts with status lifecycle
19
+ - ✅ defines order repository contracts
20
+ - ✅ exposes the `error` subpath for order-specific error types
21
+
22
+ This package does NOT:
23
+
24
+ - ❌ implement checkout orchestration or payment processing
25
+ - ❌ manage inventory or pricing logic
26
+ - ❌ validate coupons or apply promotion rules
27
+ - ❌ render order UI
28
+
29
+ ---
30
+
31
+ ## Public API
32
+
33
+ - `Order` — order entity with lifecycle and item mutations (`addItem`, `removeItem`, `updateItemQuantity`, `submit`, `confirm`, `fulfill`, `cancel`)
34
+ - `OrderId` — order identifier value object
35
+ - `OrderRepository` — persistence-boundary contract (`getById`, `search`, `save`)
36
+ - `OrderState`, `OrderCreate`, `OrderUpdate`, `OrderSnapshot`, `OrderData`, `OrderStatus` — domain contracts
37
+ - `OrderItem`, `OrderProductSnapshot` (with `OrderVariantSnapshot`, `OrderProductAttribute`, `OrderProductOption`) — embedded item value structures
38
+ - Error types (`@comity/order/errors`)
39
+
40
+ The product data in an order is an **owned, immutable snapshot**
41
+ (`OrderProductSnapshot`): it is self-contained and never references the
42
+ catalog after order creation. The application/checkout maps
43
+ `ProductProjection` into the snapshot at creation time.
44
+
45
+ Price composition is owned by `@comity/pricing`: orders store the immutable
46
+ `Price` value (line item and order level) and do not store modifiers
47
+ separately — modifiers are accessed only through `price.modifiers`.
48
+
49
+ No exhaustive reference; see docs for constraints.
50
+
51
+ ---
52
+
53
+ ## Documentation
54
+
55
+ - docs/overview.md
56
+ - docs/conventions.md
57
+
58
+ ---
59
+
60
+ ## Related Packages
61
+
62
+ - @comity/pricing — price and money models (`order → pricing`, registered in ADR-008)
63
+ - @comity/catalog — source of `ProductProjection`, mapped by the application into owned `OrderProductSnapshot` (no direct dependency)
64
+ - @comity/kernel — module lifecycle runtime
65
+
66
+ ---
67
+
68
+ ## Status
69
+
70
+ Stable
71
+
72
+ _Review Completed: 2026-08-01_
73
+ _Compliance Score: N/A% (Green)_
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ //# sourceMappingURL=address-snapshot.js.map
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ //# sourceMappingURL=contact.js.map
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ //# sourceMappingURL=customer-snapshot.js.map
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ //# sourceMappingURL=item.js.map
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ //# sourceMappingURL=order-repository.js.map
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ //# sourceMappingURL=order.js.map
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ //# sourceMappingURL=payment-snapshot.js.map
@@ -0,0 +1,52 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.canTransition = canTransition;
4
+ exports.transitionOrderStatus = transitionOrderStatus;
5
+ const result_1 = require("@comity/primitives/result");
6
+ const order_js_1 = require("../errors/order.js");
7
+ /**
8
+ * Legal transitions for each order status.
9
+ *
10
+ * - `draft` → `pending` (submit)
11
+ * - `pending` → `confirmed` (confirm)
12
+ * - `confirmed` → `fulfilled` (fulfill)
13
+ * - `draft | pending | confirmed` → `cancelled` (cancel)
14
+ *
15
+ * `fulfilled` and `cancelled` are terminal: no transition is legal from them.
16
+ */
17
+ const TRANSITIONS = {
18
+ draft: ["pending", "cancelled"],
19
+ pending: ["confirmed", "cancelled"],
20
+ confirmed: ["fulfilled", "cancelled"],
21
+ fulfilled: [],
22
+ cancelled: [],
23
+ };
24
+ /**
25
+ * Checks whether a status transition is legal.
26
+ *
27
+ * @param from - Source status.
28
+ * @param to - Target status.
29
+ *
30
+ * @returns True if the transition is allowed, false otherwise.
31
+ */
32
+ function canTransition(from, to) {
33
+ return TRANSITIONS[from].includes(to);
34
+ }
35
+ /**
36
+ * Applies a status transition.
37
+ *
38
+ * @param from - Source status.
39
+ * @param to - Target status.
40
+ *
41
+ * @returns The target status, or an `invalid_status_transition` error when the
42
+ * transition is not allowed.
43
+ */
44
+ function transitionOrderStatus(from, to) {
45
+ if (!canTransition(from, to)) {
46
+ return (0, result_1.failure)(new order_js_1.OrderError("invalid_status_transition", {
47
+ details: { from, to },
48
+ }));
49
+ }
50
+ return (0, result_1.success)(to);
51
+ }
52
+ //# sourceMappingURL=order-transitions.js.map
@@ -0,0 +1,459 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Order = void 0;
4
+ const result_1 = require("@comity/primitives/result");
5
+ const time_1 = require("@comity/primitives/time");
6
+ const order_transitions_js_1 = require("../domain/order-transitions.js");
7
+ const order_js_1 = require("../errors/order.js");
8
+ /**
9
+ * Returns a defensive copy of a product snapshot so later mutations of the
10
+ * input cannot leak into the order.
11
+ *
12
+ * @param snapshot - The product snapshot to copy.
13
+ *
14
+ * @returns A defensive copy of the snapshot.
15
+ */
16
+ function copyProductSnapshot(snapshot) {
17
+ /**
18
+ * Returns a defensive copy of a variant snapshot.
19
+ *
20
+ * @param variant - The variant snapshot to copy.
21
+ *
22
+ * @returns A defensive copy of the variant.
23
+ */
24
+ const copyVariant = (variant) => ({
25
+ ...variant,
26
+ ...(variant.attributes !== undefined
27
+ ? { attributes: variant.attributes.map((attribute) => ({ ...attribute })) }
28
+ : {}),
29
+ ...(variant.options !== undefined
30
+ ? { options: variant.options.map((option) => ({ ...option })) }
31
+ : {}),
32
+ });
33
+ return {
34
+ ...snapshot,
35
+ ...(snapshot.attributes !== undefined
36
+ ? { attributes: snapshot.attributes.map((attribute) => ({ ...attribute })) }
37
+ : {}),
38
+ ...(snapshot.options !== undefined
39
+ ? { options: snapshot.options.map((option) => ({ ...option })) }
40
+ : {}),
41
+ ...(snapshot.variant !== undefined ? { variant: copyVariant(snapshot.variant) } : {}),
42
+ ...(snapshot.metadata !== undefined ? { metadata: { ...snapshot.metadata } } : {}),
43
+ };
44
+ }
45
+ /**
46
+ * Returns a defensive copy of a contact so later mutations of the input cannot
47
+ * leak into the order.
48
+ *
49
+ * @param contact - The contact to copy.
50
+ *
51
+ * @returns A defensive copy of the contact.
52
+ */
53
+ function copyContact(contact) {
54
+ return { ...contact };
55
+ }
56
+ /**
57
+ * Returns a defensive copy of the buyer fact so later mutations of the input
58
+ * cannot leak into the order.
59
+ *
60
+ * @param customer - The customer fact to copy.
61
+ *
62
+ * @returns A defensive copy of the customer fact.
63
+ */
64
+ function copyCustomerSnapshot(customer) {
65
+ return {
66
+ ...customer,
67
+ ...(customer.contacts !== undefined
68
+ ? { contacts: customer.contacts.map((contact) => copyContact(contact)) }
69
+ : {}),
70
+ };
71
+ }
72
+ /**
73
+ * Returns a defensive copy of an address fact so later mutations of the input
74
+ * cannot leak into the order.
75
+ *
76
+ * @param address - The address fact to copy.
77
+ *
78
+ * @returns A defensive copy of the address fact.
79
+ */
80
+ function copyAddressSnapshot(address) {
81
+ return {
82
+ ...address,
83
+ lines: [...address.lines],
84
+ };
85
+ }
86
+ /**
87
+ * Returns whether a stored address fact and a replacement destination are
88
+ * structurally identical.
89
+ *
90
+ * @param address - The address fact stored on the order.
91
+ * @param destination - The replacement destination to compare against.
92
+ *
93
+ * @returns `true` when every field matches, otherwise `false`.
94
+ */
95
+ function isSameShippingDestination(address, destination) {
96
+ return (address.addressId === destination.addressId &&
97
+ address.lines.length === destination.lines.length &&
98
+ address.lines.every((line, index) => line === destination.lines[index]) &&
99
+ address.city === destination.city &&
100
+ address.administrativeArea === destination.administrativeArea &&
101
+ address.postalCode === destination.postalCode &&
102
+ address.countryCode === destination.countryCode &&
103
+ address.capturedAt.epochMilliseconds === destination.capturedAt.epochMilliseconds);
104
+ }
105
+ /**
106
+ * Represents an order entity in the system.
107
+ *
108
+ * @remark
109
+ * An order is a lifecycle aggregate: it owns its status transitions, its line
110
+ * items, the applied pricing result, and its historical facts (buyer,
111
+ * addresses, payment). It does not validate coupons, does not know promotion
112
+ * rules, and does not orchestrate external services — those concerns belong
113
+ * to `@comity/pricing`, other Core Modules, and application/domain services.
114
+ */
115
+ class Order {
116
+ #id;
117
+ #status;
118
+ #items;
119
+ #price;
120
+ #channelId;
121
+ #customer;
122
+ #addresses;
123
+ #payments;
124
+ #meta;
125
+ #createdAt;
126
+ #updatedAt;
127
+ /**
128
+ * @param fields - The fields used to create or hydrate the order.
129
+ * @param id - The unique identifier of the order, if it has been assigned.
130
+ */
131
+ constructor(fields, id) {
132
+ this.#id = id;
133
+ this.#status = fields.status ?? "draft";
134
+ this.#items = [...fields.items];
135
+ this.#price = fields.price;
136
+ this.#channelId = fields.channelId;
137
+ this.#customer = fields.customer !== undefined ? copyCustomerSnapshot(fields.customer) : undefined;
138
+ this.#addresses =
139
+ fields.addresses !== undefined ? fields.addresses.map((address) => copyAddressSnapshot(address)) : undefined;
140
+ this.#payments =
141
+ fields.payments !== undefined
142
+ ? fields.payments.map((payment) => ({ ...payment }))
143
+ : undefined;
144
+ this.#meta = fields.meta ? { ...fields.meta } : undefined;
145
+ this.#createdAt = fields.createdAt ?? time_1.Instant.now();
146
+ this.#updatedAt = fields.updatedAt ?? this.#createdAt;
147
+ }
148
+ /**
149
+ * @returns The unique identifier of the order, if it has been assigned.
150
+ */
151
+ get id() {
152
+ return this.#id;
153
+ }
154
+ /**
155
+ * @returns The commercial channel through which the order was placed.
156
+ */
157
+ get channelId() {
158
+ return this.#channelId;
159
+ }
160
+ /**
161
+ * @returns The lifecycle status of the order.
162
+ */
163
+ get status() {
164
+ return this.#status;
165
+ }
166
+ /**
167
+ * @returns The order items.
168
+ */
169
+ get items() {
170
+ return [...this.#items];
171
+ }
172
+ /**
173
+ * @returns The order price.
174
+ */
175
+ get price() {
176
+ return this.#price;
177
+ }
178
+ /**
179
+ * @returns The historical buyer fact, if captured.
180
+ */
181
+ get customer() {
182
+ return this.#customer !== undefined
183
+ ? {
184
+ ...this.#customer,
185
+ ...(this.#customer.contacts !== undefined
186
+ ? { contacts: this.#customer.contacts.map((contact) => ({ ...contact })) }
187
+ : {}),
188
+ }
189
+ : undefined;
190
+ }
191
+ /**
192
+ * @returns The historical address facts, if captured.
193
+ */
194
+ get addresses() {
195
+ return this.#addresses !== undefined
196
+ ? this.#addresses.map((address) => ({ ...address, lines: [...address.lines] }))
197
+ : undefined;
198
+ }
199
+ /**
200
+ * @returns The historical payment facts, if attached.
201
+ */
202
+ get payments() {
203
+ return this.#payments?.map((payment) => ({ ...payment }));
204
+ }
205
+ /**
206
+ * @returns The custom metadata of the order, if any.
207
+ */
208
+ get meta() {
209
+ return this.#meta ? { ...this.#meta } : undefined;
210
+ }
211
+ /**
212
+ * @returns The timestamp when the order was created.
213
+ */
214
+ get createdAt() {
215
+ return this.#createdAt;
216
+ }
217
+ /**
218
+ * @returns The timestamp when the order was last updated.
219
+ */
220
+ get updatedAt() {
221
+ return this.#updatedAt;
222
+ }
223
+ /**
224
+ * Updates the applied pricing result and metadata.
225
+ *
226
+ * Status transitions are performed through the domain methods; items are
227
+ * mutated through `addItem`/`removeItem`/`updateItemQuantity`.
228
+ *
229
+ * @param changes - The changes to apply to the order.
230
+ */
231
+ update(changes) {
232
+ if (changes.price !== undefined) {
233
+ this.#price = changes.price;
234
+ }
235
+ if (changes.meta !== undefined) {
236
+ this.#meta = { ...changes.meta };
237
+ }
238
+ this.#updatedAt = time_1.Instant.now();
239
+ }
240
+ /**
241
+ * Appends a historical payment fact to the order.
242
+ *
243
+ * A payment may be processed after the order is created, so the outcome is
244
+ * recorded through a dedicated method rather than the generic update path.
245
+ * This records a historical fact only: it performs no payment logic, knows
246
+ * no payment module, and does not coordinate, authorize, or capture.
247
+ *
248
+ * @param snapshot - The payment fact to record.
249
+ */
250
+ attachPayment(snapshot) {
251
+ this.#payments = [
252
+ ...(this.#payments ?? []),
253
+ { ...snapshot },
254
+ ];
255
+ this.#updatedAt = time_1.Instant.now();
256
+ }
257
+ /**
258
+ * Replaces the shipping destination of the order.
259
+ *
260
+ * Allowed only while the order is `draft | pending`; in later statuses the
261
+ * shipping destination is immutable (ADR-018). The order must hold exactly
262
+ * one shipping address fact; a missing or ambiguous one is an error rather
263
+ * than a guess. A structurally identical destination is a successful no-op
264
+ * that leaves timestamps untouched. The operation never reaches the generic
265
+ * `update()` path, never touches billing, customer, item, price, or payment
266
+ * facts, and records no history: the aggregate retains only the current
267
+ * shipping destination.
268
+ *
269
+ * @param destination - The replacement shipping destination. The address
270
+ * role is owned by this operation and is always stored as `"shipping"`.
271
+ *
272
+ * @returns A result indicating the success or failure of the change.
273
+ */
274
+ changeShippingDestination(destination) {
275
+ if (this.#status !== "draft" && this.#status !== "pending") {
276
+ return (0, result_1.failure)(new order_js_1.OrderError("shipping_destination_immutable", {
277
+ details: {
278
+ ...(this.#id !== undefined ? { orderId: this.#id.toString() } : {}),
279
+ },
280
+ }));
281
+ }
282
+ const addresses = this.#addresses;
283
+ const shipping = (addresses ?? []).filter((address) => address.role === "shipping");
284
+ const current = shipping.length === 1 ? shipping[0] : undefined;
285
+ if (current === undefined || addresses === undefined) {
286
+ return (0, result_1.failure)(new order_js_1.OrderError("ambiguous_shipping_destination", {
287
+ details: {
288
+ ...(this.#id !== undefined ? { orderId: this.#id.toString() } : {}),
289
+ },
290
+ }));
291
+ }
292
+ if (isSameShippingDestination(current, destination)) {
293
+ return (0, result_1.success)(undefined);
294
+ }
295
+ this.#addresses = addresses.map((address) => address.role === "shipping" ? copyAddressSnapshot({ ...destination, role: "shipping" }) : address);
296
+ this.#updatedAt = time_1.Instant.now();
297
+ return (0, result_1.success)(undefined);
298
+ }
299
+ /**
300
+ * Adds an item to the order.
301
+ *
302
+ * @param input - The item data to add.
303
+ *
304
+ * @returns The created item, or an `invalid_quantity` error when the
305
+ * quantity is not a positive integer.
306
+ */
307
+ addItem(input) {
308
+ if (!Number.isInteger(input.quantity) || input.quantity < 1) {
309
+ return (0, result_1.failure)(new order_js_1.OrderError("invalid_quantity", {
310
+ details: {
311
+ ...(this.#id !== undefined ? { orderId: this.#id.toString() } : {}),
312
+ field: "quantity",
313
+ },
314
+ }));
315
+ }
316
+ const item = {
317
+ id: crypto.randomUUID(),
318
+ product: copyProductSnapshot(input.product),
319
+ quantity: input.quantity,
320
+ price: input.price,
321
+ };
322
+ this.#items.push(item);
323
+ this.#updatedAt = time_1.Instant.now();
324
+ return (0, result_1.success)(item);
325
+ }
326
+ /**
327
+ * Removes an item from the order.
328
+ *
329
+ * @param itemId - The order item ID.
330
+ *
331
+ * @returns A result indicating the success or failure of the removal.
332
+ */
333
+ removeItem(itemId) {
334
+ const index = this.#items.findIndex((item) => item.id === itemId);
335
+ if (index === -1) {
336
+ return (0, result_1.failure)(new order_js_1.OrderError("invalid_item", {
337
+ details: {
338
+ ...(this.#id !== undefined ? { orderId: this.#id.toString() } : {}),
339
+ itemId,
340
+ },
341
+ }));
342
+ }
343
+ this.#items.splice(index, 1);
344
+ this.#updatedAt = time_1.Instant.now();
345
+ return (0, result_1.success)(undefined);
346
+ }
347
+ /**
348
+ * Updates the quantity of an order item.
349
+ *
350
+ * @param itemId - The order item ID.
351
+ * @param quantity - The new quantity.
352
+ *
353
+ * @returns A result indicating the success or failure of the update.
354
+ */
355
+ updateItemQuantity(itemId, quantity) {
356
+ if (!Number.isInteger(quantity) || quantity < 1) {
357
+ return (0, result_1.failure)(new order_js_1.OrderError("invalid_quantity", {
358
+ details: {
359
+ ...(this.#id !== undefined ? { orderId: this.#id.toString() } : {}),
360
+ itemId,
361
+ field: "quantity",
362
+ },
363
+ }));
364
+ }
365
+ const index = this.#items.findIndex((candidate) => candidate.id === itemId);
366
+ if (index === -1) {
367
+ return (0, result_1.failure)(new order_js_1.OrderError("invalid_item", {
368
+ details: {
369
+ ...(this.#id !== undefined ? { orderId: this.#id.toString() } : {}),
370
+ itemId,
371
+ },
372
+ }));
373
+ }
374
+ const current = this.#items[index];
375
+ if (current !== undefined) {
376
+ this.#items[index] = { ...current, quantity };
377
+ this.#updatedAt = time_1.Instant.now();
378
+ }
379
+ return (0, result_1.success)(undefined);
380
+ }
381
+ /**
382
+ * Submits the order: `draft` → `pending`.
383
+ *
384
+ * @returns A result indicating the success or failure of the transition.
385
+ */
386
+ submit() {
387
+ return this.#transition("pending");
388
+ }
389
+ /**
390
+ * Confirms the order: `pending` → `confirmed`.
391
+ *
392
+ * @returns A result indicating the success or failure of the transition.
393
+ */
394
+ confirm() {
395
+ return this.#transition("confirmed");
396
+ }
397
+ /**
398
+ * Fulfills the order: `confirmed` → `fulfilled`.
399
+ *
400
+ * @returns A result indicating the success or failure of the transition.
401
+ */
402
+ fulfill() {
403
+ return this.#transition("fulfilled");
404
+ }
405
+ /**
406
+ * Cancels the order: `draft | pending | confirmed` → `cancelled`.
407
+ *
408
+ * @returns A result indicating the success or failure of the transition.
409
+ */
410
+ cancel() {
411
+ return this.#transition("cancelled");
412
+ }
413
+ /**
414
+ * Creates a snapshot of the current state of the order.
415
+ * Requires the order to have an assigned identifier.
416
+ *
417
+ * @returns A snapshot representing the current state of the order.
418
+ */
419
+ snapshot() {
420
+ return {
421
+ id: this.#id,
422
+ status: this.#status,
423
+ items: [...this.#items],
424
+ price: this.#price,
425
+ channelId: this.#channelId,
426
+ ...(this.#customer !== undefined ? { customer: copyCustomerSnapshot(this.#customer) } : {}),
427
+ ...(this.#addresses !== undefined
428
+ ? { addresses: this.#addresses.map((address) => copyAddressSnapshot(address)) }
429
+ : {}),
430
+ ...(this.#payments !== undefined
431
+ ? {
432
+ payments: this.#payments.map((payment) => ({ ...payment })),
433
+ }
434
+ : {}),
435
+ ...(this.#meta !== undefined ? { meta: { ...this.#meta } } : {}),
436
+ createdAt: this.#createdAt,
437
+ updatedAt: this.#updatedAt,
438
+ capturedAt: time_1.Instant.now(),
439
+ };
440
+ }
441
+ /**
442
+ * Applies a lifecycle transition through the centralized transition rules.
443
+ *
444
+ * @param to - The target status.
445
+ *
446
+ * @returns A result indicating the success or failure of the transition.
447
+ */
448
+ #transition(to) {
449
+ const result = (0, order_transitions_js_1.transitionOrderStatus)(this.#status, to);
450
+ if (!result.success) {
451
+ return result;
452
+ }
453
+ this.#status = to;
454
+ this.#updatedAt = time_1.Instant.now();
455
+ return (0, result_1.success)(undefined);
456
+ }
457
+ }
458
+ exports.Order = Order;
459
+ //# sourceMappingURL=order.js.map
@@ -0,0 +1,6 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.OrderError = void 0;
4
+ var order_js_1 = require("./order.js");
5
+ Object.defineProperty(exports, "OrderError", { enumerable: true, get: function () { return order_js_1.OrderError; } });
6
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,39 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.OrderError = void 0;
4
+ const errors_1 = require("@comity/primitives/errors");
5
+ const REASON_MESSAGES = {
6
+ invalid_quantity: "Invalid order item quantity",
7
+ invalid_item: "Order item not found",
8
+ invalid_status_transition: "Invalid order status transition",
9
+ shipping_destination_immutable: "Shipping destination cannot be changed in the current order status",
10
+ ambiguous_shipping_destination: "Order must contain exactly one shipping destination",
11
+ };
12
+ const REASON_HTTP_STATUS = {
13
+ invalid_quantity: 400,
14
+ invalid_item: 400,
15
+ invalid_status_transition: 409,
16
+ shipping_destination_immutable: 409,
17
+ ambiguous_shipping_destination: 409,
18
+ };
19
+ /**
20
+ * Order operation error with typed reasons.
21
+ */
22
+ class OrderError extends errors_1.BaseError {
23
+ /** Error code. */
24
+ code;
25
+ /**
26
+ * @param reason - The reason for the order error.
27
+ * @param meta - Additional metadata for the error.
28
+ */
29
+ constructor(reason, meta) {
30
+ super(REASON_MESSAGES[reason], {
31
+ httpStatus: REASON_HTTP_STATUS[reason],
32
+ ...meta,
33
+ reason,
34
+ });
35
+ this.code = `order:${reason}`;
36
+ }
37
+ }
38
+ exports.OrderError = OrderError;
39
+ //# sourceMappingURL=order.js.map