@base44/app-plugin-commerce 0.1.5 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/README.md +11 -11
  2. package/base44/agents/commerce/StoreAdmin.jsonc +2 -2
  3. package/base44/entities/commerce.Cart.jsonc +1 -1
  4. package/base44/entities/commerce.Coupon.jsonc +5 -0
  5. package/base44/entities/commerce.Order.jsonc +6 -7
  6. package/base44/entities/commerce.OrderRefund.jsonc +1 -1
  7. package/base44/entities/commerce.PaymentGateway.jsonc +1 -1
  8. package/base44/entities/commerce.Product.jsonc +6 -16
  9. package/base44/entities/{commerce.ProductTag.jsonc → commerce.ProductRibbon.jsonc} +2 -2
  10. package/base44/entities/commerce.ProductVariation.jsonc +1 -9
  11. package/base44/entities/commerce.ShippingTaxLocation.jsonc +85 -0
  12. package/base44/entities/commerce.Webhook.jsonc +1 -1
  13. package/base44/functions/commerce/admin-orders/helpers.ts +7 -13
  14. package/base44/functions/commerce/admin-products/entry.ts +11 -17
  15. package/base44/functions/commerce/admin-refunds/entry.ts +10 -9
  16. package/base44/functions/commerce/admin-reports/entry.ts +3 -3
  17. package/base44/functions/commerce/admin-tools/entry.ts +9 -36
  18. package/base44/functions/commerce/payment-webhook/entry.ts +50 -89
  19. package/base44/functions/commerce/payments/entry.ts +46 -42
  20. package/base44/functions/commerce/seed-store/defaults.ts +35 -42
  21. package/base44/functions/commerce/seed-store/entry.ts +84 -31
  22. package/base44/functions/commerce/seed-store/sample-data.ts +2 -15
  23. package/base44/functions/commerce/seed-store/seed-catalog.ts +105 -55
  24. package/base44/functions/commerce/storefront-cart/cart-pricing.ts +6 -11
  25. package/base44/functions/commerce/storefront-cart/entry.ts +36 -2
  26. package/base44/functions/commerce/storefront-catalog/entry.ts +55 -72
  27. package/base44/functions/commerce/storefront-checkout/cart-pricing.ts +6 -11
  28. package/base44/functions/commerce/storefront-checkout/entry.ts +43 -56
  29. package/base44/shared/commerce/card-payment.ts +80 -0
  30. package/base44/shared/commerce/coupons.ts +16 -10
  31. package/base44/shared/commerce/emails.ts +30 -18
  32. package/base44/shared/commerce/money.ts +11 -24
  33. package/base44/shared/commerce/payments.ts +55 -286
  34. package/base44/shared/commerce/scan.ts +1 -1
  35. package/base44/shared/commerce/sequence.ts +1 -1
  36. package/base44/shared/commerce/settings.ts +4 -9
  37. package/base44/shared/commerce/shipping.ts +65 -133
  38. package/base44/shared/commerce/tax.ts +48 -96
  39. package/base44/shared/commerce/totals.ts +77 -91
  40. package/package.json +1 -1
  41. package/scripts/install.js +28 -7
  42. package/skills/commerce/SKILL.md +14 -14
  43. package/skills/commerce/docs/api-admin.md +23 -26
  44. package/skills/commerce/docs/api-storefront.md +67 -61
  45. package/skills/commerce/installation-guidelines.md +8 -8
  46. package/skills/commerce/post-installation.md +76 -41
  47. package/skills/commerce/references/admin-product-form.md +15 -12
  48. package/skills/commerce/references/emails.md +2 -2
  49. package/skills/commerce/references/guest-access-security.md +2 -2
  50. package/skills/commerce/references/online-payments.md +24 -191
  51. package/skills/commerce/references/product-render.md +18 -18
  52. package/skills/commerce/references/reviews.md +14 -8
  53. package/skills/commerce/references/storefront-product-page.md +1 -1
  54. package/src/commerce/admin/README.md +4 -5
  55. package/src/commerce/admin/bot/Markdown.jsx +1 -1
  56. package/src/commerce/admin/hooks/useMoney.js +13 -22
  57. package/src/commerce/admin/layout/AuthGuard.jsx +1 -1
  58. package/src/commerce/admin/lib/constants.js +2 -29
  59. package/src/commerce/admin/lib/order-utils.js +1 -1
  60. package/src/commerce/admin/pages/coupons/CouponEditor.jsx +131 -140
  61. package/src/commerce/admin/pages/coupons/CouponsList.jsx +14 -7
  62. package/src/commerce/admin/pages/orders/OrderEditor.jsx +3 -3
  63. package/src/commerce/admin/pages/orders/components/PaymentPanel.jsx +17 -28
  64. package/src/commerce/admin/pages/orders/components/RefundPanel.jsx +5 -11
  65. package/src/commerce/admin/pages/products/Categories.jsx +147 -177
  66. package/src/commerce/admin/pages/products/ProductEditor.jsx +23 -18
  67. package/src/commerce/admin/pages/products/Reviews.jsx +37 -1
  68. package/src/commerce/admin/pages/products/components/ProductDataPanel.jsx +33 -47
  69. package/src/commerce/admin/pages/products/components/PublishBox.jsx +13 -34
  70. package/src/commerce/admin/pages/products/components/TaxonomyPanel.jsx +29 -29
  71. package/src/commerce/admin/pages/products/components/tabs/PriceInventoryTab.jsx +14 -44
  72. package/src/commerce/admin/pages/reports/Reports.jsx +2 -2
  73. package/src/commerce/admin/pages/settings/EmailsSettings.jsx +101 -68
  74. package/src/commerce/admin/pages/settings/GeneralSettings.jsx +40 -38
  75. package/src/commerce/admin/pages/settings/InventorySettings.jsx +1 -41
  76. package/src/commerce/admin/pages/settings/LocationEditor.jsx +377 -0
  77. package/src/commerce/admin/pages/settings/PaymentsSettings.jsx +137 -119
  78. package/src/commerce/admin/pages/settings/SettingsLayout.jsx +2 -4
  79. package/src/commerce/admin/pages/settings/ShippingTaxSettings.jsx +191 -0
  80. package/src/commerce/admin/routes.jsx +6 -12
  81. package/src/commerce/utils/index.js +2 -2
  82. package/src/commerce/utils/shipping-promos.js +45 -49
  83. package/src/commerce/utils/variants.js +1 -1
  84. package/base44/entities/commerce.ShippingClass.jsonc +0 -30
  85. package/base44/entities/commerce.ShippingZone.jsonc +0 -41
  86. package/base44/entities/commerce.ShippingZoneMethod.jsonc +0 -84
  87. package/base44/entities/commerce.TaxClass.jsonc +0 -23
  88. package/base44/entities/commerce.TaxRate.jsonc +0 -68
  89. package/base44/shared/commerce/stripe.ts +0 -463
  90. package/src/commerce/admin/hooks/usePaymentProvider.js +0 -27
  91. package/src/commerce/admin/pages/settings/ProductsSettings.jsx +0 -118
  92. package/src/commerce/admin/pages/settings/ShippingSettings.jsx +0 -304
  93. package/src/commerce/admin/pages/settings/ShippingZoneEditor.jsx +0 -514
  94. package/src/commerce/admin/pages/settings/TaxRatesTable.jsx +0 -231
  95. package/src/commerce/admin/pages/settings/TaxSettings.jsx +0 -280
