@cogenta/commerce 0.2.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 (98) hide show
  1. package/LICENSE +373 -0
  2. package/dist/admin/permissions.d.ts +57 -0
  3. package/dist/admin/permissions.d.ts.map +1 -0
  4. package/dist/admin/permissions.js +87 -0
  5. package/dist/admin/permissions.js.map +1 -0
  6. package/dist/admin/router.d.ts +53 -0
  7. package/dist/admin/router.d.ts.map +1 -0
  8. package/dist/admin/router.js +476 -0
  9. package/dist/admin/router.js.map +1 -0
  10. package/dist/cart/store.d.ts +91 -0
  11. package/dist/cart/store.d.ts.map +1 -0
  12. package/dist/cart/store.js +342 -0
  13. package/dist/cart/store.js.map +1 -0
  14. package/dist/cart/totals.d.ts +73 -0
  15. package/dist/cart/totals.d.ts.map +1 -0
  16. package/dist/cart/totals.js +65 -0
  17. package/dist/cart/totals.js.map +1 -0
  18. package/dist/catalog/store.d.ts +53 -0
  19. package/dist/catalog/store.d.ts.map +1 -0
  20. package/dist/catalog/store.js +358 -0
  21. package/dist/catalog/store.js.map +1 -0
  22. package/dist/catalog/types.d.ts +95 -0
  23. package/dist/catalog/types.d.ts.map +1 -0
  24. package/dist/catalog/types.js +5 -0
  25. package/dist/catalog/types.js.map +1 -0
  26. package/dist/coupon/store.d.ts +103 -0
  27. package/dist/coupon/store.d.ts.map +1 -0
  28. package/dist/coupon/store.js +181 -0
  29. package/dist/coupon/store.js.map +1 -0
  30. package/dist/customer/store.d.ts +41 -0
  31. package/dist/customer/store.d.ts.map +1 -0
  32. package/dist/customer/store.js +103 -0
  33. package/dist/customer/store.js.map +1 -0
  34. package/dist/index.d.ts +42 -0
  35. package/dist/index.d.ts.map +1 -0
  36. package/dist/index.js +23 -0
  37. package/dist/index.js.map +1 -0
  38. package/dist/invoice/pdf.d.ts +67 -0
  39. package/dist/invoice/pdf.d.ts.map +1 -0
  40. package/dist/invoice/pdf.js +449 -0
  41. package/dist/invoice/pdf.js.map +1 -0
  42. package/dist/invoice/store.d.ts +96 -0
  43. package/dist/invoice/store.d.ts.map +1 -0
  44. package/dist/invoice/store.js +221 -0
  45. package/dist/invoice/store.js.map +1 -0
  46. package/dist/money.d.ts +55 -0
  47. package/dist/money.d.ts.map +1 -0
  48. package/dist/money.js +146 -0
  49. package/dist/money.js.map +1 -0
  50. package/dist/order/store.d.ts +75 -0
  51. package/dist/order/store.d.ts.map +1 -0
  52. package/dist/order/store.js +301 -0
  53. package/dist/order/store.js.map +1 -0
  54. package/dist/order/types.d.ts +67 -0
  55. package/dist/order/types.d.ts.map +1 -0
  56. package/dist/order/types.js +71 -0
  57. package/dist/order/types.js.map +1 -0
  58. package/dist/payment/manual.d.ts +27 -0
  59. package/dist/payment/manual.d.ts.map +1 -0
  60. package/dist/payment/manual.js +105 -0
  61. package/dist/payment/manual.js.map +1 -0
  62. package/dist/payment/registry.d.ts +23 -0
  63. package/dist/payment/registry.d.ts.map +1 -0
  64. package/dist/payment/registry.js +30 -0
  65. package/dist/payment/registry.js.map +1 -0
  66. package/dist/payment/store.d.ts +38 -0
  67. package/dist/payment/store.d.ts.map +1 -0
  68. package/dist/payment/store.js +253 -0
  69. package/dist/payment/store.js.map +1 -0
  70. package/dist/payment/stripe.d.ts +10 -0
  71. package/dist/payment/stripe.d.ts.map +1 -0
  72. package/dist/payment/stripe.js +419 -0
  73. package/dist/payment/stripe.js.map +1 -0
  74. package/dist/payment/types.d.ts +124 -0
  75. package/dist/payment/types.d.ts.map +1 -0
  76. package/dist/payment/types.js +18 -0
  77. package/dist/payment/types.js.map +1 -0
  78. package/dist/rows.d.ts +26 -0
  79. package/dist/rows.d.ts.map +1 -0
  80. package/dist/rows.js +87 -0
  81. package/dist/rows.js.map +1 -0
  82. package/dist/shipping/store.d.ts +88 -0
  83. package/dist/shipping/store.d.ts.map +1 -0
  84. package/dist/shipping/store.js +165 -0
  85. package/dist/shipping/store.js.map +1 -0
  86. package/dist/subscription/store.d.ts +106 -0
  87. package/dist/subscription/store.d.ts.map +1 -0
  88. package/dist/subscription/store.js +310 -0
  89. package/dist/subscription/store.js.map +1 -0
  90. package/dist/tables.d.ts +41 -0
  91. package/dist/tables.d.ts.map +1 -0
  92. package/dist/tables.js +383 -0
  93. package/dist/tables.js.map +1 -0
  94. package/dist/tax/store.d.ts +68 -0
  95. package/dist/tax/store.d.ts.map +1 -0
  96. package/dist/tax/store.js +126 -0
  97. package/dist/tax/store.js.map +1 -0
  98. package/package.json +42 -0
