@base44/app-plugin-commerce 0.1.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 (173) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +117 -0
  3. package/base44/agents/commerce/StoreAdmin.jsonc +64 -0
  4. package/base44/entities/commerce.Cart.jsonc +73 -0
  5. package/base44/entities/commerce.Coupon.jsonc +113 -0
  6. package/base44/entities/commerce.Customer.jsonc +96 -0
  7. package/base44/entities/commerce.DownloadPermission.jsonc +54 -0
  8. package/base44/entities/commerce.EmailLog.jsonc +43 -0
  9. package/base44/entities/commerce.Order.jsonc +287 -0
  10. package/base44/entities/commerce.OrderNote.jsonc +31 -0
  11. package/base44/entities/commerce.OrderRefund.jsonc +64 -0
  12. package/base44/entities/commerce.PaymentGateway.jsonc +48 -0
  13. package/base44/entities/commerce.Product.jsonc +291 -0
  14. package/base44/entities/commerce.ProductAttribute.jsonc +39 -0
  15. package/base44/entities/commerce.ProductAttributeTerm.jsonc +38 -0
  16. package/base44/entities/commerce.ProductCategory.jsonc +51 -0
  17. package/base44/entities/commerce.ProductReview.jsonc +48 -0
  18. package/base44/entities/commerce.ProductTag.jsonc +30 -0
  19. package/base44/entities/commerce.ProductVariation.jsonc +167 -0
  20. package/base44/entities/commerce.ShippingClass.jsonc +30 -0
  21. package/base44/entities/commerce.ShippingZone.jsonc +41 -0
  22. package/base44/entities/commerce.ShippingZoneMethod.jsonc +84 -0
  23. package/base44/entities/commerce.StoreSettings.jsonc +23 -0
  24. package/base44/entities/commerce.TaxClass.jsonc +23 -0
  25. package/base44/entities/commerce.TaxRate.jsonc +68 -0
  26. package/base44/entities/commerce.Webhook.jsonc +57 -0
  27. package/base44/entities/commerce.WebhookDelivery.jsonc +45 -0
  28. package/base44/functions/commerce/admin-coupons/entry.ts +100 -0
  29. package/base44/functions/commerce/admin-customers/entry.ts +141 -0
  30. package/base44/functions/commerce/admin-orders/entry.ts +396 -0
  31. package/base44/functions/commerce/admin-orders/helpers.ts +246 -0
  32. package/base44/functions/commerce/admin-products/entry.ts +506 -0
  33. package/base44/functions/commerce/admin-refunds/entry.ts +158 -0
  34. package/base44/functions/commerce/admin-reports/entry.ts +283 -0
  35. package/base44/functions/commerce/admin-reviews/entry.ts +66 -0
  36. package/base44/functions/commerce/admin-tools/entry.ts +261 -0
  37. package/base44/functions/commerce/admin-webhooks/entry.ts +52 -0
  38. package/base44/functions/commerce/payment-webhook/entry.ts +135 -0
  39. package/base44/functions/commerce/payments/entry.ts +238 -0
  40. package/base44/functions/commerce/seed-store/defaults.ts +162 -0
  41. package/base44/functions/commerce/seed-store/entry.ts +310 -0
  42. package/base44/functions/commerce/seed-store/sample-data.ts +349 -0
  43. package/base44/functions/commerce/storefront-account/entry.ts +207 -0
  44. package/base44/functions/commerce/storefront-cart/cart-pricing.ts +258 -0
  45. package/base44/functions/commerce/storefront-cart/entry.ts +283 -0
  46. package/base44/functions/commerce/storefront-catalog/entry.ts +459 -0
  47. package/base44/functions/commerce/storefront-checkout/cart-pricing.ts +258 -0
  48. package/base44/functions/commerce/storefront-checkout/entry.ts +485 -0
  49. package/base44/shared/commerce/auth.ts +60 -0
  50. package/base44/shared/commerce/coupons.ts +257 -0
  51. package/base44/shared/commerce/data/continents.ts +75 -0
  52. package/base44/shared/commerce/data/countries.ts +307 -0
  53. package/base44/shared/commerce/data/currencies.ts +46 -0
  54. package/base44/shared/commerce/email-templates.ts +240 -0
  55. package/base44/shared/commerce/emails.ts +225 -0
  56. package/base44/shared/commerce/money.ts +66 -0
  57. package/base44/shared/commerce/orders.ts +251 -0
  58. package/base44/shared/commerce/payments.ts +495 -0
  59. package/base44/shared/commerce/reviews.ts +36 -0
  60. package/base44/shared/commerce/scan.ts +57 -0
  61. package/base44/shared/commerce/sequence.ts +35 -0
  62. package/base44/shared/commerce/settings.ts +57 -0
  63. package/base44/shared/commerce/shipping.ts +215 -0
  64. package/base44/shared/commerce/stock.ts +227 -0
  65. package/base44/shared/commerce/stripe.ts +463 -0
  66. package/base44/shared/commerce/tax.ts +136 -0
  67. package/base44/shared/commerce/totals.ts +314 -0
  68. package/base44/shared/commerce/webhooks.ts +116 -0
  69. package/package.json +37 -0
  70. package/scripts/install.js +156 -0
  71. package/skills/commerce/SKILL.md +62 -0
  72. package/skills/commerce/docs/api-admin.md +186 -0
  73. package/skills/commerce/docs/api-storefront.md +408 -0
  74. package/skills/commerce/installation-guidelines.md +91 -0
  75. package/skills/commerce/post-installation.md +157 -0
  76. package/skills/commerce/references/emails.md +13 -0
  77. package/skills/commerce/references/guest-access-security.md +18 -0
  78. package/skills/commerce/references/limits-and-performance.md +16 -0
  79. package/skills/commerce/references/media-and-downloads.md +4 -0
  80. package/skills/commerce/references/online-payments.md +201 -0
  81. package/skills/commerce/references/product-render.md +87 -0
  82. package/skills/commerce/references/scheduled-work.md +19 -0
  83. package/skills/commerce/references/storefront-product-page.md +83 -0
  84. package/skills/commerce/references/webhooks.md +8 -0
  85. package/src/commerce/admin/README.md +107 -0
  86. package/src/commerce/admin/bot/Markdown.jsx +138 -0
  87. package/src/commerce/admin/bot/StoreAdminBot.jsx +249 -0
  88. package/src/commerce/admin/bot/pipe-tables.js +116 -0
  89. package/src/commerce/admin/components/AddressForm.jsx +78 -0
  90. package/src/commerce/admin/components/ConfirmDialog.jsx +52 -0
  91. package/src/commerce/admin/components/CountrySelect.jsx +81 -0
  92. package/src/commerce/admin/components/DataTable.jsx +192 -0
  93. package/src/commerce/admin/components/DateRangePicker.jsx +91 -0
  94. package/src/commerce/admin/components/EmptyState.jsx +17 -0
  95. package/src/commerce/admin/components/MediaUploader.jsx +116 -0
  96. package/src/commerce/admin/components/MetaDataEditor.jsx +45 -0
  97. package/src/commerce/admin/components/MoneyInput.jsx +50 -0
  98. package/src/commerce/admin/components/PageHeader.jsx +29 -0
  99. package/src/commerce/admin/components/RichTextarea.jsx +21 -0
  100. package/src/commerce/admin/components/SearchSelect.jsx +142 -0
  101. package/src/commerce/admin/components/StatusBadge.jsx +17 -0
  102. package/src/commerce/admin/context/BasePathContext.jsx +26 -0
  103. package/src/commerce/admin/context/SettingsContext.jsx +207 -0
  104. package/src/commerce/admin/hooks/useAsync.js +46 -0
  105. package/src/commerce/admin/hooks/useDebounce.js +11 -0
  106. package/src/commerce/admin/hooks/useMoney.js +52 -0
  107. package/src/commerce/admin/hooks/usePagedList.js +83 -0
  108. package/src/commerce/admin/hooks/usePaymentProvider.js +27 -0
  109. package/src/commerce/admin/hooks/useRealtime.js +129 -0
  110. package/src/commerce/admin/index.jsx +34 -0
  111. package/src/commerce/admin/layout/AccessDenied.jsx +54 -0
  112. package/src/commerce/admin/layout/AdminLayout.jsx +33 -0
  113. package/src/commerce/admin/layout/AuthGuard.jsx +84 -0
  114. package/src/commerce/admin/layout/Sidebar.jsx +130 -0
  115. package/src/commerce/admin/layout/Topbar.jsx +94 -0
  116. package/src/commerce/admin/lib/api.js +55 -0
  117. package/src/commerce/admin/lib/constants.js +157 -0
  118. package/src/commerce/admin/lib/format.js +27 -0
  119. package/src/commerce/admin/lib/geo-data.js +125 -0
  120. package/src/commerce/admin/lib/order-utils.js +147 -0
  121. package/src/commerce/admin/lib/paths.js +35 -0
  122. package/src/commerce/admin/lib/product-utils.js +55 -0
  123. package/src/commerce/admin/pages/Dashboard.jsx +245 -0
  124. package/src/commerce/admin/pages/coupons/CouponEditor.jsx +565 -0
  125. package/src/commerce/admin/pages/coupons/CouponsList.jsx +172 -0
  126. package/src/commerce/admin/pages/customers/CustomerEditor.jsx +318 -0
  127. package/src/commerce/admin/pages/customers/CustomersList.jsx +169 -0
  128. package/src/commerce/admin/pages/orders/OrderEditor.jsx +952 -0
  129. package/src/commerce/admin/pages/orders/OrdersList.jsx +227 -0
  130. package/src/commerce/admin/pages/orders/components/AddProductDialog.jsx +149 -0
  131. package/src/commerce/admin/pages/orders/components/DownloadPermissionsPanel.jsx +119 -0
  132. package/src/commerce/admin/pages/orders/components/LineItemsTable.jsx +208 -0
  133. package/src/commerce/admin/pages/orders/components/OrderNotesPanel.jsx +123 -0
  134. package/src/commerce/admin/pages/orders/components/PaymentPanel.jsx +199 -0
  135. package/src/commerce/admin/pages/orders/components/RefundPanel.jsx +239 -0
  136. package/src/commerce/admin/pages/orders/components/TotalsBox.jsx +52 -0
  137. package/src/commerce/admin/pages/products/AttributeTerms.jsx +180 -0
  138. package/src/commerce/admin/pages/products/Attributes.jsx +183 -0
  139. package/src/commerce/admin/pages/products/Categories.jsx +236 -0
  140. package/src/commerce/admin/pages/products/ProductEditor.jsx +267 -0
  141. package/src/commerce/admin/pages/products/ProductsList.jsx +391 -0
  142. package/src/commerce/admin/pages/products/Reviews.jsx +255 -0
  143. package/src/commerce/admin/pages/products/Tags.jsx +150 -0
  144. package/src/commerce/admin/pages/products/components/ProductDataPanel.jsx +132 -0
  145. package/src/commerce/admin/pages/products/components/PublishBox.jsx +101 -0
  146. package/src/commerce/admin/pages/products/components/TaxonomyPanel.jsx +243 -0
  147. package/src/commerce/admin/pages/products/components/tabs/AdvancedTab.jsx +48 -0
  148. package/src/commerce/admin/pages/products/components/tabs/AttributesTab.jsx +208 -0
  149. package/src/commerce/admin/pages/products/components/tabs/DownloadsTab.jsx +91 -0
  150. package/src/commerce/admin/pages/products/components/tabs/ExternalTab.jsx +41 -0
  151. package/src/commerce/admin/pages/products/components/tabs/GeneralTab.jsx +103 -0
  152. package/src/commerce/admin/pages/products/components/tabs/InventoryTab.jsx +93 -0
  153. package/src/commerce/admin/pages/products/components/tabs/LinkedTab.jsx +102 -0
  154. package/src/commerce/admin/pages/products/components/tabs/ShippingTab.jsx +86 -0
  155. package/src/commerce/admin/pages/products/components/tabs/VariationsTab.jsx +377 -0
  156. package/src/commerce/admin/pages/reports/Reports.jsx +416 -0
  157. package/src/commerce/admin/pages/settings/EmailsSettings.jsx +240 -0
  158. package/src/commerce/admin/pages/settings/GeneralSettings.jsx +232 -0
  159. package/src/commerce/admin/pages/settings/InventorySettings.jsx +146 -0
  160. package/src/commerce/admin/pages/settings/PaymentsSettings.jsx +260 -0
  161. package/src/commerce/admin/pages/settings/ProductsSettings.jsx +118 -0
  162. package/src/commerce/admin/pages/settings/SettingsLayout.jsx +53 -0
  163. package/src/commerce/admin/pages/settings/ShippingSettings.jsx +304 -0
  164. package/src/commerce/admin/pages/settings/ShippingZoneEditor.jsx +514 -0
  165. package/src/commerce/admin/pages/settings/TaxRatesTable.jsx +231 -0
  166. package/src/commerce/admin/pages/settings/TaxSettings.jsx +281 -0
  167. package/src/commerce/admin/pages/settings/useGroupForm.jsx +76 -0
  168. package/src/commerce/admin/pages/status/WebhookEditor.jsx +296 -0
  169. package/src/commerce/admin/pages/status/Webhooks.jsx +53 -0
  170. package/src/commerce/admin/routes.jsx +151 -0
  171. package/src/commerce/utils/index.js +19 -0
  172. package/src/commerce/utils/shipping-promos.js +99 -0
  173. package/src/commerce/utils/variants.js +411 -0