@@ -3,12 +3,14 @@
3
3
  * Used by: commerce/storefront-cart `totals` (preview), commerce/storefront-checkout
4
4
  * `place-order` (authoritative), commerce/admin-orders `create`/`recalculate`.
5
5
  *
6
- * Pipeline (standard commerce semantics): resolve lines → coupons → fees → shipping → tax → totals.
6
+ * Pipeline: resolve lines → coupons → fees → shipping → tax → totals.
7
7
  * All internal math is ex-tax; tax-inclusive catalog prices are netted first.
8
+ * Shipping options and tax both come from the ONE matched
9
+ * commerce.ShippingTaxLocation (see shared/commerce/shipping.ts + tax.ts).
8
10
  */
9
11
  import { round2, distributeProportionally } from "./money.ts";
10
- import { applyRates, extractInclusiveTax, matchTaxRates, sumTax } from "./tax.ts";
11
- import { availableMethods, matchZone } from "./shipping.ts";
12
+ import { applyRates, DEFAULT_TAX_GROUP, extractInclusiveTax, shippingTaxFor, sumTax, taxRatesFor } from "./tax.ts";
13
+ import { availableRates, matchLocation } from "./shipping.ts";
12
14
  import { applyCoupons } from "./coupons.ts";
13
15
  import { getSetting } from "./settings.ts";
14
16
  import { uuid } from "./sequence.ts";