@@ -0,0 +1,181 @@
1
+ import { CogentaError, identifier, newId, sql, } from '@cogenta/core';
2
+ import { applyBasisPoints, assertCurrency, assertMinor } from '../money.js';
3
+ import { fromBool, toBool, toInt, toNullableInt, toNullableText, toText } from '../rows.js';
4
+ import { TABLES } from '../tables.js';
5
+ /**
6
+ * Three kinds, and no more.
7
+ *
8
+ * "Buy two get one free", tiered discounts and per-collection promotions all
9
+ * belong to a rules engine, and a rules engine is exactly what a shop does not
10
+ * need on day one. These three cover the overwhelming majority of real
11
+ * coupons, and each has an unambiguous arithmetic — which matters more here
12
+ * than range, because a discount that is a percent of something ambiguous is a
13
+ * dispute with a customer.
14
+ */
15
+ export const COUPON_KINDS = ['percentage', 'fixed', 'free_shipping'];
16
+ function decode(row) {
17
+ return {
18
+ code: toText(row.code, 'coupon.code'),
19
+ kind: toText(row.kind, 'coupon.kind'),
20
+ value: toInt(row.value, 'coupon.value'),
21
+ currency: toNullableText(row.currency),
22
+ minSubtotalMinor: toInt(row.min_subtotal_minor, 'coupon.min_subtotal_minor'),
23
+ startsAt: toNullableText(row.starts_at),
24
+ endsAt: toNullableText(row.ends_at),
25
+ maxRedemptions: toNullableInt(row.max_redemptions, 'coupon.max_redemptions'),
26
+ redemptions: toInt(row.redemptions, 'coupon.redemptions'),
27
+ active: toBool(row.active),
28
+ createdAt: toText(row.created_at, 'coupon.created_at'),
29
+ };
30
+ }
31
+ /** Upper case, trimmed. A coupon typed in lower case is the same coupon. */
32
+ export function normaliseCode(code) {
33
+ return code.trim().toUpperCase();
34
+ }
35
+ export function createCouponStore(db, now = Date.now) {
36
+ const d = db.dialect;
37
+ const table = identifier(TABLES.coupons, d);
38
+ const redemptions = identifier(TABLES.couponRedemptions, d);
39
+ async function read(code) {
40
+ const result = await db.query(sql `select * from ${table} where code = ${normaliseCode(code)}`);
41
+ const row = result.rows[0];
42
+ return row === undefined ? null : decode(row);
43
+ }
44
+ return {
45
+ create: async (input) => {
46
+ const code = normaliseCode(input.code);
47
+ if (code === '' || code.length > 64) {
48
+ throw new CogentaError({
49
+ code: 'COMMERCE_COUPON_INVALID',
50
+ message: 'A coupon code must be between 1 and 64 characters.',
51
+ hint: 'Short codes get typed correctly. "SPRING25" beats a UUID.',
52
+ });
53
+ }
54
+ if (input.kind === 'percentage') {
55
+ const value = input.value ?? 0;
56
+ if (!Number.isInteger(value) || value <= 0 || value > 10_000) {
57
+ throw new CogentaError({
58
+ code: 'COMMERCE_COUPON_INVALID',
59
+ message: `A percentage coupon must be between 1 and 10000 basis points, got ${String(input.value)}.`,
60
+ hint: '25 % off is 2500 basis points. 10000 is everything free.',
61
+ });
62
+ }
63
+ }
64
+ if (input.kind === 'fixed') {
65
+ assertMinor(input.value ?? 0, 'A fixed coupon amount');
66
+ if ((input.currency ?? null) === null) {
67
+ throw new CogentaError({
68
+ code: 'COMMERCE_COUPON_INVALID',
69
+ message: 'A fixed-amount coupon needs a currency.',
70
+ hint: '"£10 off" is not the same offer as "€10 off". A percentage coupon needs no currency.',
71
+ });
72
+ }
73
+ }
74
+ if (await read(code)) {
75
+ throw new CogentaError({
76
+ code: 'COMMERCE_COUPON_INVALID',
77
+ message: `The coupon code "${code}" already exists.`,
78
+ hint: 'Deactivate the existing one, or choose another code.',
79
+ });
80
+ }
81
+ await db.query(sql `
82
+ insert into ${table} (code, kind, value, currency, min_subtotal_minor, starts_at, ends_at,
83
+ max_redemptions, redemptions, active, created_at)
84
+ values (${code}, ${input.kind}, ${input.value ?? 0},
85
+ ${input.currency === undefined || input.currency === null ? null : assertCurrency(input.currency)},
86
+ ${assertMinor(input.minSubtotalMinor ?? 0, 'A coupon minimum')},
87
+ ${input.startsAt ?? null}, ${input.endsAt ?? null},
88
+ ${input.maxRedemptions ?? null}, ${0},
89
+ ${fromBool(input.active ?? true, d)}, ${new Date(now()).toISOString()})`);
90
+ const created = await read(code);
91
+ if (created === null) {
92
+ throw new CogentaError({
93
+ code: 'COMMERCE_COUPON_INVALID',
94
+ message: 'The coupon was not stored.',
95
+ hint: 'Check that the commerce tables exist (ensureCommerceTables).',
96
+ });
97
+ }
98
+ return created;
99
+ },
100
+ read,
101
+ deactivate: async (code) => {
102
+ await db.query(sql `update ${table} set active = ${fromBool(false, d)} where code = ${normaliseCode(code)}`);
103
+ },
104
+ list: async () => {
105
+ const result = await db.query(sql `select * from ${table} order by created_at desc`);
106
+ return result.rows.map(decode);
107
+ },
108
+ check: async (code, subtotalMinor, currency) => {
109
+ const coupon = await read(code);
110
+ if (coupon === null)
111
+ return { kind: 'unknown' };
112
+ if (!coupon.active)
113
+ return { kind: 'inactive' };
114
+ const at = now();
115
+ if (coupon.startsAt !== null && new Date(coupon.startsAt).getTime() > at) {
116
+ return { kind: 'not_yet', startsAt: coupon.startsAt };
117
+ }
118
+ if (coupon.endsAt !== null && new Date(coupon.endsAt).getTime() <= at) {
119
+ return { kind: 'expired', endsAt: coupon.endsAt };
120
+ }
121
+ if (coupon.maxRedemptions !== null && coupon.redemptions >= coupon.maxRedemptions) {
122
+ return { kind: 'exhausted' };
123
+ }
124
+ if (subtotalMinor < coupon.minSubtotalMinor) {
125
+ return { kind: 'below_minimum', minSubtotalMinor: coupon.minSubtotalMinor };
126
+ }
127
+ if (coupon.currency !== null && coupon.currency !== currency) {
128
+ return { kind: 'wrong_currency', currency: coupon.currency };
129
+ }
130
+ return { kind: 'ok', coupon };
131
+ },
132
+ redeem: async (code, orderId, customerId, tx) => {
133
+ const normalised = normaliseCode(code);
134
+ const run = async (executor) => {
135
+ const claimed = await executor.query(sql `
136
+ update ${table} set redemptions = redemptions + 1
137
+ where code = ${normalised}
138
+ and active = ${fromBool(true, d)}
139
+ and (max_redemptions is null or redemptions < max_redemptions)`);
140
+ if (claimed.rowsAffected === 0)
141
+ return false;
142
+ await executor.query(sql `
143
+ insert into ${redemptions} (id, code, order_id, customer_id, at)
144
+ values (${newId(now)}, ${normalised}, ${orderId}, ${customerId}, ${new Date(now()).toISOString()})`);
145
+ return true;
146
+ };
147
+ return tx === undefined ? db.transaction(run, { immediate: true }) : run(tx);
148
+ },
149
+ release: async (orderId, tx) => {
150
+ const run = async (executor) => {
151
+ const found = await executor.query(sql `select code from ${redemptions} where order_id = ${orderId}`);
152
+ const row = found.rows[0];
153
+ if (row === undefined)
154
+ return;
155
+ const code = toText(row.code, 'coupon_redemption.code');
156
+ await executor.query(sql `delete from ${redemptions} where order_id = ${orderId}`);
157
+ // `redemptions > 0` guards against ever writing a negative count, which
158
+ // would silently hand out extra redemptions of a one-shot code.
159
+ await executor.query(sql `update ${table} set redemptions = redemptions - 1 where code = ${code} and redemptions > 0`);
160
+ };
161
+ if (tx === undefined)
162
+ await db.transaction(run, { immediate: true });
163
+ else
164
+ await run(tx);
165
+ },
166
+ };
167
+ }
168
+ /**
169
+ * What a coupon takes off a subtotal, and whether it makes shipping free.
170
+ *
171
+ * Never more than the subtotal: a £20 coupon on a £15 basket takes £15, not
172
+ * £20, because the alternative is an order with a negative total — and a
173
+ * negative total in a payment request is a refund a shop did not intend.
174
+ */
175
+ export function discountFor(coupon, subtotalMinor) {
176
+ if (coupon.kind === 'free_shipping')
177
+ return { discountMinor: 0, freeShipping: true };
178
+ const raw = coupon.kind === 'percentage' ? applyBasisPoints(subtotalMinor, coupon.value) : coupon.value;
179
+ return { discountMinor: Math.min(raw, subtotalMinor), freeShipping: false };
180
+ }
181
+ //# sourceMappingURL=store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store.js","sourceRoot":"","sources":["../../src/coupon/store.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,41 @@
1
+ import { type DatabaseHandle } from '@cogenta/core';
2
+ /**
3
+ * A customer is not a user.
4
+ *
5
+ * `@cogenta/auth` models people who sign in to the admin. Most people a shop
6
+ * sells to never do, and forcing them into that table would mean every buyer
7
+ * has a role, a session and a credential record they will never use — plus a
8
+ * password reset flow aimed at an account nobody has. `userId` links the two
9
+ * when they happen to be the same person, and is null the rest of the time.
10
+ */
11
+ export interface Customer {
12
+ readonly id: string;
13
+ readonly email: string;
14
+ readonly name: string | null;
15
+ readonly userId: string | null;
16
+ readonly createdAt: string;
17
+ readonly updatedAt: string;
18
+ }
19
+ export interface CustomerStore {
20
+ /** Finds by email or creates. Checkout calls this; it must not fail on a repeat buyer. */
21
+ ensure(email: string, name?: string | null): Promise<Customer>;
22
+ read(id: string): Promise<Customer | null>;
23
+ readByEmail(email: string): Promise<Customer | null>;
24
+ link(id: string, userId: string | null): Promise<Customer>;
25
+ list(options?: {
26
+ readonly search?: string;
27
+ readonly limit?: number;
28
+ }): Promise<readonly Customer[]>;
29
+ }
30
+ /**
31
+ * Lower-cased and trimmed, and that is all.
32
+ *
33
+ * No dot-stripping, no plus-tag removal: those are Gmail's rules, not email's,
34
+ * and applying them would merge two people who are genuinely different on most
35
+ * other providers. The only normalisation that is safe everywhere is case,
36
+ * because the domain half is case-insensitive by RFC and no real provider
37
+ * treats the local half as case-sensitive.
38
+ */
39
+ export declare function normaliseEmail(email: string): string;
40
+ export declare function createCustomerStore(db: DatabaseHandle, now?: () => number): CustomerStore;
41
+ //# sourceMappingURL=store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/customer/store.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,KAAK,cAAc,EAA0B,MAAM,eAAe,CAAA;AAIzF;;;;;;;;GAQG;AACH,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;IACnB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;IAC5B,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;IAC9B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;CAC3B;AAED,MAAM,WAAW,aAAa;IAC5B,0FAA0F;IAC1F,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAA;IAC9D,IAAI,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,CAAA;IAC1C,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,CAAA;IACpD,IAAI,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAA;IAC1D,IAAI,CAAC,OAAO,CAAC,EAAE;QACb,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;QACxB,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KACxB,GAAG,OAAO,CAAC,SAAS,QAAQ,EAAE,CAAC,CAAA;CACjC;AAsBD;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEpD;AAED,wBAAgB,mBAAmB,CACjC,EAAE,EAAE,cAAc,EAClB,GAAG,GAAE,MAAM,MAAiB,GAC3B,aAAa,CA4Ff"}
@@ -0,0 +1,103 @@
1
+ import { CogentaError, identifier, newId, sql } from '@cogenta/core';
2
+ import { toNullableText, toText } from '../rows.js';
3
+ import { TABLES } from '../tables.js';
4
+ function decode(row) {
5
+ return {
6
+ id: toText(row.id, 'customer.id'),
7
+ email: toText(row.email, 'customer.email'),
8
+ name: toNullableText(row.name),
9
+ userId: toNullableText(row.user_id),
10
+ createdAt: toText(row.created_at, 'customer.created_at'),
11
+ updatedAt: toText(row.updated_at, 'customer.updated_at'),
12
+ };
13
+ }
14
+ /**
15
+ * Lower-cased and trimmed, and that is all.
16
+ *
17
+ * No dot-stripping, no plus-tag removal: those are Gmail's rules, not email's,
18
+ * and applying them would merge two people who are genuinely different on most
19
+ * other providers. The only normalisation that is safe everywhere is case,
20
+ * because the domain half is case-insensitive by RFC and no real provider
21
+ * treats the local half as case-sensitive.
22
+ */
23
+ export function normaliseEmail(email) {
24
+ return email.trim().toLowerCase();
25
+ }
26
+ export function createCustomerStore(db, now = Date.now) {
27
+ const d = db.dialect;
28
+ const table = identifier(TABLES.customers, d);
29
+ const stamp = () => new Date(now()).toISOString();
30
+ async function readByEmail(email) {
31
+ const result = await db.query(sql `select * from ${table} where email = ${normaliseEmail(email)}`);
32
+ const row = result.rows[0];
33
+ return row === undefined ? null : decode(row);
34
+ }
35
+ async function read(id) {
36
+ const result = await db.query(sql `select * from ${table} where id = ${id}`);
37
+ const row = result.rows[0];
38
+ return row === undefined ? null : decode(row);
39
+ }
40
+ return {
41
+ ensure: async (email, name) => {
42
+ const normalised = normaliseEmail(email);
43
+ if (!normalised.includes('@') || normalised.length > 254) {
44
+ throw new CogentaError({
45
+ code: 'COMMERCE_ORDER_NOT_FOUND',
46
+ message: 'That is not a usable email address.',
47
+ hint: 'An order confirmation has to reach somebody.',
48
+ });
49
+ }
50
+ const existing = await readByEmail(normalised);
51
+ if (existing !== null) {
52
+ // A repeat buyer who now gives a name gets it recorded; one who gives
53
+ // none keeps the name already known, rather than having it wiped.
54
+ if (name != null && name !== existing.name) {
55
+ await db.query(sql `update ${table} set name = ${name}, updated_at = ${stamp()} where id = ${existing.id}`);
56
+ const updated = await read(existing.id);
57
+ if (updated !== null)
58
+ return updated;
59
+ }
60
+ return existing;
61
+ }
62
+ const id = newId(now);
63
+ const at = stamp();
64
+ await db.query(sql `
65
+ insert into ${table} (id, email, name, user_id, created_at, updated_at)
66
+ values (${id}, ${normalised}, ${name ?? null}, ${null}, ${at}, ${at})`);
67
+ const created = await read(id);
68
+ if (created === null) {
69
+ throw new CogentaError({
70
+ code: 'INTERNAL',
71
+ message: 'The customer was not stored.',
72
+ hint: 'Check that the commerce tables exist (ensureCommerceTables).',
73
+ });
74
+ }
75
+ return created;
76
+ },
77
+ read,
78
+ readByEmail,
79
+ link: async (id, userId) => {
80
+ await db.query(sql `update ${table} set user_id = ${userId}, updated_at = ${stamp()} where id = ${id}`);
81
+ const linked = await read(id);
82
+ if (linked === null) {
83
+ throw new CogentaError({
84
+ code: 'INTERNAL',
85
+ message: 'This customer does not exist.',
86
+ hint: 'It may have been removed.',
87
+ });
88
+ }
89
+ return linked;
90
+ },
91
+ list: async (options) => {
92
+ const search = options?.search?.trim().toLowerCase();
93
+ const result = search === undefined || search === ''
94
+ ? await db.query(sql `select * from ${table} order by created_at desc`)
95
+ : await db.query(sql `
96
+ select * from ${table}
97
+ where lower(email) like ${`%${search}%`} or lower(name) like ${`%${search}%`}
98
+ order by created_at desc`);
99
+ return result.rows.slice(0, options?.limit ?? 100).map(decode);
100
+ },
101
+ };
102
+ }
103
+ //# sourceMappingURL=store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store.js","sourceRoot":"","sources":["../../src/customer/store.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,42 @@
1
+ export type { CommerceActor, CommercePermission, CommercePermissionLayer, CommercePermissionOptions, } from './admin/permissions.js';
2
+ export { COMMERCE_ANONYMOUS, COMMERCE_PERMISSIONS, createCommercePermissions, DEFAULT_COMMERCE_ROLES, } from './admin/permissions.js';
3
+ export type { CommerceAdminRouter, CommerceAdminRouterOptions, CommerceRequest, CommerceResponse, } from './admin/router.js';
4
+ export { createCommerceAdminRouter } from './admin/router.js';
5
+ export type { Cart, CartLine, CartStatus, CartStore, CartStoreDependencies, OpenCartInput, PricedCart, } from './cart/store.js';
6
+ export { CART_STATUSES, CART_TTL_MS, couponRefusal, createCartStore } from './cart/store.js';
7
+ export type { Totals, TotalsInput, TotalsLine, TotalsLineInput } from './cart/totals.js';
8
+ export { computeTotals } from './cart/totals.js';
9
+ export type { CatalogStore, ProductListOptions, StockOutcome } from './catalog/store.js';
10
+ export { createCatalogStore } from './catalog/store.js';
11
+ export type { ContentRef, CreateProductInput, CreateVariantInput, Product, ProductStatus, StockRequest, StockShortfall, UpdateProductInput, UpdateVariantInput, Variant, } from './catalog/types.js';
12
+ export { PRODUCT_STATUSES } from './catalog/types.js';
13
+ export type { Coupon, CouponCheck, CouponKind, CouponStore, CreateCouponInput, } from './coupon/store.js';
14
+ export { COUPON_KINDS, createCouponStore, discountFor, normaliseCode, } from './coupon/store.js';
15
+ export type { Customer, CustomerStore } from './customer/store.js';
16
+ export { createCustomerStore, normaliseEmail } from './customer/store.js';
17
+ export type { PdfInvoiceDocument, PdfLine } from './invoice/pdf.js';
18
+ export { renderInvoicePdf } from './invoice/pdf.js';
19
+ export type { Invoice, InvoiceDocument, InvoiceStore, InvoiceStoreDependencies, IssueInvoiceInput, SellerDetails, } from './invoice/store.js';
20
+ export { createInvoiceStore, formatInvoiceNumber, pdfDocumentFor } from './invoice/store.js';
21
+ export type { Money } from './money.js';
22
+ export { applyBasisPoints, assertCurrency, assertMinor, assertSameCurrency, distribute, formatMoney, minorUnitExponent, normaliseCurrency, } from './money.js';
23
+ export type { OrderListOptions, OrderStore, OrderStoreDependencies, PlaceOrderInput, PlaceOrderOutcome, } from './order/store.js';
24
+ export { createOrderStore, referenceFrom } from './order/store.js';
25
+ export type { Order, OrderEvent, OrderEventKind, OrderLine, OrderStatus } from './order/types.js';
26
+ export { assertTransition, canTransition, holdsStock, ORDER_EVENT_KINDS, ORDER_STATUSES, } from './order/types.js';
27
+ export { createManualPaymentGateway, manualPaymentDriver, transferReference, } from './payment/manual.js';
28
+ export type { PaymentRegistryOptions } from './payment/registry.js';
29
+ export { createPaymentRegistry } from './payment/registry.js';
30
+ export type { PaymentStore, PaymentStoreDependencies } from './payment/store.js';
31
+ export { createPaymentStore } from './payment/store.js';
32
+ export { stripePaymentDriver } from './payment/stripe.js';
33
+ export type { DriverRefundRequest, DriverRefundResult, PaymentConfig, PaymentDriver, PaymentEvent, PaymentGateway, PaymentRecord, PaymentStatus, RefundRecord, StartedPayment, StartPaymentRequest, } from './payment/types.js';
34
+ export { PAYMENT_STATUSES } from './payment/types.js';
35
+ export type { CarrierRateProvider, CreateShippingMethodInput, ShipmentBasis, ShippingKind, ShippingMethod, ShippingQuote, ShippingStore, ShippingStoreOptions, } from './shipping/store.js';
36
+ export { createShippingStore, SHIPPING_KINDS, storedRate } from './shipping/store.js';
37
+ export type { BillingRunResult, CreateSubscriptionInput, IntervalUnit, Subscription, SubscriptionCycle, SubscriptionStatus, SubscriptionStore, SubscriptionStoreDependencies, } from './subscription/store.js';
38
+ export { advancePeriod, createSubscriptionStore, INTERVAL_UNITS, SUBSCRIPTION_STATUSES, } from './subscription/store.js';
39
+ export { ensureCommerceTables, TABLES } from './tables.js';
40
+ export type { CreateTaxRuleInput, TaxOutcome, TaxRule, TaxStore, TaxZone, } from './tax/store.js';
41
+ export { createTaxStore, taxFor } from './tax/store.js';
42
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/index.js ADDED
@@ -0,0 +1,23 @@
1
+ export { COMMERCE_ANONYMOUS, COMMERCE_PERMISSIONS, createCommercePermissions, DEFAULT_COMMERCE_ROLES, } from './admin/permissions.js';
2
+ export { createCommerceAdminRouter } from './admin/router.js';
3
+ export { CART_STATUSES, CART_TTL_MS, couponRefusal, createCartStore } from './cart/store.js';
4
+ export { computeTotals } from './cart/totals.js';
5
+ export { createCatalogStore } from './catalog/store.js';
6
+ export { PRODUCT_STATUSES } from './catalog/types.js';
7
+ export { COUPON_KINDS, createCouponStore, discountFor, normaliseCode, } from './coupon/store.js';
8
+ export { createCustomerStore, normaliseEmail } from './customer/store.js';
9
+ export { renderInvoicePdf } from './invoice/pdf.js';
10
+ export { createInvoiceStore, formatInvoiceNumber, pdfDocumentFor } from './invoice/store.js';
11
+ export { applyBasisPoints, assertCurrency, assertMinor, assertSameCurrency, distribute, formatMoney, minorUnitExponent, normaliseCurrency, } from './money.js';
12
+ export { createOrderStore, referenceFrom } from './order/store.js';
13
+ export { assertTransition, canTransition, holdsStock, ORDER_EVENT_KINDS, ORDER_STATUSES, } from './order/types.js';
14
+ export { createManualPaymentGateway, manualPaymentDriver, transferReference, } from './payment/manual.js';
15
+ export { createPaymentRegistry } from './payment/registry.js';
16
+ export { createPaymentStore } from './payment/store.js';
17
+ export { stripePaymentDriver } from './payment/stripe.js';
18
+ export { PAYMENT_STATUSES } from './payment/types.js';
19
+ export { createShippingStore, SHIPPING_KINDS, storedRate } from './shipping/store.js';
20
+ export { advancePeriod, createSubscriptionStore, INTERVAL_UNITS, SUBSCRIPTION_STATUSES, } from './subscription/store.js';
21
+ export { ensureCommerceTables, TABLES } from './tables.js';
22
+ export { createTaxStore, taxFor } from './tax/store.js';
23
+ //# sourceMappingURL=index.js.map
@@ -0,0 +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,EAAE,aAAa,EAAE,WAAW,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAA;AAE5F,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAA;AAEhD,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAA;AAavD,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAA;AAQrD,OAAO,EACL,YAAY,EACZ,iBAAiB,EACjB,WAAW,EACX,aAAa,GACd,MAAM,mBAAmB,CAAA;AAE1B,OAAO,EAAE,mBAAmB,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAA;AAEzE,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAA;AASnD,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;AAQnB,OAAO,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAA;AAElE,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;AAWrF,OAAO,EACL,aAAa,EACb,uBAAuB,EACvB,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,67 @@
1
+ /**
2
+ * An invoice rendered as a PDF, written byte by byte, with no dependency.
3
+ *
4
+ * Rule R9 asks for zero dependencies before a small one and rule R10 forbids
5
+ * native code, which rules out `pdfkit` (a large tree) and `puppeteer` (a
6
+ * browser download plus a native binary). A PDF that only has to place text on
7
+ * A4 in a base-14 font is a few hundred lines of byte assembly, so that is what
8
+ * this is.
9
+ *
10
+ * Three decisions shape the file it produces:
11
+ *
12
+ * - **Base-14 fonts only.** `/Helvetica` and `/Helvetica-Bold` are guaranteed
13
+ * present in every conforming reader, so nothing is embedded and an invoice
14
+ * stays a few kilobytes. The price is `WinAnsiEncoding`: one byte per glyph,
15
+ * and a character outside that repertoire becomes `?` rather than a broken
16
+ * file.
17
+ * - **No compression.** A `/FlateDecode` stream would need zlib and would make
18
+ * the output unreadable to anything but a PDF parser. Uncompressed streams
19
+ * keep an invoice greppable and the whole file auditable by eye.
20
+ * - **Deterministic to the byte.** Nothing here reads the clock or a random
21
+ * source. The same document renders to the same bytes forever, so a hash
22
+ * stored next to an invoice still means something a year later. `/CreationDate`
23
+ * is derived from the document's own `issuedAt` or omitted entirely.
24
+ */
25
+ /** One row of the line-item table. One string per column. */
26
+ export interface PdfLine {
27
+ readonly cells: readonly string[];
28
+ }
29
+ /**
30
+ * Everything the renderer needs, already formatted for a human.
31
+ *
32
+ * There is no `Money`, no locale and no date here on purpose: formatting money
33
+ * and dates is the caller's job (it knows the locale and the currency), and
34
+ * keeping this structure made of plain strings is what makes the output
35
+ * reproducible from stored data alone.
36
+ */
37
+ export interface PdfInvoiceDocument {
38
+ /** Heading, e.g. `Invoice`. Whatever language the shop sells in. */
39
+ readonly title: string;
40
+ /** The invoice number, printed verbatim. */
41
+ readonly number: string;
42
+ /** Already formatted for a human, e.g. `2026-03-12` or `12 March 2026`. */
43
+ readonly issuedAt: string;
44
+ /** Address block, one string per line. The first line is printed bold. */
45
+ readonly seller: readonly string[];
46
+ /** Address block, one string per line. The first line is printed bold. */
47
+ readonly buyer: readonly string[];
48
+ /** Table header. Repeated at the top of every continuation page. */
49
+ readonly columns: readonly string[];
50
+ readonly lines: readonly PdfLine[];
51
+ /** Label / amount pairs. The last pair is printed bold. */
52
+ readonly totals: readonly (readonly [string, string])[];
53
+ /** Legal mentions, payment terms — printed at the foot of every page. */
54
+ readonly footer?: string;
55
+ }
56
+ /**
57
+ * The one entry point: a document in, a complete PDF file out.
58
+ *
59
+ * Total by design — it never throws. Every input that could be called invalid
60
+ * has a defined, lossless rendering instead: a row shorter than the header is
61
+ * padded, a row longer than the header widens the table, an unrepresentable
62
+ * character becomes `?`, and text too wide for its column wraps rather than
63
+ * being cut. An invoice is a legal document; losing a line to a validation
64
+ * decision taken inside a renderer would be worse than printing it oddly.
65
+ */
66
+ export declare function renderInvoicePdf(document: PdfInvoiceDocument): Uint8Array;
67
+ //# sourceMappingURL=pdf.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pdf.d.ts","sourceRoot":"","sources":["../../src/invoice/pdf.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,6DAA6D;AAC7D,MAAM,WAAW,OAAO;IACtB,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAA;CAClC;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,kBAAkB;IACjC,oEAAoE;IACpE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,4CAA4C;IAC5C,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,2EAA2E;IAC3E,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,0EAA0E;IAC1E,QAAQ,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAA;IAClC,0EAA0E;IAC1E,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAA;IACjC,oEAAoE;IACpE,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAA;IACnC,QAAQ,CAAC,KAAK,EAAE,SAAS,OAAO,EAAE,CAAA;IAClC,2DAA2D;IAC3D,QAAQ,CAAC,MAAM,EAAE,SAAS,CAAC,SAAS,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,EAAE,CAAA;IACvD,yEAAyE;IACzE,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CACzB;AAiED;;;;;;;;;GASG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,kBAAkB,GAAG,UAAU,CAEzE"}