@@ -0,0 +1,257 @@
1
+ /**
2
+ * Coupon validation and discount application (standard commerce semantics).
3
+ * Validation runs the ordered checks from the plan; application supports
4
+ * percent / fixed_cart / fixed_product with the standard eligibility rules.
5
+ */
6
+ import { distributeProportionally, round2 } from "./money.ts";
7
+
8
+ export interface CouponLineInput {
9
+ line_id: string;
10
+ product_id: string;
11
+ variation_id?: string;
12
+ quantity: number;
13
+ unit_price: number; // ex-tax unit price
14
+ subtotal: number; // ex-tax pre-discount line total
15
+ discount: number; // accumulated so far (mutated by applyCoupons)
16
+ product?: any; // resolved Product (category_ids, on_sale)
17
+ variation?: any;
18
+ }
19
+
20
+ export interface CouponValidationCtx {
21
+ lines: CouponLineInput[];
22
+ itemsSubtotal: number;
23
+ customerEmail?: string;
24
+ appliedCoupons?: any[]; // other coupons already in the cart
25
+ }
26
+
27
+ export interface CouponValidationResult {
28
+ valid: boolean;
29
+ error?: string;
30
+ code?: string;
31
+ }
32
+
33
+ function emailMatchesRestriction(email: string, patterns: string[]): boolean {
34
+ const e = (email || "").toLowerCase();
35
+ return (patterns || []).some((p) => {
36
+ const pat = (p || "").toLowerCase().trim();
37
+ if (!pat) return false;
38
+ const re = new RegExp(
39
+ "^" + pat.split("*").map((s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join(".*") + "$",
40
+ );
41
+ return re.test(e);
42
+ });
43
+ }
44
+
45
+ /** Lines a coupon can touch: include-lists intersect, exclude-lists subtract. */
46
+ export function eligibleLines(coupon: any, lines: CouponLineInput[]): CouponLineInput[] {
47
+ let out = [...(lines || [])];
48
+ const inclProducts: string[] = coupon.product_ids || [];
49
+ const exclProducts: string[] = coupon.excluded_product_ids || [];
50
+ const inclCats: string[] = coupon.product_category_ids || [];
51
+ const exclCats: string[] = coupon.excluded_product_category_ids || [];
52
+
53
+ if (inclProducts.length) {
54
+ out = out.filter((l) => inclProducts.includes(l.product_id) || (l.variation_id && inclProducts.includes(l.variation_id)));
55
+ }
56
+ if (exclProducts.length) {
57
+ out = out.filter((l) => !exclProducts.includes(l.product_id) && !(l.variation_id && exclProducts.includes(l.variation_id)));
58
+ }
59
+ if (inclCats.length) {
60
+ out = out.filter((l) => (l.product?.category_ids || []).some((c: string) => inclCats.includes(c)));
61
+ }
62
+ if (exclCats.length) {
63
+ out = out.filter((l) => !(l.product?.category_ids || []).some((c: string) => exclCats.includes(c)));
64
+ }
65
+ if (coupon.exclude_sale_items) {
66
+ out = out.filter((l) => !(l.variation?.on_sale ?? l.product?.on_sale));
67
+ }
68
+ return out;
69
+ }
70
+
71
+ /**
72
+ * The 9 ordered validation checks. Pass `coupon = null` for a lookup miss.
73
+ * Error codes: not_found | expired | usage_limit_reached |
74
+ * usage_limit_per_user_reached | individual_use | min_amount_not_met |
75
+ * max_amount_exceeded | not_applicable | sale_items_excluded | email_restricted
76
+ */
77
+ export function validateCoupon(coupon: any | null, ctx: CouponValidationCtx): CouponValidationResult {
78
+ // 1. exists
79
+ if (!coupon) return { valid: false, code: "not_found", error: "Coupon does not exist." };
80
+
81
+ // 2. expiry
82
+ if (coupon.date_expires && new Date(coupon.date_expires).getTime() < Date.now()) {
83
+ return { valid: false, code: "expired", error: "This coupon has expired." };
84
+ }
85
+
86
+ // 3. global usage limit
87
+ if (coupon.usage_limit != null && (coupon.usage_count ?? 0) >= coupon.usage_limit) {
88
+ return { valid: false, code: "usage_limit_reached", error: "Coupon usage limit has been reached." };
89
+ }
90
+
91
+ // 4. per-user usage limit (usages recorded in used_by, one entry per use)
92
+ if (coupon.usage_limit_per_user != null && ctx.customerEmail) {
93
+ const uses = (coupon.used_by || []).filter(
94
+ (e: string) => (e || "").toLowerCase() === ctx.customerEmail!.toLowerCase(),
95
+ ).length;
96
+ if (uses >= coupon.usage_limit_per_user) {
97
+ return { valid: false, code: "usage_limit_per_user_reached", error: "Coupon usage limit has been reached for your account." };
98
+ }
99
+ }
100
+
101
+ // 5. individual use — both directions
102
+ const others = (ctx.appliedCoupons || []).filter((c) => c.code !== coupon.code);
103
+ if (coupon.individual_use && others.length) {
104
+ return { valid: false, code: "individual_use", error: "This coupon cannot be used in conjunction with other coupons." };
105
+ }
106
+ if (others.some((c) => c.individual_use)) {
107
+ return { valid: false, code: "individual_use", error: "An applied coupon does not allow other coupons." };
108
+ }
109
+
110
+ // 6. min / max spend (vs items subtotal, ex tax)
111
+ if (coupon.minimum_amount != null && coupon.minimum_amount > 0 && ctx.itemsSubtotal < coupon.minimum_amount) {
112
+ return { valid: false, code: "min_amount_not_met", error: `The minimum spend for this coupon is ${coupon.minimum_amount}.` };
113
+ }
114
+ if (coupon.maximum_amount != null && coupon.maximum_amount > 0 && ctx.itemsSubtotal > coupon.maximum_amount) {
115
+ return { valid: false, code: "max_amount_exceeded", error: `The maximum spend for this coupon is ${coupon.maximum_amount}.` };
116
+ }
117
+
118
+ // 7. product/category restrictions must leave something to discount
119
+ const restricted =
120
+ (coupon.product_ids || []).length || (coupon.excluded_product_ids || []).length ||
121
+ (coupon.product_category_ids || []).length || (coupon.excluded_product_category_ids || []).length;
122
+ const withoutSaleFilter = eligibleLines({ ...coupon, exclude_sale_items: false }, ctx.lines);
123
+ if (restricted && !withoutSaleFilter.length) {
124
+ return { valid: false, code: "not_applicable", error: "Sorry, this coupon is not applicable to your cart contents." };
125
+ }
126
+
127
+ // 8. exclude_sale_items must still leave eligible lines
128
+ if (coupon.exclude_sale_items && withoutSaleFilter.length && !eligibleLines(coupon, ctx.lines).length) {
129
+ return { valid: false, code: "sale_items_excluded", error: "Sorry, this coupon is not valid for sale items." };
130
+ }
131
+
132
+ // 9. email restrictions
133
+ if ((coupon.email_restrictions || []).length) {
134
+ if (!ctx.customerEmail || !emailMatchesRestriction(ctx.customerEmail, coupon.email_restrictions)) {
135
+ return { valid: false, code: "email_restricted", error: "This coupon is not valid for your email address." };
136
+ }
137
+ }
138
+
139
+ return { valid: true };
140
+ }
141
+
142
+ export interface CouponLineTotal {
143
+ code: string;
144
+ coupon_id: string;
145
+ discount: number;
146
+ discount_tax: number; // filled in by totals.ts after tax
147
+ free_shipping: boolean;
148
+ }
149
+
150
+ /**
151
+ * Apply coupons in order, mutating each line's `.discount`.
152
+ * `calc_discounts_sequentially` (general settings): when true each coupon sees
153
+ * the line total minus prior discounts; when false all see the original
154
+ * subtotal (the default). Line discounts never exceed the line's remaining value.
155
+ */
156
+ export function applyCoupons(
157
+ lines: CouponLineInput[],
158
+ coupons: any[],
159
+ settings: Record<string, any>,
160
+ ): CouponLineTotal[] {
161
+ const sequential = !!settings?.general?.calc_discounts_sequentially;
162
+ const couponLines: CouponLineTotal[] = [];
163
+ const base = (l: CouponLineInput) => Math.max(0, sequential ? l.subtotal - l.discount : l.subtotal);
164
+ const remaining = (l: CouponLineInput) => Math.max(0, round2(l.subtotal - l.discount));
165
+
166
+ for (const coupon of coupons || []) {
167
+ const eligible = eligibleLines(coupon, lines);
168
+ const amount = Number(coupon.amount) || 0;
169
+ let discountTotal = 0;
170
+ const add = (l: CouponLineInput, d: number) => {
171
+ const capped = Math.min(round2(d), remaining(l));
172
+ if (capped <= 0) return;
173
+ l.discount = round2(l.discount + capped);
174
+ discountTotal = round2(discountTotal + capped);
175
+ };
176
+
177
+ switch (coupon.discount_type) {
178
+ case "percent": {
179
+ for (const l of eligible) add(l, base(l) * (amount / 100));
180
+ break;
181
+ }
182
+ case "fixed_product": {
183
+ // per-unit discount; limit_usage_to_x_items caps units, priciest first
184
+ let unitsCap = coupon.limit_usage_to_x_items != null ? Number(coupon.limit_usage_to_x_items) : Infinity;
185
+ const sorted = [...eligible].sort((a, b) => b.unit_price - a.unit_price);
186
+ for (const l of sorted) {
187
+ if (unitsCap <= 0) break;
188
+ const units = Math.min(l.quantity, unitsCap);
189
+ const perUnit = Math.min(amount, l.unit_price);
190
+ add(l, perUnit * units);
191
+ unitsCap -= units;
192
+ }
193
+ break;
194
+ }
195
+ case "fixed_cart":
196
+ default: {
197
+ // distribute across eligible lines proportionally, cents-exact
198
+ const weights = eligible.map((l) => base(l));
199
+ const pool = Math.min(amount, weights.reduce((a, b) => a + b, 0));
200
+ const shares = distributeProportionally(pool, weights);
201
+ eligible.forEach((l, i) => add(l, shares[i] ?? 0));
202
+ }
203
+ }
204
+
205
+ couponLines.push({
206
+ code: coupon.code,
207
+ coupon_id: coupon.id ?? "",
208
+ discount: discountTotal,
209
+ discount_tax: 0,
210
+ free_shipping: !!coupon.free_shipping,
211
+ });
212
+ }
213
+ return couponLines;
214
+ }
215
+
216
+ /**
217
+ * Record coupon usage for an order (usage_count +1, used_by += billing email
218
+ * per coupon line). Caller guards/persists order.coupon_usages_counted.
219
+ */
220
+ export async function countUsage(sr: any, order: any): Promise<void> {
221
+ const email = (order.billing?.email || "").toLowerCase();
222
+ for (const cl of order.coupon_lines || []) {
223
+ const coupon = await findCoupon(sr, cl);
224
+ if (!coupon) continue;
225
+ await sr.entities["commerce.Coupon"].update(coupon.id, {
226
+ usage_count: (coupon.usage_count ?? 0) + 1,
227
+ used_by: [...(coupon.used_by || []), email],
228
+ });
229
+ }
230
+ }
231
+
232
+ /** Reverse of countUsage (removes ONE used_by entry per coupon line). */
233
+ export async function uncountUsage(sr: any, order: any): Promise<void> {
234
+ const email = (order.billing?.email || "").toLowerCase();
235
+ for (const cl of order.coupon_lines || []) {
236
+ const coupon = await findCoupon(sr, cl);
237
+ if (!coupon) continue;
238
+ const usedBy = [...(coupon.used_by || [])];
239
+ const idx = usedBy.findIndex((e: string) => (e || "").toLowerCase() === email);
240
+ if (idx >= 0) usedBy.splice(idx, 1);
241
+ await sr.entities["commerce.Coupon"].update(coupon.id, {
242
+ usage_count: Math.max(0, (coupon.usage_count ?? 0) - 1),
243
+ used_by: usedBy,
244
+ });
245
+ }
246
+ }
247
+
248
+ async function findCoupon(sr: any, couponLine: any): Promise<any | null> {
249
+ if (couponLine.coupon_id) {
250
+ try {
251
+ const c = await sr.entities["commerce.Coupon"].get(couponLine.coupon_id);
252
+ if (c) return c;
253
+ } catch { /* fall through to code lookup */ }
254
+ }
255
+ const matches = await sr.entities["commerce.Coupon"].filter({ code: (couponLine.code || "").toLowerCase() }, undefined, 1);
256
+ return matches?.[0] ?? null;
257
+ }
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Continent → country-code groupings (reference /data/continents dataset).
3
+ * Used by shipping-zone location matching (location type "continent").
4
+ */
5
+ export interface ContinentInfo {
6
+ code: string;
7
+ name: string;
8
+ countries: string[];
9
+ }
10
+
11
+ export const CONTINENTS: ContinentInfo[] = [
12
+ {
13
+ code: "AF",
14
+ name: "Africa",
15
+ countries: [
16
+ "AO", "BF", "BI", "BJ", "BW", "CD", "CF", "CG", "CI", "CM", "CV", "DJ", "DZ", "EG", "EH", "ER",
17
+ "ET", "GA", "GH", "GM", "GN", "GQ", "GW", "KE", "KM", "LR", "LS", "LY", "MA", "MG", "ML", "MR",
18
+ "MU", "MW", "MZ", "NA", "NE", "NG", "RE", "RW", "SC", "SD", "SH", "SL", "SN", "SO", "SS", "ST",
19
+ "SZ", "TD", "TG", "TN", "TZ", "UG", "YT", "ZA", "ZM", "ZW",
20
+ ],
21
+ },
22
+ {
23
+ code: "AN",
24
+ name: "Antarctica",
25
+ countries: ["AQ", "BV", "GS", "HM", "TF"],
26
+ },
27
+ {
28
+ code: "AS",
29
+ name: "Asia",
30
+ countries: [
31
+ "AE", "AF", "AM", "AZ", "BD", "BH", "BN", "BT", "CC", "CN", "CX", "CY", "GE", "HK", "ID", "IL",
32
+ "IN", "IO", "IQ", "IR", "JO", "JP", "KG", "KH", "KP", "KR", "KW", "KZ", "LA", "LB", "LK", "MM",
33
+ "MN", "MO", "MV", "MY", "NP", "OM", "PH", "PK", "PS", "QA", "SA", "SG", "SY", "TH", "TJ", "TL",
34
+ "TM", "TR", "TW", "UZ", "VN", "YE",
35
+ ],
36
+ },
37
+ {
38
+ code: "EU",
39
+ name: "Europe",
40
+ countries: [
41
+ "AD", "AL", "AT", "AX", "BA", "BE", "BG", "BY", "CH", "CZ", "DE", "DK", "EE", "ES", "FI", "FO",
42
+ "FR", "GB", "GG", "GI", "GR", "HR", "HU", "IE", "IM", "IS", "IT", "JE", "LI", "LT", "LU", "LV",
43
+ "MC", "MD", "ME", "MK", "MT", "NL", "NO", "PL", "PT", "RO", "RS", "RU", "SE", "SI", "SJ", "SK",
44
+ "SM", "UA", "VA",
45
+ ],
46
+ },
47
+ {
48
+ code: "NA",
49
+ name: "North America",
50
+ countries: [
51
+ "AG", "AI", "AW", "BB", "BL", "BM", "BQ", "BS", "BZ", "CA", "CR", "CU", "CW", "DM", "DO", "GD",
52
+ "GL", "GP", "GT", "HN", "HT", "JM", "KN", "KY", "LC", "MF", "MQ", "MS", "MX", "NI", "PA", "PM",
53
+ "PR", "SV", "SX", "TC", "TT", "US", "VC", "VG", "VI",
54
+ ],
55
+ },
56
+ {
57
+ code: "OC",
58
+ name: "Oceania",
59
+ countries: [
60
+ "AS", "AU", "CK", "FJ", "FM", "GU", "KI", "MH", "MP", "NC", "NF", "NR", "NU", "NZ", "PF", "PG",
61
+ "PN", "PW", "SB", "TK", "TO", "TV", "UM", "VU", "WF", "WS",
62
+ ],
63
+ },
64
+ {
65
+ code: "SA",
66
+ name: "South America",
67
+ countries: ["AR", "BO", "BR", "CL", "CO", "EC", "FK", "GF", "GY", "PE", "PY", "SR", "UY", "VE"],
68
+ },
69
+ ];
70
+
71
+ /** Continent containing the given country code, if any. */
72
+ export function continentOf(countryCode: string): ContinentInfo | undefined {
73
+ const cc = (countryCode || "").toUpperCase();
74
+ return CONTINENTS.find((c) => c.countries.includes(cc));
75
+ }
@@ -0,0 +1,307 @@
1
+ /**
2
+ * ISO 3166-1 countries with state/province subdivisions for the countries the
3
+ * template ships with (US, CA, AU — a common default trio;
4
+ * other countries use a free-text state field). A reference
5
+ * /data/countries dataset. Extend freely — plain static data.
6
+ */
7
+ export interface StateInfo {
8
+ code: string;
9
+ name: string;
10
+ }
11
+ export interface CountryInfo {
12
+ code: string;
13
+ name: string;
14
+ states?: StateInfo[];
15
+ }
16
+
17
+ const US_STATES: StateInfo[] = [
18
+ { code: "AL", name: "Alabama" }, { code: "AK", name: "Alaska" }, { code: "AZ", name: "Arizona" },
19
+ { code: "AR", name: "Arkansas" }, { code: "CA", name: "California" }, { code: "CO", name: "Colorado" },
20
+ { code: "CT", name: "Connecticut" }, { code: "DE", name: "Delaware" }, { code: "DC", name: "District of Columbia" },
21
+ { code: "FL", name: "Florida" }, { code: "GA", name: "Georgia" }, { code: "HI", name: "Hawaii" },
22
+ { code: "ID", name: "Idaho" }, { code: "IL", name: "Illinois" }, { code: "IN", name: "Indiana" },
23
+ { code: "IA", name: "Iowa" }, { code: "KS", name: "Kansas" }, { code: "KY", name: "Kentucky" },
24
+ { code: "LA", name: "Louisiana" }, { code: "ME", name: "Maine" }, { code: "MD", name: "Maryland" },
25
+ { code: "MA", name: "Massachusetts" }, { code: "MI", name: "Michigan" }, { code: "MN", name: "Minnesota" },
26
+ { code: "MS", name: "Mississippi" }, { code: "MO", name: "Missouri" }, { code: "MT", name: "Montana" },
27
+ { code: "NE", name: "Nebraska" }, { code: "NV", name: "Nevada" }, { code: "NH", name: "New Hampshire" },
28
+ { code: "NJ", name: "New Jersey" }, { code: "NM", name: "New Mexico" }, { code: "NY", name: "New York" },
29
+ { code: "NC", name: "North Carolina" }, { code: "ND", name: "North Dakota" }, { code: "OH", name: "Ohio" },
30
+ { code: "OK", name: "Oklahoma" }, { code: "OR", name: "Oregon" }, { code: "PA", name: "Pennsylvania" },
31
+ { code: "RI", name: "Rhode Island" }, { code: "SC", name: "South Carolina" }, { code: "SD", name: "South Dakota" },
32
+ { code: "TN", name: "Tennessee" }, { code: "TX", name: "Texas" }, { code: "UT", name: "Utah" },
33
+ { code: "VT", name: "Vermont" }, { code: "VA", name: "Virginia" }, { code: "WA", name: "Washington" },
34
+ { code: "WV", name: "West Virginia" }, { code: "WI", name: "Wisconsin" }, { code: "WY", name: "Wyoming" },
35
+ ];
36
+
37
+ const CA_PROVINCES: StateInfo[] = [
38
+ { code: "AB", name: "Alberta" }, { code: "BC", name: "British Columbia" }, { code: "MB", name: "Manitoba" },
39
+ { code: "NB", name: "New Brunswick" }, { code: "NL", name: "Newfoundland and Labrador" },
40
+ { code: "NT", name: "Northwest Territories" }, { code: "NS", name: "Nova Scotia" }, { code: "NU", name: "Nunavut" },
41
+ { code: "ON", name: "Ontario" }, { code: "PE", name: "Prince Edward Island" }, { code: "QC", name: "Quebec" },
42
+ { code: "SK", name: "Saskatchewan" }, { code: "YT", name: "Yukon" },
43
+ ];
44
+
45
+ const AU_STATES: StateInfo[] = [
46
+ { code: "ACT", name: "Australian Capital Territory" }, { code: "NSW", name: "New South Wales" },
47
+ { code: "NT", name: "Northern Territory" }, { code: "QLD", name: "Queensland" },
48
+ { code: "SA", name: "South Australia" }, { code: "TAS", name: "Tasmania" },
49
+ { code: "VIC", name: "Victoria" }, { code: "WA", name: "Western Australia" },
50
+ ];
51
+
52
+ export const COUNTRIES: CountryInfo[] = [
53
+ { code: "AF", name: "Afghanistan" },
54
+ { code: "AX", name: "Åland Islands" },
55
+ { code: "AL", name: "Albania" },
56
+ { code: "DZ", name: "Algeria" },
57
+ { code: "AS", name: "American Samoa" },
58
+ { code: "AD", name: "Andorra" },
59
+ { code: "AO", name: "Angola" },
60
+ { code: "AI", name: "Anguilla" },
61
+ { code: "AQ", name: "Antarctica" },
62
+ { code: "AG", name: "Antigua and Barbuda" },
63
+ { code: "AR", name: "Argentina" },
64
+ { code: "AM", name: "Armenia" },
65
+ { code: "AW", name: "Aruba" },
66
+ { code: "AU", name: "Australia", states: AU_STATES },
67
+ { code: "AT", name: "Austria" },
68
+ { code: "AZ", name: "Azerbaijan" },
69
+ { code: "BS", name: "Bahamas" },
70
+ { code: "BH", name: "Bahrain" },
71
+ { code: "BD", name: "Bangladesh" },
72
+ { code: "BB", name: "Barbados" },
73
+ { code: "BY", name: "Belarus" },
74
+ { code: "BE", name: "Belgium" },
75
+ { code: "BZ", name: "Belize" },
76
+ { code: "BJ", name: "Benin" },
77
+ { code: "BM", name: "Bermuda" },
78
+ { code: "BT", name: "Bhutan" },
79
+ { code: "BO", name: "Bolivia" },
80
+ { code: "BQ", name: "Bonaire, Sint Eustatius and Saba" },
81
+ { code: "BA", name: "Bosnia and Herzegovina" },
82
+ { code: "BW", name: "Botswana" },
83
+ { code: "BV", name: "Bouvet Island" },
84
+ { code: "BR", name: "Brazil" },
85
+ { code: "IO", name: "British Indian Ocean Territory" },
86
+ { code: "BN", name: "Brunei Darussalam" },
87
+ { code: "BG", name: "Bulgaria" },
88
+ { code: "BF", name: "Burkina Faso" },
89
+ { code: "BI", name: "Burundi" },
90
+ { code: "CV", name: "Cabo Verde" },
91
+ { code: "KH", name: "Cambodia" },
92
+ { code: "CM", name: "Cameroon" },
93
+ { code: "CA", name: "Canada", states: CA_PROVINCES },
94
+ { code: "KY", name: "Cayman Islands" },
95
+ { code: "CF", name: "Central African Republic" },
96
+ { code: "TD", name: "Chad" },
97
+ { code: "CL", name: "Chile" },
98
+ { code: "CN", name: "China" },
99
+ { code: "CX", name: "Christmas Island" },
100
+ { code: "CC", name: "Cocos (Keeling) Islands" },
101
+ { code: "CO", name: "Colombia" },
102
+ { code: "KM", name: "Comoros" },
103
+ { code: "CG", name: "Congo" },
104
+ { code: "CD", name: "Congo, Democratic Republic of the" },
105
+ { code: "CK", name: "Cook Islands" },
106
+ { code: "CR", name: "Costa Rica" },
107
+ { code: "CI", name: "Côte d'Ivoire" },
108
+ { code: "HR", name: "Croatia" },
109
+ { code: "CU", name: "Cuba" },
110
+ { code: "CW", name: "Curaçao" },
111
+ { code: "CY", name: "Cyprus" },
112
+ { code: "CZ", name: "Czechia" },
113
+ { code: "DK", name: "Denmark" },
114
+ { code: "DJ", name: "Djibouti" },
115
+ { code: "DM", name: "Dominica" },
116
+ { code: "DO", name: "Dominican Republic" },
117
+ { code: "EC", name: "Ecuador" },
118
+ { code: "EG", name: "Egypt" },
119
+ { code: "SV", name: "El Salvador" },
120
+ { code: "GQ", name: "Equatorial Guinea" },
121
+ { code: "ER", name: "Eritrea" },
122
+ { code: "EE", name: "Estonia" },
123
+ { code: "SZ", name: "Eswatini" },
124
+ { code: "ET", name: "Ethiopia" },
125
+ { code: "FK", name: "Falkland Islands" },
126
+ { code: "FO", name: "Faroe Islands" },
127
+ { code: "FJ", name: "Fiji" },
128
+ { code: "FI", name: "Finland" },
129
+ { code: "FR", name: "France" },
130
+ { code: "GF", name: "French Guiana" },
131
+ { code: "PF", name: "French Polynesia" },
132
+ { code: "TF", name: "French Southern Territories" },
133
+ { code: "GA", name: "Gabon" },
134
+ { code: "GM", name: "Gambia" },
135
+ { code: "GE", name: "Georgia" },
136
+ { code: "DE", name: "Germany" },
137
+ { code: "GH", name: "Ghana" },
138
+ { code: "GI", name: "Gibraltar" },
139
+ { code: "GR", name: "Greece" },
140
+ { code: "GL", name: "Greenland" },
141
+ { code: "GD", name: "Grenada" },
142
+ { code: "GP", name: "Guadeloupe" },
143
+ { code: "GU", name: "Guam" },
144
+ { code: "GT", name: "Guatemala" },
145
+ { code: "GG", name: "Guernsey" },
146
+ { code: "GN", name: "Guinea" },
147
+ { code: "GW", name: "Guinea-Bissau" },
148
+ { code: "GY", name: "Guyana" },
149
+ { code: "HT", name: "Haiti" },
150
+ { code: "HM", name: "Heard Island and McDonald Islands" },
151
+ { code: "VA", name: "Holy See" },
152
+ { code: "HN", name: "Honduras" },
153
+ { code: "HK", name: "Hong Kong" },
154
+ { code: "HU", name: "Hungary" },
155
+ { code: "IS", name: "Iceland" },
156
+ { code: "IN", name: "India" },
157
+ { code: "ID", name: "Indonesia" },
158
+ { code: "IR", name: "Iran" },
159
+ { code: "IQ", name: "Iraq" },
160
+ { code: "IE", name: "Ireland" },
161
+ { code: "IM", name: "Isle of Man" },
162
+ { code: "IL", name: "Israel" },
163
+ { code: "IT", name: "Italy" },
164
+ { code: "JM", name: "Jamaica" },
165
+ { code: "JP", name: "Japan" },
166
+ { code: "JE", name: "Jersey" },
167
+ { code: "JO", name: "Jordan" },
168
+ { code: "KZ", name: "Kazakhstan" },
169
+ { code: "KE", name: "Kenya" },
170
+ { code: "KI", name: "Kiribati" },
171
+ { code: "KP", name: "Korea, Democratic People's Republic of" },
172
+ { code: "KR", name: "Korea, Republic of" },
173
+ { code: "KW", name: "Kuwait" },
174
+ { code: "KG", name: "Kyrgyzstan" },
175
+ { code: "LA", name: "Lao People's Democratic Republic" },
176
+ { code: "LV", name: "Latvia" },
177
+ { code: "LB", name: "Lebanon" },
178
+ { code: "LS", name: "Lesotho" },
179
+ { code: "LR", name: "Liberia" },
180
+ { code: "LY", name: "Libya" },
181
+ { code: "LI", name: "Liechtenstein" },
182
+ { code: "LT", name: "Lithuania" },
183
+ { code: "LU", name: "Luxembourg" },
184
+ { code: "MO", name: "Macao" },
185
+ { code: "MG", name: "Madagascar" },
186
+ { code: "MW", name: "Malawi" },
187
+ { code: "MY", name: "Malaysia" },
188
+ { code: "MV", name: "Maldives" },
189
+ { code: "ML", name: "Mali" },
190
+ { code: "MT", name: "Malta" },
191
+ { code: "MH", name: "Marshall Islands" },
192
+ { code: "MQ", name: "Martinique" },
193
+ { code: "MR", name: "Mauritania" },
194
+ { code: "MU", name: "Mauritius" },
195
+ { code: "YT", name: "Mayotte" },
196
+ { code: "MX", name: "Mexico" },
197
+ { code: "FM", name: "Micronesia" },
198
+ { code: "MD", name: "Moldova" },
199
+ { code: "MC", name: "Monaco" },
200
+ { code: "MN", name: "Mongolia" },
201
+ { code: "ME", name: "Montenegro" },
202
+ { code: "MS", name: "Montserrat" },
203
+ { code: "MA", name: "Morocco" },
204
+ { code: "MZ", name: "Mozambique" },
205
+ { code: "MM", name: "Myanmar" },
206
+ { code: "NA", name: "Namibia" },
207
+ { code: "NR", name: "Nauru" },
208
+ { code: "NP", name: "Nepal" },
209
+ { code: "NL", name: "Netherlands" },
210
+ { code: "NC", name: "New Caledonia" },
211
+ { code: "NZ", name: "New Zealand" },
212
+ { code: "NI", name: "Nicaragua" },
213
+ { code: "NE", name: "Niger" },
214
+ { code: "NG", name: "Nigeria" },
215
+ { code: "NU", name: "Niue" },
216
+ { code: "NF", name: "Norfolk Island" },
217
+ { code: "MK", name: "North Macedonia" },
218
+ { code: "MP", name: "Northern Mariana Islands" },
219
+ { code: "NO", name: "Norway" },
220
+ { code: "OM", name: "Oman" },
221
+ { code: "PK", name: "Pakistan" },
222
+ { code: "PW", name: "Palau" },
223
+ { code: "PS", name: "Palestine, State of" },
224
+ { code: "PA", name: "Panama" },
225
+ { code: "PG", name: "Papua New Guinea" },
226
+ { code: "PY", name: "Paraguay" },
227
+ { code: "PE", name: "Peru" },
228
+ { code: "PH", name: "Philippines" },
229
+ { code: "PN", name: "Pitcairn" },
230
+ { code: "PL", name: "Poland" },
231
+ { code: "PT", name: "Portugal" },
232
+ { code: "PR", name: "Puerto Rico" },
233
+ { code: "QA", name: "Qatar" },
234
+ { code: "RE", name: "Réunion" },
235
+ { code: "RO", name: "Romania" },
236
+ { code: "RU", name: "Russian Federation" },
237
+ { code: "RW", name: "Rwanda" },
238
+ { code: "BL", name: "Saint Barthélemy" },
239
+ { code: "SH", name: "Saint Helena, Ascension and Tristan da Cunha" },
240
+ { code: "KN", name: "Saint Kitts and Nevis" },
241
+ { code: "LC", name: "Saint Lucia" },
242
+ { code: "MF", name: "Saint Martin (French part)" },
243
+ { code: "PM", name: "Saint Pierre and Miquelon" },
244
+ { code: "VC", name: "Saint Vincent and the Grenadines" },
245
+ { code: "WS", name: "Samoa" },
246
+ { code: "SM", name: "San Marino" },
247
+ { code: "ST", name: "Sao Tome and Principe" },
248
+ { code: "SA", name: "Saudi Arabia" },
249
+ { code: "SN", name: "Senegal" },
250
+ { code: "RS", name: "Serbia" },
251
+ { code: "SC", name: "Seychelles" },
252
+ { code: "SL", name: "Sierra Leone" },
253
+ { code: "SG", name: "Singapore" },
254
+ { code: "SX", name: "Sint Maarten (Dutch part)" },
255
+ { code: "SK", name: "Slovakia" },
256
+ { code: "SI", name: "Slovenia" },
257
+ { code: "SB", name: "Solomon Islands" },
258
+ { code: "SO", name: "Somalia" },
259
+ { code: "ZA", name: "South Africa" },
260
+ { code: "GS", name: "South Georgia and the South Sandwich Islands" },
261
+ { code: "SS", name: "South Sudan" },
262
+ { code: "ES", name: "Spain" },
263
+ { code: "LK", name: "Sri Lanka" },
264
+ { code: "SD", name: "Sudan" },
265
+ { code: "SR", name: "Suriname" },
266
+ { code: "SJ", name: "Svalbard and Jan Mayen" },
267
+ { code: "SE", name: "Sweden" },
268
+ { code: "CH", name: "Switzerland" },
269
+ { code: "SY", name: "Syrian Arab Republic" },
270
+ { code: "TW", name: "Taiwan" },
271
+ { code: "TJ", name: "Tajikistan" },
272
+ { code: "TZ", name: "Tanzania" },
273
+ { code: "TH", name: "Thailand" },
274
+ { code: "TL", name: "Timor-Leste" },
275
+ { code: "TG", name: "Togo" },
276
+ { code: "TK", name: "Tokelau" },
277
+ { code: "TO", name: "Tonga" },
278
+ { code: "TT", name: "Trinidad and Tobago" },
279
+ { code: "TN", name: "Tunisia" },
280
+ { code: "TR", name: "Türkiye" },
281
+ { code: "TM", name: "Turkmenistan" },
282
+ { code: "TC", name: "Turks and Caicos Islands" },
283
+ { code: "TV", name: "Tuvalu" },
284
+ { code: "UG", name: "Uganda" },
285
+ { code: "UA", name: "Ukraine" },
286
+ { code: "AE", name: "United Arab Emirates" },
287
+ { code: "GB", name: "United Kingdom" },
288
+ { code: "US", name: "United States", states: US_STATES },
289
+ { code: "UM", name: "United States Minor Outlying Islands" },
290
+ { code: "UY", name: "Uruguay" },
291
+ { code: "UZ", name: "Uzbekistan" },
292
+ { code: "VU", name: "Vanuatu" },
293
+ { code: "VE", name: "Venezuela" },
294
+ { code: "VN", name: "Viet Nam" },
295
+ { code: "VG", name: "Virgin Islands (British)" },
296
+ { code: "VI", name: "Virgin Islands (U.S.)" },
297
+ { code: "WF", name: "Wallis and Futuna" },
298
+ { code: "EH", name: "Western Sahara" },
299
+ { code: "YE", name: "Yemen" },
300
+ { code: "ZM", name: "Zambia" },
301
+ { code: "ZW", name: "Zimbabwe" },
302
+ ];
303
+
304
+ /** Lookup helper: country by ISO code. */
305
+ export function getCountry(code: string): CountryInfo | undefined {
306
+ return COUNTRIES.find((c) => c.code === (code || "").toUpperCase());
307
+ }