@@ -24,7 +26,7 @@ export interface TotalsItem {
24
26
  export interface TotalsFee {
25
27
  name: string;
26
28
  amount: number;
27
- tax_class?: string;
29
+ tax_group?: string;
28
30
  tax_status?: string; // taxable | none
29
31
  line_id?: string;
30
32
  }
@@ -35,11 +37,9 @@ export interface TotalsInput {
35
37
  fees?: TotalsFee[];
36
38
  billing?: any;
37
39
  shipping_address?: any;
38
- chosenShippingMethodId?: string; // ShippingZoneMethod id
40
+ chosenShippingMethodId?: string; // the chosen shipping rate id
39
41
  settings: Record<string, any>; // groups object from getSettings()
40
- taxRates?: any[];
41
- zones?: any[];
42
- zoneMethods?: any[];
42
+ locations?: any[]; // commerce.ShippingTaxLocation records
43
43
  }
44
44
 
45
45
  export interface TotalsResult {
@@ -57,17 +57,16 @@ export interface TotalsResult {
57
57
  total_tax: number;
58
58
  total: number;
59
59
  prices_include_tax: boolean;
60
- available_shipping_methods: Array<{ id: string; method_id: string; title: string; cost: number }>;
61
- matched_zone_id: string | null;
60
+ available_shipping_methods: Array<{ id: string; title: string; cost: number }>;
61
+ matched_location_id: string | null;
62
62
  }
63
63
 
64
- /** Effective tax class/status with variation "parent" inheritance. */
65
- function effectiveTax(product: any, variation?: any): { tax_class: string; tax_status: string } {
64
+ /** Effective tax group/status with variation "parent" status inheritance. */
65
+ function effectiveTax(product: any, variation?: any): { tax_group: string; tax_status: string } {
66
66
  const status = variation?.tax_status && variation.tax_status !== "parent"
67
67
  ? variation.tax_status
68
68
  : product.tax_status ?? "taxable";
69
- const cls = (variation?.tax_class || product.tax_class) ?? "standard";
70
- return { tax_class: cls, tax_status: status };
69
+ return { tax_group: product.tax_group || DEFAULT_TAX_GROUP, tax_status: status };
71
70
  }
72
71
 
73
72
  function resolveTaxAddress(input: TotalsInput): any {
@@ -76,18 +75,28 @@ function resolveTaxAddress(input: TotalsInput): any {
76
75
  return input.shipping_address ?? input.billing ?? {}; // "shipping" default
77
76
  }
78
77
 
78
+ /**
79
+ * Resolve the location for an address; with no address, a store with exactly
80
+ * ONE location still resolves to it — its options and taxes are the same for
81
+ * every address, so a cart can price before the customer types anything.
82
+ */
83
+ function locationFor(locations: any[], addr: any): any | null {
84
+ if (addr?.country) return matchLocation(locations, addr);
85
+ return locations.length === 1 ? locations[0] : null;
86
+ }
87
+
79
88
  /** Run the full totals pipeline. Pure aside from reading its inputs. */
80
89
  export function calculateTotals(input: TotalsInput): TotalsResult {
81
90
  const settings = input.settings ?? {};
82
91
  const pricesIncludeTax = !!getSetting(settings, "tax", "prices_include_tax", false);
83
- const taxRates = input.taxRates ?? [];
84
- const taxAddress = resolveTaxAddress(input);
92
+ const locations = input.locations ?? [];
93
+ const taxLocation = locationFor(locations, resolveTaxAddress(input));
85
94
 
86
95
  // ── 1. resolve lines (ex-tax) ────────────────────────────────────────────
87
96
  const lines = (input.items || []).map((it) => {
88
97
  const src = it.variation ?? it.product;
89
- const { tax_class, tax_status } = effectiveTax(it.product, it.variation);
90
- const rates = tax_status === "taxable" ? matchTaxRates(taxRates, taxAddress, tax_class) : [];
98
+ const { tax_group, tax_status } = effectiveTax(it.product, it.variation);
99
+ const rates = tax_status === "taxable" ? taxRatesFor(taxLocation, tax_group) : [];
91
100
  let unitPrice = Number(src.price ?? it.product?.price ?? 0);
92
101
  let subtotal = round2(unitPrice * it.quantity);
93
102
  if (pricesIncludeTax && rates.length) {
@@ -103,7 +112,7 @@ export function calculateTotals(input: TotalsInput): TotalsResult {
103
112
  sku: (it.variation?.sku || it.product?.sku) ?? "",
104
113
  quantity: it.quantity,
105
114
  unit_price: unitPrice,
106
- tax_class,
115
+ tax_group,
107
116
  tax_status,
108
117
  rates,
109
118
  subtotal,
@@ -114,7 +123,6 @@ export function calculateTotals(input: TotalsInput): TotalsResult {
114
123
  taxes: [] as any[],
115
124
  attributes: it.attributes ?? [],
116
125
  meta_data: it.meta_data ?? [],
117
- shipping_class_id: (it.variation?.shipping_class_id || it.product?.shipping_class_id) ?? "",
118
126
  virtual: !!(it.variation?.virtual ?? it.product?.virtual),
119
127
  product: it.product,
120
128
  variation: it.variation,
@@ -134,7 +142,7 @@ export function calculateTotals(input: TotalsInput): TotalsResult {
134
142
  const feeLines = (input.fees || []).map((f) => ({
135
143
  line_id: f.line_id ?? uuid(),
136
144
  name: f.name,
137
- tax_class: f.tax_class ?? "standard",
145
+ tax_group: f.tax_group ?? DEFAULT_TAX_GROUP,
138
146
  tax_status: f.tax_status ?? "taxable",
139
147
  total: round2(Number(f.amount) || 0),
140
148
  total_tax: 0,
@@ -145,44 +153,41 @@ export function calculateTotals(input: TotalsInput): TotalsResult {
145
153
  const shippingEnabled = getSetting(settings, "shipping", "enable_shipping", true) !== false;
146
154
  const needsShipping = lines.some((l) => !l.virtual);
147
155
  const shippingLines: any[] = [];
148
- let available: Array<{ id: string; method_id: string; title: string; cost: number }> = [];
149
- let matchedZoneId: string | null = null;
156
+ let available: Array<{ id: string; title: string; cost: number }> = [];
157
+ const shipLocation = shippingEnabled && needsShipping
158
+ ? locationFor(locations, input.shipping_address)
159
+ : null;
150
160
 
151
- if (shippingEnabled && needsShipping && input.shipping_address?.country) {
152
- const zone = matchZone(input.zones ?? [], input.shipping_address);
153
- if (zone) {
154
- matchedZoneId = zone.id ?? null;
155
- const cart = { lines, itemsSubtotal, itemsSubtotalAfterDiscount: itemsAfterDiscount };
156
- const offers = availableMethods(zone, input.zoneMethods ?? [], cart, settings, coupons);
157
- available = offers.map((o) => ({
158
- id: o.method.id,
159
- method_id: o.method.method_id,
160
- title: o.method.title || defaultMethodTitle(o.method.method_id),
161
- cost: o.cost,
162
- }));
163
- if (input.chosenShippingMethodId) {
164
- const chosen = offers.find((o) => o.method.id === input.chosenShippingMethodId);
165
- if (chosen) {
166
- shippingLines.push({
167
- line_id: uuid(),
168
- method_id: chosen.method.method_id,
169
- instance_id: chosen.method.id,
170
- method_title: chosen.method.title || defaultMethodTitle(chosen.method.method_id),
171
- total: chosen.cost,
172
- total_tax: 0,
173
- taxes: [] as any[],
174
- });
175
- }
161
+ if (shipLocation) {
162
+ const cart = { itemsSubtotal, itemsSubtotalAfterDiscount: itemsAfterDiscount };
163
+ const offers = availableRates(shipLocation, cart, coupons);
164
+ available = offers.map((o) => ({
165
+ id: o.rate.id,
166
+ title: o.rate.name || "Shipping",
167
+ cost: o.cost,
168
+ }));
169
+ if (input.chosenShippingMethodId) {
170
+ const chosen = offers.find((o) => o.rate.id === input.chosenShippingMethodId);
171
+ if (chosen) {
172
+ shippingLines.push({
173
+ line_id: uuid(),
174
+ method_id: "rate",
175
+ instance_id: chosen.rate.id,
176
+ method_title: chosen.rate.name || "Shipping",
177
+ total: chosen.cost,
178
+ total_tax: 0,
179
+ taxes: [] as any[],
180
+ });
176
181
  }
177
182
  }
178
183
  }
179
184
 
180
185
  // ── 5. tax ───────────────────────────────────────────────────────────────
181
- // aggregated per rate for order-level tax_lines
182
- const agg = new Map<string, { rate: any; tax_total: number; shipping_tax_total: number }>();
183
- const bump = (rate: any, tax: number, shippingTax: number) => {
184
- const key = rate.id ?? `${rate.name}-${rate.priority}`;
185
- const cur = agg.get(key) ?? { rate, tax_total: 0, shipping_tax_total: 0 };
186
+ // aggregated per rate label for order-level tax_lines
187
+ const agg = new Map<string, { label: string; rate_percent: number; tax_total: number; shipping_tax_total: number }>();
188
+ const bump = (label: string, ratePercent: number, tax: number, shippingTax: number) => {
189
+ const key = `${label}|${ratePercent}`;
190
+ const cur = agg.get(key) ?? { label, rate_percent: ratePercent, tax_total: 0, shipping_tax_total: 0 };
186
191
  cur.tax_total = round2(cur.tax_total + tax);
187
192
  cur.shipping_tax_total = round2(cur.shipping_tax_total + shippingTax);
188
193
  agg.set(key, cur);
@@ -195,32 +200,28 @@ export function calculateTotals(input: TotalsInput): TotalsResult {
195
200
  l.subtotal_tax = sumTax(subApplied);
196
201
  l.total_tax = sumTax(totApplied);
197
202
  l.taxes = totApplied.map((t, i) => ({
198
- rate_id: t.rate.id ?? "",
203
+ label: t.rate.name || "Tax",
199
204
  total: t.amount,
200
205
  subtotal: subApplied[i]?.amount ?? 0,
201
206
  }));
202
- for (const t of totApplied) bump(t.rate, t.amount, 0);
207
+ for (const t of totApplied) bump(t.rate.name || "Tax", t.rate.rate, t.amount, 0);
203
208
  }
204
209
  for (const fee of feeLines) {
205
210
  if (fee.tax_status !== "taxable") continue;
206
- const rates = matchTaxRates(taxRates, taxAddress, fee.tax_class);
211
+ const rates = taxRatesFor(taxLocation, fee.tax_group);
207
212
  const applied = applyRates(fee.total, rates);
208
213
  fee.total_tax = sumTax(applied);
209
- fee.taxes = applied.map((t) => ({ rate_id: t.rate.id ?? "", total: t.amount }));
210
- for (const t of applied) bump(t.rate, t.amount, 0);
214
+ fee.taxes = applied.map((t) => ({ label: t.rate.name || "Tax", total: t.amount }));
215
+ for (const t of applied) bump(t.rate.name || "Tax", t.rate.rate, t.amount, 0);
211
216
  }
212
- if (shippingLines.length) {
213
- // shipping_tax_class "inherit" = first taxable line's class (intended behavior)
214
- const cfg = getSetting(settings, "tax", "shipping_tax_class", "inherit");
215
- const cls = cfg === "inherit"
216
- ? (lines.find((l) => l.tax_status === "taxable")?.tax_class ?? "standard")
217
- : cfg;
218
- const rates = matchTaxRates(taxRates, taxAddress, cls).filter((r) => r.shipping !== false);
217
+ if (shippingLines.length && taxLocation) {
219
218
  for (const sl of shippingLines) {
220
- const applied = applyRates(sl.total, rates);
221
- sl.total_tax = sumTax(applied);
222
- sl.taxes = applied.map((t) => ({ rate_id: t.rate.id ?? "", total: t.amount }));
223
- for (const t of applied) bump(t.rate, 0, t.amount);
219
+ const tax = shippingTaxFor(taxLocation, sl.total);
220
+ if (tax <= 0) continue;
221
+ sl.total_tax = tax;
222
+ sl.taxes = [{ label: "Shipping tax", total: tax }];
223
+ const pct = taxLocation.shipping_tax?.type === "fixed" ? 0 : Number(taxLocation.shipping_tax?.value) || 0;
224
+ bump("Shipping tax", pct, 0, tax);
224
225
  }
225
226
  }
226
227
 
@@ -243,12 +244,11 @@ export function calculateTotals(input: TotalsInput): TotalsResult {
243
244
 
244
245
  const total = round2(itemsSubtotal - discountTotal + feesTotal + shippingTotal + totalTax);
245
246
 
246
- const taxLines = [...agg.values()].map(({ rate, tax_total, shipping_tax_total }) => ({
247
- rate_id: rate.id ?? "",
248
- rate_code: buildRateCode(rate),
249
- label: rate.name ?? "Tax",
250
- rate_percent: Number(rate.rate) || 0,
251
- compound: !!rate.compound,
247
+ const taxLines = [...agg.values()].map(({ label, rate_percent, tax_total, shipping_tax_total }) => ({
248
+ rate_id: "",
249
+ rate_code: label.replace(/\s+/g, "-").toUpperCase(),
250
+ label,
251
+ rate_percent,
252
252
  tax_total,
253
253
  shipping_tax_total,
254
254
  }));
@@ -269,7 +269,7 @@ export function calculateTotals(input: TotalsInput): TotalsResult {
269
269
  total,
270
270
  prices_include_tax: pricesIncludeTax,
271
271
  available_shipping_methods: available,
272
- matched_zone_id: matchedZoneId,
272
+ matched_location_id: shipLocation?.id ?? taxLocation?.id ?? null,
273
273
  };
274
274
  }
275
275
 
@@ -283,7 +283,7 @@ function stripLine(l: any): any {
283
283
  sku: l.sku,
284
284
  quantity: l.quantity,
285
285
  price: l.quantity ? round2(l.total / l.quantity) : 0, // post-discount ex-tax unit price
286
- tax_class: l.tax_class,
286
+ tax_group: l.tax_group,
287
287
  subtotal: l.subtotal,
288
288
  subtotal_tax: l.subtotal_tax,
289
289
  total: l.total,
@@ -293,17 +293,3 @@ function stripLine(l: any): any {
293
293
  meta_data: l.meta_data,
294
294
  };
295
295
  }
296
-
297
- function buildRateCode(rate: any): string {
298
- const parts = [rate.country || "ANY", rate.state || "", (rate.name || "TAX").replace(/\s+/g, "-"), String(rate.priority ?? 1)];
299
- return parts.filter(Boolean).join("-").toUpperCase();
300
- }
301
-
302
- function defaultMethodTitle(methodId: string): string {
303
- switch (methodId) {
304
- case "flat_rate": return "Flat rate";
305
- case "free_shipping": return "Free shipping";
306
- case "local_pickup": return "Local pickup";
307
- default: return methodId;
308
- }
309
- }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@base44/app-plugin-commerce",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "Base44 Commerce plugin — entities, backend functions, shared commerce engine, admin UI and the commerce skill, shipped as copyable source",
5
5
  "keywords": [
6
6
  "base44",
@@ -18,7 +18,7 @@
18
18
  * ../base44/agents/commerce/ → ../../../base44/agents/commerce/
19
19
  * ../src/commerce/admin/ → ../../../src/commerce/admin/
20
20
  * ../src/commerce/utils/ → ../../../src/commerce/utils/
21
- * ../skills/commerce/ → ../../../skills/commerce/
21
+ * ../skills/commerce/ → ../../../.agents/skills/commerce/
22
22
  *
23
23
  * Merge semantics: directories are merged — files owned by the template are
24
24
  * overwritten (so re-running after a template update is safe), everything
@@ -89,12 +89,19 @@
89
89
  // ── retired files ─────────────────────────────────────────────────────────
90
90
  // Copying merges, so a template file that gets renamed would otherwise linger
91
91
  // in the app forever — and stale guidance is worse than none, since agents
92
- // read whatever is in skills/. Anything this template used to own and no
93
- // longer ships is listed here and removed on install.
92
+ // read whatever is in .agents/skills/. Anything this template used to own and
93
+ // no longer ships is listed here and removed on install.
94
94
  const RETIRED = [
95
- ["skills", "commerce", "references", "stripe-payments.md"], // → references/online-payments.md
95
+ // The commerce skill moved from skills/commerce/ to .agents/skills/commerce/;
96
+ // remove the old copy so agents don't keep reading the stale one.
97
+ ["skills", "commerce"],
98
+ [".agents", "skills", "commerce", "references", "stripe-payments.md"], // → references/online-payments.md
99
+ // Payments moved to the two-file model (card-payment.ts + the webhook);
100
+ // the Stripe adapter and the provider-status hook are gone.
101
+ ["base44", "shared", "commerce", "stripe.ts"],
102
+ ["src", "commerce", "admin", "hooks", "usePaymentProvider.js"],
96
103
  // Attributes and their values are edited in the Attributes section of the
97
- // Price & Inventory tab, tags in the product sidebar; the product type and the
104
+ // Price & Inventory section, ribbons in the product sidebar; the product type and the
98
105
  // Advanced tab are gone entirely.
99
106
  ["src", "commerce", "admin", "pages", "products", "Attributes.jsx"],
100
107
  ["src", "commerce", "admin", "pages", "products", "AttributeTerms.jsx"],
@@ -109,6 +116,20 @@
109
116
  // are per variant, so the product-level Shipping tab has nothing left.
110
117
  ["src", "commerce", "admin", "pages", "products", "components", "tabs", "AttributesTab.jsx"],
111
118
  ["src", "commerce", "admin", "pages", "products", "components", "tabs", "ShippingTab.jsx"],
119
+ // The zone/class tax-and-shipping model became one Shipping & Tax Location
120
+ // (commerce.ShippingTaxLocation + the settings/shipping-tax screen), and
121
+ // ProductTag became commerce.ProductRibbon.
122
+ ["base44", "entities", "commerce.TaxClass.jsonc"],
123
+ ["base44", "entities", "commerce.TaxRate.jsonc"],
124
+ ["base44", "entities", "commerce.ShippingClass.jsonc"],
125
+ ["base44", "entities", "commerce.ShippingZone.jsonc"],
126
+ ["base44", "entities", "commerce.ShippingZoneMethod.jsonc"],
127
+ ["base44", "entities", "commerce.ProductTag.jsonc"],
128
+ ["src", "commerce", "admin", "pages", "settings", "ProductsSettings.jsx"],
129
+ ["src", "commerce", "admin", "pages", "settings", "TaxSettings.jsx"],
130
+ ["src", "commerce", "admin", "pages", "settings", "TaxRatesTable.jsx"],
131
+ ["src", "commerce", "admin", "pages", "settings", "ShippingSettings.jsx"],
132
+ ["src", "commerce", "admin", "pages", "settings", "ShippingZoneEditor.jsx"],
112
133
  ];
113
134
  let retiredRemoved = 0;
114
135
  for (const parts of RETIRED) {
@@ -144,7 +165,7 @@
144
165
  { label: "utils", from: ["src", "commerce", "utils"], to: ["src", "commerce", "utils"] },
145
166
  // Commerce skill — SKILL.md, install/post-install guides, references/
146
167
  // and docs/, the guidance agents read before working on the store.
147
- { label: "skills", from: ["skills", "commerce"], to: ["skills", "commerce"] },
168
+ { label: "skills", from: ["skills", "commerce"], to: [".agents", "skills", "commerce"] },
148
169
  ];
149
170
  for (const job of dirJobs) {
150
171
  const src = path.join(templateRoot, ...job.from);
@@ -159,7 +180,7 @@
159
180
  (retiredRemoved ? ` (${retiredRemoved} retired file${retiredRemoved === 1 ? "" : "s"} removed)` : ""),
160
181
  );
161
182
  console.log(
162
- "\nNext steps (see skills/commerce/installation-guidelines.md + post-installation.md):\n" +
183
+ "\nNext steps (see .agents/skills/commerce/installation-guidelines.md + post-installation.md):\n" +
163
184
  " 1. No deps to add: sonner, recharts and react-markdown ship with the default\n" +
164
185
  " Base44 template — check package.json and npm i only what is truly missing;\n" +
165
186
  " do not re-install packages already listed as dependencies\n" +
@@ -1,24 +1,24 @@
1
1
  ---
2
2
  name: commerce
3
- description: Base44 Commerce template — 24 commerce.* entities, 16 commerce/* backend functions (storefront + admin APIs + online payments), the shared commerce engine under base44/shared/commerce/, the Store Management UI mounted at /store-admin, and the commerce/StoreAdmin agent (admin copilot). Read before working on store features — the admin UI, storefront building, Stripe wiring, scheduled maintenance, emails, webhooks, downloads, or scaling limits.
3
+ description: Base44 Commerce template — 20 commerce.* entities, 16 commerce/* backend functions (storefront + admin APIs + online payments), the shared commerce engine under base44/shared/commerce/, the Store Management UI mounted at /store-admin, and the commerce/StoreAdmin agent (admin copilot). Read before working on store features — the admin UI, storefront building, payment provider wiring, scheduled maintenance, emails, webhooks, downloads, or scaling limits.
4
4
  ---
5
5
 
6
6
  # Commerce
7
7
 
8
- Operational guidance for extending, operating and building on the Base44 Commerce Template. This file is the map: it stays short, and each topic links to a focused reference under [`skills/commerce/references/`](./references/) — open one only when your task touches that area. The API references live in [`skills/commerce/docs/`](./docs/).
8
+ Operational guidance for extending, operating and building on the Base44 Commerce Template. This file is the map: it stays short, and each topic links to a focused reference under [`.agents/skills/commerce/references/`](./references/) — open one only when your task touches that area. The API references live in [`.agents/skills/commerce/docs/`](./docs/).
9
9
 
10
10
  > **If you are a Base44 agent working inside the runtime, read this first:**
11
11
  > - **Don't read the whole codebase up front.** Start with this file, then open only the files your current task touches plus the matching reference below, pulling in more as you go. Reading everything first just burns context.
12
- > - **Don't weaken the admin gating.** The UI guard, entity RLS and function guards form three enforcement layers — see [`skills/commerce/post-installation.md`](./post-installation.md) — keep all of them intact when changing routes or schemas.
12
+ > - **Don't weaken the admin gating.** The UI guard, entity RLS and function guards form three enforcement layers — see [`.agents/skills/commerce/post-installation.md`](./post-installation.md) — keep all of them intact when changing routes or schemas.
13
13
 
14
14
  ## IMPORTANT — first-time installation
15
15
 
16
- If the template was just installed (or you are installing it right now), read [`skills/commerce/post-installation.md`](./post-installation.md) **before anything else**: embedding the admin pages, the three-layer admin-role enforcement (do not weaken), seeding the store's data — **one `commerce/seed-store` call takes the whole catalog** (products with attributes; variants, categories and taxonomy created internally — §2) — and the **storefront quick start** (§3): logic-only chunks for product list → product page → cart → checkout that make reading the full API docs unnecessary for the happy path. The full install-from-scratch steps are in [`skills/commerce/installation-guidelines.md`](./installation-guidelines.md).
16
+ If the template was just installed (or you are installing it right now), read [`.agents/skills/commerce/post-installation.md`](./post-installation.md) **before anything else**: embedding the admin pages, the three-layer admin-role enforcement (do not weaken), seeding the store's data — **one `commerce/seed-store` call takes the whole catalog** (products with attributes; variants, categories, ribbons and Shipping & Tax Locations created internally — §2) — and the **storefront quick start** (§3): logic-only chunks for product list → product page → cart → checkout that make reading the full API docs unnecessary for the happy path. The full install-from-scratch steps are in [`.agents/skills/commerce/installation-guidelines.md`](./installation-guidelines.md).
17
17
 
18
18
  ## Working on the UI
19
19
 
20
- - **Admin UI** (`src/commerce/admin/`) — a complete store back office ships with the template, and it is **yours to change**: restyle it, add or remove pages, rework flows, extend it however the app needs. To understand the backend it talks to, read [`skills/commerce/docs/api-admin.md`](./docs/api-admin.md) — every admin function/action plus the direct-entity-CRUD contract. The only invariant is the admin-role gating (see above).
21
- - **Storefront** — **no visitor UI ships**; the storefront *API* is complete (token-based cart), plus framework-free helpers in [`src/commerce/utils/`](../../src/commerce/utils/) (`variants.js`, `shipping-promos.js` — import from `@/commerce/utils`). Start from the logic-only quick start in [`post-installation.md` §3](./post-installation.md#3-storefront-quick-start--logic-only) — it covers the whole buy path; go to [`skills/commerce/docs/api-storefront.md`](./docs/api-storefront.md) for anything beyond it. Navigation comes from three actions — `list-categories` (tree), `list-tags` (flat, with counts) and `list-attributes` (filter UIs). What to render in the grid vs. the product page — and which fields only one of the two calls returns — is [`references/product-render.md`](./references/product-render.md); **tags are the most-skipped part of it and belong in both views**.
20
+ - **Admin UI** (`src/commerce/admin/`) — a complete store back office ships with the template, and it is **yours to change**: restyle it, add or remove pages, rework flows, extend it however the app needs. To understand the backend it talks to, read [`.agents/skills/commerce/docs/api-admin.md`](./docs/api-admin.md) — every admin function/action plus the direct-entity-CRUD contract. The only invariant is the admin-role gating (see above).
21
+ - **Storefront** — **no visitor UI ships**; the storefront *API* is complete (token-based cart), plus framework-free helpers in [`src/commerce/utils/`](../../src/commerce/utils/) (`variants.js`, `shipping-promos.js` — import from `@/commerce/utils`). Start from the logic-only quick start in [`post-installation.md` §3](./post-installation.md#3-storefront-quick-start--logic-only) — it covers the whole buy path; go to [`.agents/skills/commerce/docs/api-storefront.md`](./docs/api-storefront.md) for anything beyond it. Navigation comes from three actions — `list-categories` (tree), `list-ribbons` (flat, with counts) and `list-attributes` (filter UIs). What to render in the grid vs. the product page — and which fields only one of the two calls returns — is [`references/product-render.md`](./references/product-render.md); **ribbons are the most-skipped part of it and belong in both views**.
22
22
 
23
23
  ### If you build a storefront, these four are not optional
24
24
 
@@ -32,11 +32,11 @@ Agents keep shipping storefronts that miss these, and each one breaks buying out
32
32
  ```
33
33
  `add-item` **rejects a product with attributes unless it gets a `variation_id`** (`400 variation_required`), so a page that ignores this cannot sell anything.
34
34
 
35
- 2. **Checkout must present shipping options and send a choice.** After `set-shipping-address`, read `shipping_status` on the cart: `auto_selected` (one option, already applied) · `chosen` · `choice_required` → **you must show `available_shipping_methods` and call `choose-shipping-method`** · `none_available` → say so. `place-order` refuses with `400 shipping_method_required` until then — that is not a bug to work around.
35
+ 2. **Checkout must present shipping options and send a choice.** Call `set-shipping-address` **as soon as the customer provides an address** — it recalculates the options and cost, and fails with `400 shipping_not_available` when the store doesn't ship there. Then read `shipping_status` on the returned cart: `auto_selected` (one option, already applied) · `chosen` · `choice_required` → **you must show `available_shipping_methods` and call `choose-shipping-method`** · `missing_address` → collect the address first (a single-location store shows its options even before one). `place-order` refuses with `400 shipping_method_required` until then — that is not a bug to work around. (`chosen_shipping_method` on the cart is the rate **id**, a string — display the choice by looking it up in `available_shipping_methods` for its `title`/`cost`, never by rendering the id or dotting into it.)
36
36
 
37
- 3. **Take card payments, and build `/order-received`.** (Ask the user to connect the provider *after* the store is set up — it's their step in the dashboard, and a catalog plus a shipping method is what makes a test payment provable.) Choosing the online gateway returns `payment.checkout_url` — redirect there. Every payment link comes back to `/order-received`, which **you must implement**: call `commerce/payments` `complete-return` with the query params and render its `state`. Without that page a customer pays into a 404 and the order is never marked paid.
37
+ 3. **Handle card payments, and build `/order-received`.** Choosing the card gateway returns `payment.checkout_url` — redirect there (until a payment provider is implemented it answers `503 no_card_payment_provider`; offer the other methods). Every payment link comes back to `/order-received`, which **you must implement**: call `commerce/payments` `complete-return` with the query params and render its `state`. Without that page a customer pays into a 404 and the order is never marked paid. Also persist the `cart_token` from **every** cart response — `add-item` silently starts a fresh cart when the stored token is stale.
38
38
 
39
- 4. **Never advertise what isn't configured.** "Free shipping over €150" must come from a real `free_shipping` zone method. Zones are admin-only data, so a storefront cannot read them: the live answer is the cart's `available_shipping_methods` after `set-shipping-address`, and `shipping-promos.js` normalizes the rules wherever the records *are* in hand. No rule means no banner.
39
+ 4. **Never advertise what isn't configured.** "Free shipping over €150" must come from a real shipping rate that is free or carries a `free_over` threshold on a Shipping & Tax Location. Locations are admin-only data, so a storefront cannot read them: the live answer is the cart's `available_shipping_methods` after `set-shipping-address`, and `shipping-promos.js` normalizes the rules wherever the records *are* in hand. No rule means no banner.
40
40
 
41
41
  All functions return the envelope `{ success, data }` (or `{ success, error, code }`); with the SDK the body is on `res.data`:
42
42
 
@@ -47,15 +47,15 @@ const { products, has_next } = res.data.data; // res.data = envelope, .data =
47
47
 
48
48
  ## Topic references
49
49
 
50
- Open the matching file under `skills/commerce/references/` only when a task touches its area:
50
+ Open the matching file under `.agents/skills/commerce/references/` only when a task touches its area:
51
51
 
52
52
  | Topic | Read when the task involves | Reference |
53
53
  |---|---|---|
54
- | Product rendering (list + page) | what to show in a product grid vs. the product page, field availability across `list-products`/`get-product`, **tags in both views**, variant pricing on cards, adding a page-only field to the listing call | [`references/product-render.md`](./references/product-render.md) |
54
+ | Product rendering (list + page) | what to show in a product grid vs. the product page, field availability across `list-products`/`get-product`, **ribbons in both views**, variant pricing on cards, adding a page-only field to the listing call | [`references/product-render.md`](./references/product-render.md) |
55
55
  | Variant selection | attribute-level selectors, resolving a selection to a variation, availability states, incomplete-selection pricing, add-to-cart contract | [`references/storefront-product-page.md`](./references/storefront-product-page.md) |
56
- | Reviews | stars on cards and the product page, the review list + submit form (backend ships complete), moderation statuses, verified-only and rating-required behaviors | [`references/reviews.md`](./references/reviews.md) |
57
- | Admin product form | changing the product editor — its tabs are **Price & Inventory** (tax, then the attributes, then a row per variant, or a single *Base price* row when there are none), **Modifiers** (`meta_data`), **Downloads**, **Linked products**. Variants reconcile from the attribute values automatically: no generate step, no per-variant delete. Weight, dimensions and shipping class are per variant. | [`references/admin-product-form.md`](./references/admin-product-form.md) |
58
- | Online payments | **any storefront or checkout work** — card payments ship implemented (hosted page, payment links, signed webhook, refunds) behind a provider-neutral utility wired to Stripe; connect a provider to go live, or implement one adapter to use another | [`references/online-payments.md`](./references/online-payments.md) |
56
+ | Reviews | stars on cards and the product page, the review list + submit form (public by email, backend ships complete), the auto-approve toggle, UI-enforced policies (login-gated, verified-only, required rating) | [`references/reviews.md`](./references/reviews.md) |
57
+ | Admin product form | changing the product editor — its stacked sections are **Price & Inventory** (tax group, then the attributes, then a row per variant, or a single *Base price* row when there are none), **Modifiers** (`meta_data`), **Downloads**, **Linked products**; one **Visible** toggle drives `status`. Variants reconcile from the attribute values automatically: no generate step, no per-variant delete. Weight and dimensions are per variant. | [`references/admin-product-form.md`](./references/admin-product-form.md) |
58
+ | Online payments | **any storefront or checkout work** — the order side (checkout, confirmation, payment links, refund records) is premade; wiring a provider means implementing exactly two files (`shared/commerce/card-payment.ts` + the payment webhook) | [`references/online-payments.md`](./references/online-payments.md) |
59
59
  | Scheduled work | recurring maintenance — stock-hold release, abandoned-cart cleanup, webhook-log pruning, counter-drift repair | [`references/scheduled-work.md`](./references/scheduled-work.md) |
60
60
  | Emails | transactional order emails, per-type overrides, deliverability, the email log | [`references/emails.md`](./references/emails.md) |
61
61
  | Webhooks | outbound webhooks, HMAC signing, delivery log, auto-disable behavior | [`references/webhooks.md`](./references/webhooks.md) |