@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
package/dist/index.js
CHANGED
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
export { COMMERCE_ANONYMOUS, COMMERCE_PERMISSIONS, createCommercePermissions, DEFAULT_COMMERCE_ROLES, } from './admin/permissions.js';
|
|
2
2
|
export { createCommerceAdminRouter } from './admin/router.js';
|
|
3
|
-
export { CART_STATUSES, CART_TTL_MS, couponRefusal, createCartStore } from './cart/store.js';
|
|
3
|
+
export { CART_STATUSES, CART_TTL_MS, couponRefusal, createCartStore, DEFAULT_CART_ABANDON_MS, } from './cart/store.js';
|
|
4
4
|
export { computeTotals } from './cart/totals.js';
|
|
5
|
+
export { applyProductsImport, exportProductsCsv, previewProductsImport } from './catalog/csv.js';
|
|
5
6
|
export { createCatalogStore } from './catalog/store.js';
|
|
6
|
-
export { PRODUCT_STATUSES } from './catalog/types.js';
|
|
7
|
+
export { isOnSale, PRODUCT_STATUSES, STOCK_MOVEMENT_REASONS } from './catalog/types.js';
|
|
7
8
|
export { COUPON_KINDS, createCouponStore, discountFor, normaliseCode, } from './coupon/store.js';
|
|
8
9
|
export { createCustomerStore, normaliseEmail } from './customer/store.js';
|
|
10
|
+
export { createCreditNoteStore } from './invoice/credit-note.js';
|
|
9
11
|
export { renderInvoicePdf } from './invoice/pdf.js';
|
|
12
|
+
export { claimSequenceNumber, formatSequenceNumber } from './invoice/sequence.js';
|
|
10
13
|
export { createInvoiceStore, formatInvoiceNumber, pdfDocumentFor } from './invoice/store.js';
|
|
11
14
|
export { applyBasisPoints, assertCurrency, assertMinor, assertSameCurrency, distribute, formatMoney, minorUnitExponent, normaliseCurrency, } from './money.js';
|
|
15
|
+
export { ordersToCsv } from './order/csv.js';
|
|
16
|
+
export { buildConfirmationMessage, buildShipmentMessage, createOrderEmailQueue, MAX_ATTEMPTS as ORDER_EMAIL_MAX_ATTEMPTS, ORDER_EMAIL_KINDS, } from './order/notify.js';
|
|
12
17
|
export { createOrderStore, referenceFrom } from './order/store.js';
|
|
13
18
|
export { assertTransition, canTransition, holdsStock, ORDER_EVENT_KINDS, ORDER_STATUSES, } from './order/types.js';
|
|
14
19
|
export { createManualPaymentGateway, manualPaymentDriver, transferReference, } from './payment/manual.js';
|
|
@@ -17,7 +22,8 @@ export { createPaymentStore } from './payment/store.js';
|
|
|
17
22
|
export { stripePaymentDriver } from './payment/stripe.js';
|
|
18
23
|
export { PAYMENT_STATUSES } from './payment/types.js';
|
|
19
24
|
export { createShippingStore, SHIPPING_KINDS, storedRate } from './shipping/store.js';
|
|
20
|
-
export {
|
|
25
|
+
export { createEmailRenewalNotifier } from './subscription/renewal-notifier.js';
|
|
26
|
+
export { advancePeriod, createSubscriptionStore, DEFAULT_DUNNING_SCHEDULE_DAYS, DEFAULT_RENEWAL_NOTICE_DAYS, INTERVAL_UNITS, SUBSCRIPTION_STATUSES, } from './subscription/store.js';
|
|
21
27
|
export { ensureCommerceTables, TABLES } from './tables.js';
|
|
22
28
|
export { createTaxStore, taxFor } from './tax/store.js';
|
|
23
29
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAMA,OAAO,EACL,kBAAkB,EAClB,oBAAoB,EACpB,yBAAyB,EACzB,sBAAsB,GACvB,MAAM,wBAAwB,CAAA;AAO/B,OAAO,EAAE,yBAAyB,EAAE,MAAM,mBAAmB,CAAA;AAU7D,OAAO,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAMA,OAAO,EACL,kBAAkB,EAClB,oBAAoB,EACpB,yBAAyB,EACzB,sBAAsB,GACvB,MAAM,wBAAwB,CAAA;AAO/B,OAAO,EAAE,yBAAyB,EAAE,MAAM,mBAAmB,CAAA;AAU7D,OAAO,EACL,aAAa,EACb,WAAW,EACX,aAAa,EACb,eAAe,EACf,uBAAuB,GACxB,MAAM,iBAAiB,CAAA;AAExB,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAA;AAQhD,OAAO,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,qBAAqB,EAAE,MAAM,kBAAkB,CAAA;AAEhG,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAA;AAiBvD,OAAO,EAAE,QAAQ,EAAE,gBAAgB,EAAE,sBAAsB,EAAE,MAAM,oBAAoB,CAAA;AAUvF,OAAO,EACL,YAAY,EACZ,iBAAiB,EACjB,WAAW,EACX,aAAa,GACd,MAAM,mBAAmB,CAAA;AAE1B,OAAO,EAAE,mBAAmB,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAA;AAOzE,OAAO,EAAE,qBAAqB,EAAE,MAAM,0BAA0B,CAAA;AAEhE,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAA;AACnD,OAAO,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAA;AASjF,OAAO,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAA;AAE5F,OAAO,EACL,gBAAgB,EAChB,cAAc,EACd,WAAW,EACX,kBAAkB,EAClB,UAAU,EACV,WAAW,EACX,iBAAiB,EACjB,iBAAiB,GAClB,MAAM,YAAY,CAAA;AAEnB,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AAQ5C,OAAO,EACL,wBAAwB,EACxB,oBAAoB,EACpB,qBAAqB,EACrB,YAAY,IAAI,wBAAwB,EACxC,iBAAiB,GAClB,MAAM,mBAAmB,CAAA;AAU1B,OAAO,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAA;AAUlE,OAAO,EACL,gBAAgB,EAChB,aAAa,EACb,UAAU,EACV,iBAAiB,EACjB,cAAc,GACf,MAAM,kBAAkB,CAAA;AACzB,OAAO,EACL,0BAA0B,EAC1B,mBAAmB,EACnB,iBAAiB,GAClB,MAAM,qBAAqB,CAAA;AAE5B,OAAO,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAA;AAE7D,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAA;AACvD,OAAO,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAA;AAczD,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAA;AAWrD,OAAO,EAAE,mBAAmB,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAA;AACrF,OAAO,EAAE,0BAA0B,EAAE,MAAM,oCAAoC,CAAA;AAoB/E,OAAO,EACL,aAAa,EACb,uBAAuB,EACvB,6BAA6B,EAC7B,2BAA2B,EAC3B,cAAc,EACd,qBAAqB,GACtB,MAAM,yBAAyB,CAAA;AAChC,OAAO,EAAE,oBAAoB,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAQ1D,OAAO,EAAE,cAAc,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAA"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { type DatabaseHandle } from '@cogenta/core';
|
|
2
|
+
import type { OrderStore } from '../order/store.js';
|
|
3
|
+
import type { InvoiceDocument, SellerDetails } from './store.js';
|
|
4
|
+
/**
|
|
5
|
+
* A refund's accounting counterpart (fiche 52 task 6): "Avoir : nouvelle
|
|
6
|
+
* série de numérotation réutilisant le compare-and-set d'`invoice/store.ts`."
|
|
7
|
+
* One credit note per refund — never per order, since an order can be
|
|
8
|
+
* refunded in several instalments and each one needs its own number — issued
|
|
9
|
+
* in its own series (`CN-2026`, distinct from an invoice's own `2026`) so
|
|
10
|
+
* neither numbering can ever collide with or shadow the other, while both
|
|
11
|
+
* still share one compare-and-set sequence table (`sequence.ts`).
|
|
12
|
+
*/
|
|
13
|
+
export interface CreditNote {
|
|
14
|
+
readonly id: string;
|
|
15
|
+
readonly orderId: string;
|
|
16
|
+
readonly refundId: string;
|
|
17
|
+
readonly series: string;
|
|
18
|
+
readonly seq: number;
|
|
19
|
+
readonly number: string;
|
|
20
|
+
readonly issuedAt: string;
|
|
21
|
+
readonly currency: string;
|
|
22
|
+
readonly amountMinor: number;
|
|
23
|
+
readonly reason: string | null;
|
|
24
|
+
readonly document: InvoiceDocument;
|
|
25
|
+
}
|
|
26
|
+
export interface IssueCreditNoteInput {
|
|
27
|
+
readonly orderId: string;
|
|
28
|
+
readonly refundId: string;
|
|
29
|
+
readonly amountMinor: number;
|
|
30
|
+
readonly reason?: string | null;
|
|
31
|
+
readonly series?: string;
|
|
32
|
+
readonly actorId?: string | null;
|
|
33
|
+
}
|
|
34
|
+
export interface CreditNoteStoreDependencies {
|
|
35
|
+
readonly orders: OrderStore;
|
|
36
|
+
readonly seller: SellerDetails;
|
|
37
|
+
}
|
|
38
|
+
export interface CreditNoteStore {
|
|
39
|
+
issue(input: IssueCreditNoteInput): Promise<CreditNote>;
|
|
40
|
+
read(id: string): Promise<CreditNote | null>;
|
|
41
|
+
readByRefund(refundId: string): Promise<CreditNote | null>;
|
|
42
|
+
listForOrder(orderId: string): Promise<readonly CreditNote[]>;
|
|
43
|
+
}
|
|
44
|
+
export declare function createCreditNoteStore(db: DatabaseHandle, dependencies: CreditNoteStoreDependencies, now?: () => number): CreditNoteStore;
|
|
45
|
+
//# sourceMappingURL=credit-note.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"credit-note.d.ts","sourceRoot":"","sources":["../../src/invoice/credit-note.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,cAAc,EAKpB,MAAM,eAAe,CAAA;AACtB,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA;AAKnD,OAAO,KAAK,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAEhE;;;;;;;;GAQG;AACH,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;IACnB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;IAC5B,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;IAC9B,QAAQ,CAAC,QAAQ,EAAE,eAAe,CAAA;CACnC;AAED,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;IAC5B,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC/B,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CACjC;AAED,MAAM,WAAW,2BAA2B;IAC1C,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAA;IAC3B,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAA;CAC/B;AAED,MAAM,WAAW,eAAe;IAC9B,KAAK,CAAC,KAAK,EAAE,oBAAoB,GAAG,OAAO,CAAC,UAAU,CAAC,CAAA;IACvD,IAAI,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAAA;IAC5C,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAAA;IAC1D,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,UAAU,EAAE,CAAC,CAAA;CAC9D;AAoED,wBAAgB,qBAAqB,CACnC,EAAE,EAAE,cAAc,EAClB,YAAY,EAAE,2BAA2B,EACzC,GAAG,GAAE,MAAM,MAAiB,GAC3B,eAAe,CAyFjB"}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { CogentaError, identifier, newId, sql, } from '@cogenta/core';
|
|
2
|
+
import { toInt, toNullableText, toText } from '../rows.js';
|
|
3
|
+
import { TABLES } from '../tables.js';
|
|
4
|
+
import { claimSequenceNumber, formatSequenceNumber } from './sequence.js';
|
|
5
|
+
function decode(row) {
|
|
6
|
+
return {
|
|
7
|
+
id: toText(row.id, 'credit_note.id'),
|
|
8
|
+
orderId: toText(row.order_id, 'credit_note.order_id'),
|
|
9
|
+
refundId: toText(row.refund_id, 'credit_note.refund_id'),
|
|
10
|
+
series: toText(row.series, 'credit_note.series'),
|
|
11
|
+
seq: toInt(row.seq, 'credit_note.seq'),
|
|
12
|
+
number: toText(row.number, 'credit_note.number'),
|
|
13
|
+
issuedAt: toText(row.issued_at, 'credit_note.issued_at'),
|
|
14
|
+
currency: toText(row.currency, 'credit_note.currency'),
|
|
15
|
+
amountMinor: toInt(row.amount_minor, 'credit_note.amount_minor'),
|
|
16
|
+
reason: toNullableText(row.reason),
|
|
17
|
+
document: JSON.parse(toText(row.document, 'credit_note.document')),
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
function documentFor(order, number, issuedAt, amountMinor, seller) {
|
|
21
|
+
return {
|
|
22
|
+
number,
|
|
23
|
+
issuedAt,
|
|
24
|
+
orderReference: order.reference,
|
|
25
|
+
seller: seller.address,
|
|
26
|
+
buyer: [order.email],
|
|
27
|
+
currency: order.currency,
|
|
28
|
+
// A credit note shows what it refunds, not the whole order again — one
|
|
29
|
+
// line, the amount actually returned, rather than re-listing goods the
|
|
30
|
+
// customer kept.
|
|
31
|
+
lines: [
|
|
32
|
+
{
|
|
33
|
+
sku: '—',
|
|
34
|
+
title: `Refund on order ${order.reference}`,
|
|
35
|
+
quantity: 1,
|
|
36
|
+
unitPriceMinor: amountMinor,
|
|
37
|
+
taxRateBp: 0,
|
|
38
|
+
totalMinor: amountMinor,
|
|
39
|
+
},
|
|
40
|
+
],
|
|
41
|
+
subtotalMinor: amountMinor,
|
|
42
|
+
discountMinor: 0,
|
|
43
|
+
shippingMinor: 0,
|
|
44
|
+
taxMinor: 0,
|
|
45
|
+
totalMinor: amountMinor,
|
|
46
|
+
footer: seller.footer ?? null,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
export function createCreditNoteStore(db, dependencies, now = Date.now) {
|
|
50
|
+
const d = db.dialect;
|
|
51
|
+
const table = identifier(TABLES.creditNotes, d);
|
|
52
|
+
async function read(id, executor = db) {
|
|
53
|
+
const result = await executor.query(sql `select * from ${table} where id = ${id}`);
|
|
54
|
+
const row = result.rows[0];
|
|
55
|
+
return row === undefined ? null : decode(row);
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
issue: async (input) => {
|
|
59
|
+
const order = await dependencies.orders.read(input.orderId);
|
|
60
|
+
if (order === null) {
|
|
61
|
+
throw new CogentaError({
|
|
62
|
+
code: 'COMMERCE_ORDER_NOT_FOUND',
|
|
63
|
+
message: 'This order does not exist.',
|
|
64
|
+
hint: 'Check the order reference.',
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
const existing = await db.query(sql `select * from ${table} where refund_id = ${input.refundId}`);
|
|
68
|
+
if (existing.rows[0] !== undefined)
|
|
69
|
+
return decode(existing.rows[0]);
|
|
70
|
+
const issuedAt = new Date(now()).toISOString();
|
|
71
|
+
const series = input.series ?? `CN-${issuedAt.slice(0, 4)}`;
|
|
72
|
+
const id = newId(now);
|
|
73
|
+
return db.transaction(async (tx) => {
|
|
74
|
+
const seq = await claimSequenceNumber(tx, d, series);
|
|
75
|
+
const number = formatSequenceNumber(series, seq);
|
|
76
|
+
const document = documentFor(order, number, issuedAt, input.amountMinor, dependencies.seller);
|
|
77
|
+
await tx.query(sql `
|
|
78
|
+
insert into ${table} (id, order_id, refund_id, series, seq, number, issued_at,
|
|
79
|
+
currency, amount_minor, reason, document)
|
|
80
|
+
values (${id}, ${order.id}, ${input.refundId}, ${series}, ${seq}, ${number}, ${issuedAt},
|
|
81
|
+
${order.currency}, ${input.amountMinor}, ${input.reason ?? null}, ${JSON.stringify(document)})`);
|
|
82
|
+
await dependencies.orders.record(order.id, 'note', {
|
|
83
|
+
note: `Credit note ${number} issued for refund ${input.refundId}.`,
|
|
84
|
+
actorId: input.actorId ?? null,
|
|
85
|
+
}, tx);
|
|
86
|
+
const stored = await read(id, tx);
|
|
87
|
+
if (stored === null) {
|
|
88
|
+
throw new CogentaError({
|
|
89
|
+
code: 'COMMERCE_CREDIT_NOTE_NOT_FOUND',
|
|
90
|
+
message: 'The credit note was not stored.',
|
|
91
|
+
hint: 'Check that the commerce tables exist (ensureCommerceTables).',
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
return stored;
|
|
95
|
+
}, { immediate: true });
|
|
96
|
+
},
|
|
97
|
+
read,
|
|
98
|
+
readByRefund: async (refundId) => {
|
|
99
|
+
const result = await db.query(sql `select * from ${table} where refund_id = ${refundId}`);
|
|
100
|
+
const row = result.rows[0];
|
|
101
|
+
return row === undefined ? null : decode(row);
|
|
102
|
+
},
|
|
103
|
+
listForOrder: async (orderId) => {
|
|
104
|
+
const result = await db.query(sql `select * from ${table} where order_id = ${orderId} order by seq asc`);
|
|
105
|
+
return result.rows.map(decode);
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
//# sourceMappingURL=credit-note.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"credit-note.js","sourceRoot":"","sources":["../../src/invoice/credit-note.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type DatabaseDialect, type SqlExecutor } from '@cogenta/core';
|
|
2
|
+
/** `2026-000042` / `CN-2026-000003`. Zero-padded so the numbers sort as strings too. */
|
|
3
|
+
export declare function formatSequenceNumber(series: string, seq: number): string;
|
|
4
|
+
/**
|
|
5
|
+
* Takes the next number in a series, atomically — shared by invoices
|
|
6
|
+
* (`invoice/store.ts`) and credit notes (`invoice/credit-note.ts`, fiche 52
|
|
7
|
+
* task 6), both series living in the same `cogenta_commerce_invoice_sequences`
|
|
8
|
+
* table, told apart only by their series string (an invoice's is the year,
|
|
9
|
+
* `2026`; a credit note's is `CN-2026` — see `credit-note.ts`).
|
|
10
|
+
*
|
|
11
|
+
* `update … set next_seq = next_seq + 1 where series = ? and next_seq = ?` —
|
|
12
|
+
* a compare-and-set whose `rowsAffected` decides the race, the same idiom
|
|
13
|
+
* that makes stock safe and a password reset token single use. Two documents
|
|
14
|
+
* issued in the same millisecond get consecutive numbers, never the same one.
|
|
15
|
+
*
|
|
16
|
+
* Emphatically not `count(*) + 1`: that hands out a duplicate under any
|
|
17
|
+
* concurrency at all, and re-issues a number that a deleted row used to
|
|
18
|
+
* hold. A number, once handed out, is spent.
|
|
19
|
+
*/
|
|
20
|
+
export declare function claimSequenceNumber(tx: SqlExecutor, dialect: DatabaseDialect, series: string): Promise<number>;
|
|
21
|
+
//# sourceMappingURL=sequence.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sequence.d.ts","sourceRoot":"","sources":["../../src/invoice/sequence.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,eAAe,EAEpB,KAAK,WAAW,EAEjB,MAAM,eAAe,CAAA;AAItB,wFAAwF;AACxF,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAExE;AAYD;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,mBAAmB,CACvC,EAAE,EAAE,WAAW,EACf,OAAO,EAAE,eAAe,EACxB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,MAAM,CAAC,CAkCjB"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { CogentaError, identifier, sql, } from '@cogenta/core';
|
|
2
|
+
import { toInt } from '../rows.js';
|
|
3
|
+
import { TABLES } from '../tables.js';
|
|
4
|
+
/** `2026-000042` / `CN-2026-000003`. Zero-padded so the numbers sort as strings too. */
|
|
5
|
+
export function formatSequenceNumber(series, seq) {
|
|
6
|
+
return `${series}-${String(seq).padStart(6, '0')}`;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* How many attempts at claiming the next number before giving up.
|
|
10
|
+
*
|
|
11
|
+
* The claim is a compare-and-set, so a loser retries with the value it just
|
|
12
|
+
* read. Under real contention two or three attempts is already unusual; ten is
|
|
13
|
+
* a number nobody reaches without something else being wrong, and failing
|
|
14
|
+
* loudly then is better than spinning.
|
|
15
|
+
*/
|
|
16
|
+
const CLAIM_ATTEMPTS = 10;
|
|
17
|
+
/**
|
|
18
|
+
* Takes the next number in a series, atomically — shared by invoices
|
|
19
|
+
* (`invoice/store.ts`) and credit notes (`invoice/credit-note.ts`, fiche 52
|
|
20
|
+
* task 6), both series living in the same `cogenta_commerce_invoice_sequences`
|
|
21
|
+
* table, told apart only by their series string (an invoice's is the year,
|
|
22
|
+
* `2026`; a credit note's is `CN-2026` — see `credit-note.ts`).
|
|
23
|
+
*
|
|
24
|
+
* `update … set next_seq = next_seq + 1 where series = ? and next_seq = ?` —
|
|
25
|
+
* a compare-and-set whose `rowsAffected` decides the race, the same idiom
|
|
26
|
+
* that makes stock safe and a password reset token single use. Two documents
|
|
27
|
+
* issued in the same millisecond get consecutive numbers, never the same one.
|
|
28
|
+
*
|
|
29
|
+
* Emphatically not `count(*) + 1`: that hands out a duplicate under any
|
|
30
|
+
* concurrency at all, and re-issues a number that a deleted row used to
|
|
31
|
+
* hold. A number, once handed out, is spent.
|
|
32
|
+
*/
|
|
33
|
+
export async function claimSequenceNumber(tx, dialect, series) {
|
|
34
|
+
const sequences = identifier(TABLES.invoiceSequences, dialect);
|
|
35
|
+
for (let attempt = 0; attempt < CLAIM_ATTEMPTS; attempt += 1) {
|
|
36
|
+
const current = await tx.query(sql `select next_seq from ${sequences} where series = ${series}`);
|
|
37
|
+
const row = current.rows[0];
|
|
38
|
+
if (row === undefined) {
|
|
39
|
+
// First document of the series. A duplicate primary key here means
|
|
40
|
+
// somebody else created it in between, so the loop reads it and
|
|
41
|
+
// competes for the increment like everyone else.
|
|
42
|
+
try {
|
|
43
|
+
await tx.query(sql `insert into ${sequences} (series, next_seq) values (${series}, ${2})`);
|
|
44
|
+
return 1;
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const next = toInt(row.next_seq, 'invoice_sequence.next_seq');
|
|
51
|
+
const claimed = await tx.query(sql `
|
|
52
|
+
update ${sequences} set next_seq = ${next + 1}
|
|
53
|
+
where series = ${series} and next_seq = ${next}`);
|
|
54
|
+
if (claimed.rowsAffected > 0)
|
|
55
|
+
return next;
|
|
56
|
+
}
|
|
57
|
+
throw new CogentaError({
|
|
58
|
+
code: 'COMMERCE_INVOICE_SEQUENCE_CONFLICT',
|
|
59
|
+
message: `Could not take the next number in series "${series}".`,
|
|
60
|
+
hint: 'The sequence is under unusually heavy contention. Retry the issue.',
|
|
61
|
+
details: { series, attempts: CLAIM_ATTEMPTS },
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
//# sourceMappingURL=sequence.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sequence.js","sourceRoot":"","sources":["../../src/invoice/sequence.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,YAAY,EAEZ,UAAU,EAEV,GAAG,GACJ,MAAM,eAAe,CAAA;AACtB,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAA;AAClC,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAA;AAErC,wFAAwF;AACxF,MAAM,UAAU,oBAAoB,CAAC,MAAc,EAAE,GAAW;IAC9D,OAAO,GAAG,MAAM,IAAI,MAAM,CAAC,GAAG,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CAAA;AACpD,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,cAAc,GAAG,EAAE,CAAA;AAEzB;;;;;;;;;;;;;;;GAeG;AACH,MAAM,CAAC,KAAK,UAAU,mBAAmB,CACvC,EAAe,EACf,OAAwB,EACxB,MAAc;IAEd,MAAM,SAAS,GAAG,UAAU,CAAC,MAAM,CAAC,gBAAgB,EAAE,OAAO,CAAC,CAAA;IAE9D,KAAK,IAAI,OAAO,GAAG,CAAC,EAAE,OAAO,GAAG,cAAc,EAAE,OAAO,IAAI,CAAC,EAAE,CAAC;QAC7D,MAAM,OAAO,GAAG,MAAM,EAAE,CAAC,KAAK,CAC5B,GAAG,CAAA,wBAAwB,SAAS,mBAAmB,MAAM,EAAE,CAChE,CAAA;QACD,MAAM,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;QAE3B,IAAI,GAAG,KAAK,SAAS,EAAE,CAAC;YACtB,mEAAmE;YACnE,gEAAgE;YAChE,iDAAiD;YACjD,IAAI,CAAC;gBACH,MAAM,EAAE,CAAC,KAAK,CAAC,GAAG,CAAA,eAAe,SAAS,+BAA+B,MAAM,KAAK,CAAC,GAAG,CAAC,CAAA;gBACzF,OAAO,CAAC,CAAA;YACV,CAAC;YAAC,MAAM,CAAC;gBACP,SAAQ;YACV,CAAC;QACH,CAAC;QAED,MAAM,IAAI,GAAG,KAAK,CAAC,GAAG,CAAC,QAAQ,EAAE,2BAA2B,CAAC,CAAA;QAC7D,MAAM,OAAO,GAAG,MAAM,EAAE,CAAC,KAAK,CAAC,GAAG,CAAA;eACvB,SAAS,mBAAmB,IAAI,GAAG,CAAC;uBAC5B,MAAM,mBAAmB,IAAI,EAAE,CAAC,CAAA;QACnD,IAAI,OAAO,CAAC,YAAY,GAAG,CAAC;YAAE,OAAO,IAAI,CAAA;IAC3C,CAAC;IAED,MAAM,IAAI,YAAY,CAAC;QACrB,IAAI,EAAE,oCAAoC;QAC1C,OAAO,EAAE,6CAA6C,MAAM,IAAI;QAChE,IAAI,EAAE,oEAAoE;QAC1E,OAAO,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,cAAc,EAAE;KAC9C,CAAC,CAAA;AACJ,CAAC"}
|
package/dist/invoice/store.d.ts
CHANGED
|
@@ -81,8 +81,30 @@ export interface InvoiceStore {
|
|
|
81
81
|
}): Promise<readonly Invoice[]>;
|
|
82
82
|
/** The PDF for an invoice. Regenerated from the snapshot, byte-identical. */
|
|
83
83
|
pdf(id: string): Promise<Uint8Array>;
|
|
84
|
+
/**
|
|
85
|
+
* A real invoice PDF for an order that has not been (and may never be)
|
|
86
|
+
* issued — fiche 54 task 2. Built from the exact same `documentFor`/
|
|
87
|
+
* `pdfDocumentFor`/`renderInvoicePdf` chain `issue()`+`pdf()` use, so a
|
|
88
|
+
* preview is never a second, drifting implementation of what an invoice
|
|
89
|
+
* looks like. What makes it a preview and not a side effect: no row is
|
|
90
|
+
* written, no order event is recorded, and above all no number is claimed
|
|
91
|
+
* from `claimNumber` — a real invoice number is gapless and never reused,
|
|
92
|
+
* so spending one on a screen a shop owner might reload ten times while
|
|
93
|
+
* tuning the template would corrupt the one thing this file exists to
|
|
94
|
+
* protect. `"PREVIEW"` in the number field is not a placeholder chosen for
|
|
95
|
+
* looks: it is what stops the output from ever being mistaken for a real,
|
|
96
|
+
* legally-numbered document.
|
|
97
|
+
*/
|
|
98
|
+
preview(orderId: string): Promise<Uint8Array>;
|
|
84
99
|
}
|
|
85
|
-
/**
|
|
100
|
+
/**
|
|
101
|
+
* `2026-000042`. Zero-padded so the numbers sort as strings too.
|
|
102
|
+
*
|
|
103
|
+
* A thin alias of `formatSequenceNumber` (`sequence.ts`), kept under its
|
|
104
|
+
* original name for source compatibility — this was this package's public
|
|
105
|
+
* export before the sequence-claiming logic moved out to be shared with
|
|
106
|
+
* credit notes (fiche 52 task 6).
|
|
107
|
+
*/
|
|
86
108
|
export declare function formatInvoiceNumber(series: string, seq: number): string;
|
|
87
109
|
export declare function createInvoiceStore(db: DatabaseHandle, dependencies: InvoiceStoreDependencies, now?: () => number): InvoiceStore;
|
|
88
110
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/invoice/store.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,cAAc,EAKpB,MAAM,eAAe,CAAA;AAEtB,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA;AAInD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAA;
|
|
1
|
+
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/invoice/store.ts"],"names":[],"mappings":"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"}
|
package/dist/invoice/store.js
CHANGED
|
@@ -3,6 +3,7 @@ import { formatMoney } from '../money.js';
|
|
|
3
3
|
import { toInt, toText } from '../rows.js';
|
|
4
4
|
import { TABLES } from '../tables.js';
|
|
5
5
|
import { renderInvoicePdf } from './pdf.js';
|
|
6
|
+
import { claimSequenceNumber, formatSequenceNumber } from './sequence.js';
|
|
6
7
|
function decode(row) {
|
|
7
8
|
return {
|
|
8
9
|
id: toText(row.id, 'invoice.id'),
|
|
@@ -16,66 +17,20 @@ function decode(row) {
|
|
|
16
17
|
document: JSON.parse(toText(row.document, 'invoice.document')),
|
|
17
18
|
};
|
|
18
19
|
}
|
|
19
|
-
/** `2026-000042`. Zero-padded so the numbers sort as strings too. */
|
|
20
|
-
export function formatInvoiceNumber(series, seq) {
|
|
21
|
-
return `${series}-${String(seq).padStart(6, '0')}`;
|
|
22
|
-
}
|
|
23
20
|
/**
|
|
24
|
-
*
|
|
21
|
+
* `2026-000042`. Zero-padded so the numbers sort as strings too.
|
|
25
22
|
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
23
|
+
* A thin alias of `formatSequenceNumber` (`sequence.ts`), kept under its
|
|
24
|
+
* original name for source compatibility — this was this package's public
|
|
25
|
+
* export before the sequence-claiming logic moved out to be shared with
|
|
26
|
+
* credit notes (fiche 52 task 6).
|
|
30
27
|
*/
|
|
31
|
-
|
|
28
|
+
export function formatInvoiceNumber(series, seq) {
|
|
29
|
+
return formatSequenceNumber(series, seq);
|
|
30
|
+
}
|
|
32
31
|
export function createInvoiceStore(db, dependencies, now = Date.now) {
|
|
33
32
|
const d = db.dialect;
|
|
34
33
|
const invoices = identifier(TABLES.invoices, d);
|
|
35
|
-
const sequences = identifier(TABLES.invoiceSequences, d);
|
|
36
|
-
/**
|
|
37
|
-
* Takes the next number in a series, atomically.
|
|
38
|
-
*
|
|
39
|
-
* `update … set next_seq = next_seq + 1 where series = ? and next_seq = ?`
|
|
40
|
-
* — a compare-and-set whose `rowsAffected` decides the race, the same idiom
|
|
41
|
-
* that makes stock safe and a password reset token single use. Two invoices
|
|
42
|
-
* issued in the same millisecond get consecutive numbers, never the same
|
|
43
|
-
* one.
|
|
44
|
-
*
|
|
45
|
-
* Emphatically not `count(*) + 1`: that hands out a duplicate under any
|
|
46
|
-
* concurrency at all, and re-issues a number that a deleted row used to
|
|
47
|
-
* hold. A number, once handed out, is spent.
|
|
48
|
-
*/
|
|
49
|
-
async function claimNumber(tx, series) {
|
|
50
|
-
for (let attempt = 0; attempt < CLAIM_ATTEMPTS; attempt += 1) {
|
|
51
|
-
const current = await tx.query(sql `select next_seq from ${sequences} where series = ${series}`);
|
|
52
|
-
const row = current.rows[0];
|
|
53
|
-
if (row === undefined) {
|
|
54
|
-
// First invoice of the series. A duplicate primary key here means
|
|
55
|
-
// somebody else created it in between, so the loop reads it and
|
|
56
|
-
// competes for the increment like everyone else.
|
|
57
|
-
try {
|
|
58
|
-
await tx.query(sql `insert into ${sequences} (series, next_seq) values (${series}, ${2})`);
|
|
59
|
-
return 1;
|
|
60
|
-
}
|
|
61
|
-
catch {
|
|
62
|
-
continue;
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
const next = toInt(row.next_seq, 'invoice_sequence.next_seq');
|
|
66
|
-
const claimed = await tx.query(sql `
|
|
67
|
-
update ${sequences} set next_seq = ${next + 1}
|
|
68
|
-
where series = ${series} and next_seq = ${next}`);
|
|
69
|
-
if (claimed.rowsAffected > 0)
|
|
70
|
-
return next;
|
|
71
|
-
}
|
|
72
|
-
throw new CogentaError({
|
|
73
|
-
code: 'COMMERCE_INVOICE_SEQUENCE_CONFLICT',
|
|
74
|
-
message: `Could not take the next number in invoice series "${series}".`,
|
|
75
|
-
hint: 'The sequence is under unusually heavy contention. Retry the issue.',
|
|
76
|
-
details: { series, attempts: CLAIM_ATTEMPTS },
|
|
77
|
-
});
|
|
78
|
-
}
|
|
79
34
|
async function read(id, executor = db) {
|
|
80
35
|
const result = await executor.query(sql `select * from ${invoices} where id = ${id}`);
|
|
81
36
|
const row = result.rows[0];
|
|
@@ -132,8 +87,8 @@ export function createInvoiceStore(db, dependencies, now = Date.now) {
|
|
|
132
87
|
// The number is taken inside the same transaction that writes the
|
|
133
88
|
// invoice. If the write fails, the increment rolls back with it and
|
|
134
89
|
// the number is not burned — which is what "gapless" requires.
|
|
135
|
-
const seq = await
|
|
136
|
-
const number =
|
|
90
|
+
const seq = await claimSequenceNumber(tx, d, series);
|
|
91
|
+
const number = formatSequenceNumber(series, seq);
|
|
137
92
|
const document = documentFor(order, number, issuedAt, input.buyerAddress ?? []);
|
|
138
93
|
await tx.query(sql `
|
|
139
94
|
insert into ${invoices} (id, order_id, series, seq, number, issued_at, currency, total_minor, document)
|
|
@@ -179,6 +134,18 @@ export function createInvoiceStore(db, dependencies, now = Date.now) {
|
|
|
179
134
|
}
|
|
180
135
|
return renderInvoicePdf(pdfDocumentFor(invoice.document));
|
|
181
136
|
},
|
|
137
|
+
preview: async (orderId) => {
|
|
138
|
+
const order = await dependencies.orders.read(orderId);
|
|
139
|
+
if (order === null) {
|
|
140
|
+
throw new CogentaError({
|
|
141
|
+
code: 'COMMERCE_ORDER_NOT_FOUND',
|
|
142
|
+
message: 'This order does not exist.',
|
|
143
|
+
hint: 'Check the order reference.',
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
const document = documentFor(order, 'PREVIEW', new Date(now()).toISOString(), []);
|
|
147
|
+
return renderInvoicePdf(pdfDocumentFor(document));
|
|
148
|
+
},
|
|
182
149
|
};
|
|
183
150
|
}
|
|
184
151
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"store.js","sourceRoot":"","sources":["../../src/invoice/store.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,YAAY,EAEZ,UAAU,EACV,KAAK,EAEL,GAAG,GACJ,MAAM,eAAe,CAAA;AACtB,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AAGzC,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,YAAY,CAAA;AAC1C,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAA;AAErC,OAAO,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAA;
|
|
1
|
+
{"version":3,"file":"store.js","sourceRoot":"","sources":["../../src/invoice/store.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { Order } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* The accounting export (fiche 52 task 7). "Décisions à trancher — Format de
|
|
4
|
+
* l'export comptable — à figer et documenter avant l'implémentation" is
|
|
5
|
+
* resolved here: **one row per order**, RFC 4180 CSV, one column per figure
|
|
6
|
+
* an accountant reconciling a bank statement against this shop's orders
|
|
7
|
+
* needs — reference, date, buyer, currency, the four figures that sum to the
|
|
8
|
+
* total, the order's current status, and its invoice number when one was
|
|
9
|
+
* issued. A row-per-order-*line* export (the other reasonable choice) is
|
|
10
|
+
* deliberately not built: it would need a second, incompatible column set
|
|
11
|
+
* (per-line SKU/quantity) and this fiche's acceptance criteria only names
|
|
12
|
+
* "export comptable" in the singular, order-level sense the rest of this
|
|
13
|
+
* fiche (address, tracking, refunds) already works at. Zero dependency
|
|
14
|
+
* (R9) — a CSV writer for flat, known-shape rows is a handful of lines, not
|
|
15
|
+
* a library.
|
|
16
|
+
*/
|
|
17
|
+
export interface OrderExportRow {
|
|
18
|
+
readonly order: Order;
|
|
19
|
+
/** The invoice number for this order, or null when none was issued. */
|
|
20
|
+
readonly invoiceNumber: string | null;
|
|
21
|
+
}
|
|
22
|
+
/** `\r\n` line endings, as RFC 4180 asks for — every spreadsheet reads it either way. */
|
|
23
|
+
export declare function ordersToCsv(rows: readonly OrderExportRow[]): string;
|
|
24
|
+
//# sourceMappingURL=csv.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"csv.d.ts","sourceRoot":"","sources":["../../src/order/csv.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,YAAY,CAAA;AAEvC;;;;;;;;;;;;;;GAcG;AACH,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAA;IACrB,uEAAuE;IACvE,QAAQ,CAAC,aAAa,EAAE,MAAM,GAAG,IAAI,CAAA;CACtC;AAwCD,yFAAyF;AACzF,wBAAgB,WAAW,CAAC,IAAI,EAAE,SAAS,cAAc,EAAE,GAAG,MAAM,CAGnE"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
const COLUMNS = [
|
|
2
|
+
'reference',
|
|
3
|
+
'placed_at',
|
|
4
|
+
'status',
|
|
5
|
+
'email',
|
|
6
|
+
'currency',
|
|
7
|
+
'subtotal_minor',
|
|
8
|
+
'discount_minor',
|
|
9
|
+
'shipping_minor',
|
|
10
|
+
'tax_minor',
|
|
11
|
+
'total_minor',
|
|
12
|
+
'invoice_number',
|
|
13
|
+
];
|
|
14
|
+
/** RFC 4180: a field containing a comma, a quote or a newline is quoted, with `"` doubled. */
|
|
15
|
+
function escapeCsvField(value) {
|
|
16
|
+
if (/[",\n\r]/u.test(value))
|
|
17
|
+
return `"${value.replace(/"/gu, '""')}"`;
|
|
18
|
+
return value;
|
|
19
|
+
}
|
|
20
|
+
function rowToCsvLine(row) {
|
|
21
|
+
const { order } = row;
|
|
22
|
+
const fields = [
|
|
23
|
+
order.reference,
|
|
24
|
+
order.placedAt,
|
|
25
|
+
order.status,
|
|
26
|
+
order.email,
|
|
27
|
+
order.currency,
|
|
28
|
+
String(order.subtotalMinor),
|
|
29
|
+
String(order.discountMinor),
|
|
30
|
+
String(order.shippingMinor),
|
|
31
|
+
String(order.taxMinor),
|
|
32
|
+
String(order.totalMinor),
|
|
33
|
+
row.invoiceNumber ?? '',
|
|
34
|
+
];
|
|
35
|
+
return fields.map(escapeCsvField).join(',');
|
|
36
|
+
}
|
|
37
|
+
/** `\r\n` line endings, as RFC 4180 asks for — every spreadsheet reads it either way. */
|
|
38
|
+
export function ordersToCsv(rows) {
|
|
39
|
+
const lines = [COLUMNS.join(','), ...rows.map(rowToCsvLine)];
|
|
40
|
+
return `${lines.join('\r\n')}\r\n`;
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=csv.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"csv.js","sourceRoot":"","sources":["../../src/order/csv.ts"],"names":[],"mappings":"AAuBA,MAAM,OAAO,GAAG;IACd,WAAW;IACX,WAAW;IACX,QAAQ;IACR,OAAO;IACP,UAAU;IACV,gBAAgB;IAChB,gBAAgB;IAChB,gBAAgB;IAChB,WAAW;IACX,aAAa;IACb,gBAAgB;CACR,CAAA;AAEV,8FAA8F;AAC9F,SAAS,cAAc,CAAC,KAAa;IACnC,IAAI,WAAW,CAAC,IAAI,CAAC,KAAK,CAAC;QAAE,OAAO,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,EAAE,IAAI,CAAC,GAAG,CAAA;IACrE,OAAO,KAAK,CAAA;AACd,CAAC;AAED,SAAS,YAAY,CAAC,GAAmB;IACvC,MAAM,EAAE,KAAK,EAAE,GAAG,GAAG,CAAA;IACrB,MAAM,MAAM,GAAG;QACb,KAAK,CAAC,SAAS;QACf,KAAK,CAAC,QAAQ;QACd,KAAK,CAAC,MAAM;QACZ,KAAK,CAAC,KAAK;QACX,KAAK,CAAC,QAAQ;QACd,MAAM,CAAC,KAAK,CAAC,aAAa,CAAC;QAC3B,MAAM,CAAC,KAAK,CAAC,aAAa,CAAC;QAC3B,MAAM,CAAC,KAAK,CAAC,aAAa,CAAC;QAC3B,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC;QACtB,MAAM,CAAC,KAAK,CAAC,UAAU,CAAC;QACxB,GAAG,CAAC,aAAa,IAAI,EAAE;KACxB,CAAA;IACD,OAAO,MAAM,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;AAC7C,CAAC;AAED,yFAAyF;AACzF,MAAM,UAAU,WAAW,CAAC,IAA+B;IACzD,MAAM,KAAK,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,GAAG,IAAI,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC,CAAA;IAC5D,OAAO,GAAG,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,CAAA;AACpC,CAAC"}
|