create-nextblock 0.16.3 → 0.17.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 (201) hide show
  1. package/CLAUDE.md +22 -0
  2. package/docker-template/.env.docker.example +69 -69
  3. package/docker-template/docker/db/init/99-jwt.sql +6 -6
  4. package/docker-template/docker/db/init/99-roles.sql +25 -25
  5. package/docker-template/docker/kong/kong.yml +112 -112
  6. package/docker-template/scripts/docker-setup.mjs +310 -310
  7. package/package.json +1 -1
  8. package/templates/nextblock-template/.browserslistrc +11 -11
  9. package/templates/nextblock-template/.swcrc +30 -30
  10. package/templates/nextblock-template/CLAUDE.md +26 -0
  11. package/templates/nextblock-template/app/(auth-pages)/sign-up/page.tsx +46 -46
  12. package/templates/nextblock-template/app/(auth-pages)/two-factor/page.tsx +51 -51
  13. package/templates/nextblock-template/app/.well-known/ucp/route.ts +16 -16
  14. package/templates/nextblock-template/app/actions/consent.ts +57 -57
  15. package/templates/nextblock-template/app/actions/twoFactorEmail.ts +22 -22
  16. package/templates/nextblock-template/app/api/ai/cortex/build-widget/route.ts +153 -153
  17. package/templates/nextblock-template/app/api/ai/generate-blocks/route.ts +96 -96
  18. package/templates/nextblock-template/app/api/brand/email-logo/route.ts +48 -48
  19. package/templates/nextblock-template/app/api/checkout/freemius/sync/route.ts +29 -29
  20. package/templates/nextblock-template/app/api/cms/check-updates/route.ts +44 -44
  21. package/templates/nextblock-template/app/api/cms/full-backup/export/route.ts +33 -33
  22. package/templates/nextblock-template/app/api/cms/full-backup/restore/route.ts +63 -63
  23. package/templates/nextblock-template/app/api/cron/reset-sandbox/route.ts +270 -205
  24. package/templates/nextblock-template/app/api/cron/reset-sandbox/sandboxResetSql.ts +10388 -9315
  25. package/templates/nextblock-template/app/api/cron/sync-currencies/route.ts +39 -39
  26. package/templates/nextblock-template/app/api/custom-blocks/db-relations/route.ts +92 -92
  27. package/templates/nextblock-template/app/api/custom-blocks/editor-definitions/route.ts +43 -43
  28. package/templates/nextblock-template/app/api/media/library/route.ts +69 -69
  29. package/templates/nextblock-template/app/api/media/r2-presigned/route.ts +53 -53
  30. package/templates/nextblock-template/app/api/visual-editing/block-draft/route.ts +47 -47
  31. package/templates/nextblock-template/app/api/visual-editing/product-draft/route.ts +47 -47
  32. package/templates/nextblock-template/app/article/[slug]/PostClientContent.tsx +442 -441
  33. package/templates/nextblock-template/app/article/[slug]/page.utils.ts +4 -1
  34. package/templates/nextblock-template/app/checkout/UcpCartHydrator.tsx +20 -20
  35. package/templates/nextblock-template/app/cms/blocks/components/BlockEditorModal.tsx +241 -241
  36. package/templates/nextblock-template/app/cms/blocks/components/CustomBlockEditorPreview.tsx +160 -160
  37. package/templates/nextblock-template/app/cms/blocks/editors/DynamicCustomBlockEditor.tsx +167 -167
  38. package/templates/nextblock-template/app/cms/components/ConnectGitHubButton.tsx +122 -122
  39. package/templates/nextblock-template/app/cms/components/CortexAiActiveContext.tsx +23 -23
  40. package/templates/nextblock-template/app/cms/components/SeoScoreBadge.tsx +3 -40
  41. package/templates/nextblock-template/app/cms/components/SystemAlertsBanner.tsx +112 -112
  42. package/templates/nextblock-template/app/cms/components/TablePagination.tsx +136 -136
  43. package/templates/nextblock-template/app/cms/components/TwoFactorReminderBanner.tsx +45 -45
  44. package/templates/nextblock-template/app/cms/components/github-connect-actions.ts +102 -102
  45. package/templates/nextblock-template/app/cms/components/system-alerts-actions.ts +31 -31
  46. package/templates/nextblock-template/app/cms/coupons/[id]/edit/page.tsx +16 -16
  47. package/templates/nextblock-template/app/cms/coupons/page.tsx +16 -16
  48. package/templates/nextblock-template/app/cms/custom-blocks/[id]/edit/page.tsx +66 -66
  49. package/templates/nextblock-template/app/cms/custom-blocks/actions.ts +519 -519
  50. package/templates/nextblock-template/app/cms/custom-blocks/components/BlocksLibraryTransferControls.tsx +256 -256
  51. package/templates/nextblock-template/app/cms/custom-blocks/components/DBRelationSelect.tsx +384 -384
  52. package/templates/nextblock-template/app/cms/custom-blocks/components/ImageR2Picker.tsx +221 -221
  53. package/templates/nextblock-template/app/cms/custom-blocks/new/page.tsx +12 -12
  54. package/templates/nextblock-template/app/cms/custom-blocks/page.tsx +438 -438
  55. package/templates/nextblock-template/app/cms/dashboard/components/DashboardComponents.tsx +200 -200
  56. package/templates/nextblock-template/app/cms/dashboard/components/DashboardOnboarding.tsx +130 -130
  57. package/templates/nextblock-template/app/cms/import-export/actions.ts +226 -226
  58. package/templates/nextblock-template/app/cms/pages/page.tsx +273 -273
  59. package/templates/nextblock-template/app/cms/posts/page.tsx +251 -251
  60. package/templates/nextblock-template/app/cms/products/ProductFormClientShell.tsx +19 -0
  61. package/templates/nextblock-template/app/cms/products/[id]/edit/page.tsx +14 -2
  62. package/templates/nextblock-template/app/cms/products/categories/page.tsx +12 -12
  63. package/templates/nextblock-template/app/cms/products/new/page.tsx +135 -135
  64. package/templates/nextblock-template/app/cms/promotions/PromotionsWorkspace.tsx +456 -456
  65. package/templates/nextblock-template/app/cms/promotions/actions.ts +115 -115
  66. package/templates/nextblock-template/app/cms/promotions/page.tsx +31 -31
  67. package/templates/nextblock-template/app/cms/settings/backup-restore/BackupRestoreWorkspace.tsx +1004 -1004
  68. package/templates/nextblock-template/app/cms/settings/backup-restore/page.tsx +29 -29
  69. package/templates/nextblock-template/app/cms/settings/bot-protection/page.tsx +24 -24
  70. package/templates/nextblock-template/app/cms/settings/email/page.tsx +28 -28
  71. package/templates/nextblock-template/app/cms/settings/extra-translations/actions.ts +276 -276
  72. package/templates/nextblock-template/app/cms/settings/google-analytics/page.tsx +26 -26
  73. package/templates/nextblock-template/app/cms/settings/logos/components/DeleteLogoButton.tsx +21 -21
  74. package/templates/nextblock-template/app/cms/settings/logos/components/SetActiveLogoButton.tsx +42 -42
  75. package/templates/nextblock-template/app/cms/settings/logos/components/SiteSeoSettingsForm.tsx +133 -133
  76. package/templates/nextblock-template/app/cms/settings/privacy/page.tsx +27 -27
  77. package/templates/nextblock-template/app/cms/settings/registration/page.tsx +27 -27
  78. package/templates/nextblock-template/app/cms/settings/security/page.tsx +33 -33
  79. package/templates/nextblock-template/app/lib/site-settings.ts +105 -105
  80. package/templates/nextblock-template/app/lib/ucp/protocol.ts +190 -190
  81. package/templates/nextblock-template/app/lib/ucp/server.test.ts +56 -56
  82. package/templates/nextblock-template/app/setup/SetupWizard.tsx +678 -678
  83. package/templates/nextblock-template/app/setup/layout.tsx +13 -13
  84. package/templates/nextblock-template/app/setup/page.tsx +111 -111
  85. package/templates/nextblock-template/app/ucp/v1/carts/[id]/cancel/route.ts +38 -38
  86. package/templates/nextblock-template/app/ucp/v1/carts/[id]/route.ts +68 -68
  87. package/templates/nextblock-template/app/ucp/v1/carts/route.ts +35 -35
  88. package/templates/nextblock-template/app/ucp/v1/catalog/lookup/route.ts +35 -35
  89. package/templates/nextblock-template/app/ucp/v1/catalog/product/route.ts +35 -35
  90. package/templates/nextblock-template/app/ucp/v1/catalog/search/route.ts +34 -34
  91. package/templates/nextblock-template/components/CartTranslator.tsx +210 -210
  92. package/templates/nextblock-template/components/DeferredCartTranslator.tsx +51 -51
  93. package/templates/nextblock-template/components/DeferredGlobalSearch.tsx +68 -68
  94. package/templates/nextblock-template/components/DeferredGoogleAnalytics.tsx +70 -70
  95. package/templates/nextblock-template/components/FeatureImageHero.tsx +47 -47
  96. package/templates/nextblock-template/components/GlobalSearch.tsx +557 -557
  97. package/templates/nextblock-template/components/Header.tsx +38 -38
  98. package/templates/nextblock-template/components/PublicEnvBootstrap.tsx +30 -30
  99. package/templates/nextblock-template/components/ResponsiveNav.tsx +14 -14
  100. package/templates/nextblock-template/components/auth/AuthBotProtection.tsx +182 -182
  101. package/templates/nextblock-template/components/blocks/PostsGridClient.tsx +48 -48
  102. package/templates/nextblock-template/components/blocks/TestimonialBlock.tsx +9 -9
  103. package/templates/nextblock-template/components/blocks/publicRendererLoaders.ts +25 -25
  104. package/templates/nextblock-template/components/blocks/renderers/ButtonBlockRenderer.tsx +92 -92
  105. package/templates/nextblock-template/components/blocks/renderers/ClientTextBlockRenderer.tsx +11 -0
  106. package/templates/nextblock-template/components/blocks/renderers/PostsGridBlockRenderer.tsx +24 -24
  107. package/templates/nextblock-template/components/blocks/renderers/TestimonialBlockRenderer.tsx +57 -57
  108. package/templates/nextblock-template/components/blocks/renderers/inline/AlertWidgetRenderer.tsx +2 -2
  109. package/templates/nextblock-template/components/blocks/renderers/inline/CtaWidgetRenderer.tsx +2 -2
  110. package/templates/nextblock-template/components/media/YouTubeFacade.tsx +105 -105
  111. package/templates/nextblock-template/components/media/youtube-embed-replace.tsx +32 -32
  112. package/templates/nextblock-template/components/privacy/ConsentBanner.tsx +170 -170
  113. package/templates/nextblock-template/components/privacy/ConsentGatedAnalytics.tsx +70 -70
  114. package/templates/nextblock-template/components/renderers/CachedDynamicLayoutEngine.tsx +28 -28
  115. package/templates/nextblock-template/components/renderers/DynamicLayoutEngine.test.tsx +166 -166
  116. package/templates/nextblock-template/components/renderers/DynamicLayoutEngine.tsx +471 -471
  117. package/templates/nextblock-template/components/visual-editing/DeferredVisualEditing.tsx +21 -21
  118. package/templates/nextblock-template/context/language-rest-client.ts +32 -32
  119. package/templates/nextblock-template/docker/db/init/99-jwt.sql +6 -6
  120. package/templates/nextblock-template/docker/db/init/99-roles.sql +25 -25
  121. package/templates/nextblock-template/docker/kong/kong.yml +112 -112
  122. package/templates/nextblock-template/docs/02-ECOMMERCE-CAPABILITIES.md +364 -364
  123. package/templates/nextblock-template/docs/04-DATABASE-AND-AUTH.md +11 -0
  124. package/templates/nextblock-template/docs/07-BLOCK-SDK-AND-EXTENSIBILITY.md +146 -146
  125. package/templates/nextblock-template/docs/08-NEXTBLOCK-CORTEX-AI-ARCHITECTURE.md +19 -0
  126. package/templates/nextblock-template/docs/10-CUSTOM-BLOCKS.md +222 -222
  127. package/templates/nextblock-template/lib/app-secrets.ts +39 -39
  128. package/templates/nextblock-template/lib/auth/cookies.ts +47 -47
  129. package/templates/nextblock-template/lib/auth/crypto.ts +45 -45
  130. package/templates/nextblock-template/lib/auth/trustedDevices.ts +92 -92
  131. package/templates/nextblock-template/lib/blocks/README.md +13 -13
  132. package/templates/nextblock-template/lib/botProtection/verify.ts +134 -134
  133. package/templates/nextblock-template/lib/cms/payments-reminder.test.ts +135 -135
  134. package/templates/nextblock-template/lib/cms/payments-reminder.ts +105 -105
  135. package/templates/nextblock-template/lib/cms-transfer/types.ts +145 -145
  136. package/templates/nextblock-template/lib/custom-block-definitions.ts +87 -87
  137. package/templates/nextblock-template/lib/custom-block-r2-upload-shared.ts +178 -178
  138. package/templates/nextblock-template/lib/custom-block-r2-upload.test.ts +140 -140
  139. package/templates/nextblock-template/lib/custom-block-r2-upload.ts +88 -88
  140. package/templates/nextblock-template/lib/custom-block-relations.test.ts +227 -227
  141. package/templates/nextblock-template/lib/custom-block-relations.ts +279 -279
  142. package/templates/nextblock-template/lib/custom-block-safelist.ts +14 -14
  143. package/templates/nextblock-template/lib/editor/dynamic-extension-core.test.ts +172 -172
  144. package/templates/nextblock-template/lib/editor/dynamic-extension-core.ts +213 -213
  145. package/templates/nextblock-template/lib/editor/dynamic-extension-loader.ts +22 -22
  146. package/templates/nextblock-template/lib/editor/dynamic-extensions.tsx +193 -193
  147. package/templates/nextblock-template/lib/email/branding-format.test.ts +133 -133
  148. package/templates/nextblock-template/lib/email/branding-format.ts +123 -123
  149. package/templates/nextblock-template/lib/email/branding.ts +76 -76
  150. package/templates/nextblock-template/lib/full-backup/manifest.test.ts +121 -121
  151. package/templates/nextblock-template/lib/full-backup/manifest.ts +206 -206
  152. package/templates/nextblock-template/lib/logos/active-logo.ts +53 -53
  153. package/templates/nextblock-template/lib/media/resolveMediaUrl.ts +56 -54
  154. package/templates/nextblock-template/lib/media/youtube.ts +99 -99
  155. package/templates/nextblock-template/lib/onboarding/actions.ts +31 -31
  156. package/templates/nextblock-template/lib/privacy/consent-client.ts +57 -57
  157. package/templates/nextblock-template/lib/privacy/contact-emails.ts +64 -64
  158. package/templates/nextblock-template/lib/privacy/settings.ts +115 -115
  159. package/templates/nextblock-template/lib/privacy/types.ts +69 -69
  160. package/templates/nextblock-template/lib/promotions/server.test.ts +74 -74
  161. package/templates/nextblock-template/lib/promotions/server.ts +741 -741
  162. package/templates/nextblock-template/lib/resolve-block-relations.test.ts +142 -142
  163. package/templates/nextblock-template/lib/resolve-block-relations.ts +255 -255
  164. package/templates/nextblock-template/lib/seo/page-audit-context.tsx +3 -3
  165. package/templates/nextblock-template/lib/seo/page-document.test.ts +34 -0
  166. package/templates/nextblock-template/lib/seo/page-document.ts +3 -462
  167. package/templates/nextblock-template/lib/seo/robots-txt.ts +5 -4
  168. package/templates/nextblock-template/lib/setup/actions.ts +460 -460
  169. package/templates/nextblock-template/lib/setup/env-status.ts +125 -125
  170. package/templates/nextblock-template/lib/setup/env-write.ts +111 -111
  171. package/templates/nextblock-template/lib/setup/migrations-bundle.ts +50 -10
  172. package/templates/nextblock-template/lib/setup/provisioning.ts +59 -59
  173. package/templates/nextblock-template/lib/setup/schema-apply.ts +408 -408
  174. package/templates/nextblock-template/lib/setup/system-config.ts +105 -105
  175. package/templates/nextblock-template/lib/setup/types.ts +18 -18
  176. package/templates/nextblock-template/lib/storage/provider.ts +66 -66
  177. package/templates/nextblock-template/lib/storage/supabase-storage.ts +103 -103
  178. package/templates/nextblock-template/lib/updates/github-device.ts +206 -206
  179. package/templates/nextblock-template/lib/updates/repo-identity.ts +56 -56
  180. package/templates/nextblock-template/lib/visual-editing/draft-content.test.ts +105 -105
  181. package/templates/nextblock-template/lib/visual-editing/draft-route.test.ts +42 -42
  182. package/templates/nextblock-template/lib/visual-editing/edit-info.test.ts +143 -143
  183. package/templates/nextblock-template/lib/visual-editing/edit-info.ts +94 -94
  184. package/templates/nextblock-template/lib/visual-editing/product-drafts.test.ts +81 -81
  185. package/templates/nextblock-template/lib/zod-config.ts +5 -5
  186. package/templates/nextblock-template/next-env.d.ts +1 -0
  187. package/templates/nextblock-template/package.json +1 -1
  188. package/templates/nextblock-template/public/images/cortex_post.webp +0 -0
  189. package/templates/nextblock-template/public/images/update_nextblock.webp +0 -0
  190. package/templates/nextblock-template/scripts/docker-setup.mjs +310 -310
  191. package/templates/nextblock-template/scripts/validate-editor-block-schema.ts +112 -112
  192. package/templates/nextblock-template/scripts/verify-cortex-ai-build-widget.tsx +98 -98
  193. package/templates/nextblock-template/scripts/verify-cortex-ai-generate-blocks.ts +62 -62
  194. package/templates/nextblock-template/scripts/verify-cortex-ai-global-tools.ts +537 -537
  195. package/templates/nextblock-template/scripts/verify-cortex-ai-routing.ts +58 -58
  196. package/templates/nextblock-template/scripts/verify-custom-block-definitions.ts +188 -188
  197. package/templates/nextblock-template/scripts/verify-dynamic-custom-block-extensions.ts +123 -123
  198. package/templates/nextblock-template/scripts/verify-dynamic-layout-engine.tsx +133 -133
  199. package/templates/nextblock-template/scripts/verify-milestone-2-custom-blocks.ts +65 -65
  200. package/templates/nextblock-template/tools/deploy-supabase.js +159 -159
  201. package/templates/nextblock-template/types/jsdom.d.ts +6 -6
@@ -1,364 +1,364 @@
1
- # 02 Ecommerce Capabilities
2
-
3
- ## Scope and Source of Truth
4
-
5
- The commerce feature set is implemented across:
6
-
7
- - `libs/ecommerce/src/lib/*`
8
- - `libs/db/src/supabase/migrations/00000000000003` through `00000000000006`
9
- - `apps/nextblock/app/api/checkout/route.ts`
10
- - `apps/nextblock/app/api/webhooks/*`
11
- - `apps/nextblock/app/cms/products`, `orders`, `shipping`, `payments`, and
12
- `settings/taxes`
13
-
14
- In workspace code, the developer-facing import paths are:
15
-
16
- - `@nextblock-cms/ecommerce`
17
- - `@nextblock-cms/ecommerce/server`
18
- - `@nextblock-cms/ecommerce/actions`
19
-
20
- One packaging discrepancy exists today: `libs/ecommerce/package.json` is still
21
- named `@nextblock-cms/ecom`, while the workspace and CLI activation flow expose
22
- the package through the `@nextblock-cms/ecommerce` alias.
23
-
24
- ## Commerce Data Model
25
-
26
- The commerce schema spans:
27
-
28
- - Catalog: `products`, `product_media`, `product_attributes`,
29
- `product_attribute_terms`, `product_variants`,
30
- `variant_attribute_mapping`, `categories`, `product_categories`
31
- (categories and their translations were added by migrations
32
- `00000000000019` and `00000000000020`)
33
- - Inventory and licensing: `inventory_items`, `package_activations`,
34
- `freemius_plans`, `freemius_pricing`
35
- - Checkout and fulfillment: `orders`, `order_items`, `shipping_zones`,
36
- `shipping_zone_locations`, `shipping_zone_methods`, `tax_rates`,
37
- `currencies`
38
-
39
- `products` can be physical or digital. The current provider selection logic
40
- resolves:
41
-
42
- - physical -> Stripe
43
- - digital -> Freemius
44
-
45
- Mixed-provider carts are rejected by `app/api/checkout/route.ts`.
46
-
47
- ## Multi-Currency
48
-
49
- The multi-currency implementation is real and fairly deep.
50
-
51
- ### Store currencies
52
-
53
- `currencies` stores:
54
-
55
- - ISO code and symbol
56
- - exchange rate relative to the current store default
57
- - default and active flags
58
- - rounding mode, rounding increment, and optional charm ending
59
- - automatic FX refresh flag
60
- - automatic product price sync flag
61
- - last exchange-rate source and refresh timestamp
62
-
63
- Supported rounding modes in code are:
64
-
65
- - `none`
66
- - `nearest`
67
- - `up`
68
- - `down`
69
- - `charm`
70
-
71
- ### Product and variant pricing
72
-
73
- Products and variants support:
74
-
75
- - legacy single-currency `price` and `sale_price`
76
- - multi-currency `prices` and `sale_prices`
77
-
78
- The pricing helpers resolve amounts by:
79
-
80
- 1. looking for an explicit amount in the selected currency
81
- 2. falling back to the base price
82
- 3. converting from the default currency when store-managed auto-sync pricing is
83
- enabled
84
-
85
- The CMS product editor respects that distinction. Store-managed currencies can
86
- be displayed in forms, but their saved overrides are stripped before
87
- persisting.
88
-
89
- ### Scheduled sales, price changes, and promotions
90
-
91
- Products and variants carry a scheduled-pricing layer (migration
92
- `00000000000025_add_sale_schedule_columns.sql`):
93
-
94
- - `sale_start_at` / `sale_end_at` — the time window during which `sale_price` /
95
- `sale_prices` apply. Both null = always-on (back-compat with static sales).
96
- - `scheduled_price` / `scheduled_prices` / `scheduled_price_at` — a pending,
97
- permanent regular-price change applied once `scheduled_price_at` passes
98
- (bulk/Stripe-oriented; Freemius regular prices are owned by Freemius).
99
- - `product_freemius_sale_coupons` — maps a product to an auto-generated,
100
- time-bounded Freemius coupon so a scheduled Freemius sale is actually enforced
101
- at Freemius-hosted checkout (Freemius enforces the coupon's start/end dates).
102
-
103
- **Enforcement is read-time, not cron-driven.** The helpers in
104
- `libs/ecommerce/src/lib/currency.ts` decide what applies *now*:
105
-
106
- - `isSaleWindowActive({ saleStartAt, saleEndAt, now })`
107
- - `resolveEffectivePriceForCurrency({ ..., saleStartAt, saleEndAt, scheduledPrice*, now })`
108
- — wraps `resolvePriceForCurrency`, gating the sale by its window and swapping in
109
- a due scheduled price. A sale outside its window resolves to `sale_price: null`.
110
-
111
- Every place that computes a payable or displayed amount goes through the
112
- window-aware helper: checkout providers (`providers/stripe.ts`,
113
- `providers/freemius.ts`), cart/tax/coupon math, and storefront components
114
- (`ProductCard`, `FeaturedProduct`, `ProductDetailsLayout`, UCP). The CMS edit
115
- form exposes the window per-product and per-variant (`SaleScheduleFields`); the
116
- bulk **Promotions** admin (`/cms/promotions`, `apps/nextblock/lib/promotions/`)
117
- imports/exports sales and price changes via CSV.
118
-
119
- > **Gotchas for future agents (these caused real revert/display bugs):**
120
- >
121
- > 1. **`generateVariantDrafts` (`variation-utils.ts`) must carry over every
122
- > per-variant field**, including `sale_start_at`/`sale_end_at`. It re-runs on
123
- > editor mount/attribute change; an omitted field resets to null and is then
124
- > autosaved away ("dates revert after publish").
125
- > 2. **Storefront block mappers must pass the window through** on *both* the
126
- > product and each variant — `ProductGridBlock`, `FeaturedProductBlock`,
127
- > `app/product/[slug]/page.tsx`. If the window is dropped, `isSaleWindowActive`
128
- > sees both bounds null and treats the sale as always-on, so an inactive sale
129
- > price shows (e.g. a "$25 – $32" range before the sale starts).
130
- > 3. **`getVariantEffectivePriceRange` is window-aware** — pass
131
- > `sale_start_at`/`sale_end_at` per variant or it ignores the schedule.
132
- > 4. **Persisting on save/publish does not rely on the RPC.** Products are
133
- > written via `upsert_product_with_variants`, but some databases run a stale
134
- > copy of that function. `persistProductSaleSchedule` (`product-actions.ts`)
135
- > writes the window columns with a direct `update` right after the RPC (same
136
- > pattern as `persistProductTaxability`), matching variants by SKU.
137
- > 5. **Autosave must not `reset()` the form or revalidate the edit route.** The
138
- > edit-form autosave (`ProductForm.tsx`) uses a serialized-snapshot guard to
139
- > avoid a render loop; `updateProductAction` writes only the draft (no
140
- > `revalidatePath`). Re-rendering mid-edit resets native datetime inputs.
141
-
142
- ### FX sync and rebasing
143
-
144
- `libs/ecommerce/src/lib/currency-sync.ts` implements two separate operations:
145
-
146
- - `syncStoreCurrencyRates()`: pulls fresh FX rates from `https://api.frankfurter.dev`
147
- unless `FX_API_BASE_URL` overrides the provider.
148
- - `rebaseStoreCurrencyExchangeRates()`: when an admin changes the default
149
- currency, every stored rate is rebased so the new default becomes `1`.
150
-
151
- The app exposes both through:
152
-
153
- - CMS currency settings actions in
154
- `apps/nextblock/app/cms/settings/currencies/actions.ts`
155
- - `GET /api/cron/sync-currencies`, guarded by `CRON_SECRET`
156
-
157
- ## Tax Calculation
158
-
159
- Tax behavior is driven by the `ecommerce_inventory_settings` site setting,
160
- loaded through `getEcommerceInventorySettings()`.
161
-
162
- The current settings shape is:
163
-
164
- - `trackQuantities`
165
- - `enableTaxes`
166
- - `taxCalculationMode`
167
-
168
- Supported tax modes are:
169
-
170
- - `manual`
171
- - `automatic`
172
-
173
- ### Manual mode
174
-
175
- Manual mode uses `tax_rates` rows keyed by country and optional state/province.
176
- Multiple rows can exist for the same jurisdiction, so stacked taxes such as GST
177
- plus PST are supported.
178
-
179
- During checkout:
180
-
181
- - only taxable products are included
182
- - the destination is normalized from shipping or billing data
183
- - matching `tax_rates` are loaded
184
- - tax lines are calculated and stored in `orders.tax_details`
185
-
186
- ### Automatic mode
187
-
188
- Automatic mode defers final tax calculation to Stripe Tax.
189
-
190
- In this mode:
191
-
192
- - checkout still records a tax intent in `orders.tax_details`
193
- - the Stripe provider marks product and shipping tax codes on line items
194
- - the webhook resync step replaces provisional tax data with finalized Stripe
195
- checkout data
196
-
197
- ## Shipping Zones and Rate Resolution
198
-
199
- Shipping is backed by:
200
-
201
- - `shipping_zones`
202
- - `shipping_zone_locations`
203
- - `shipping_zone_methods`
204
-
205
- Each method stores:
206
-
207
- - base amount and currency
208
- - localized names
209
- - per-currency amount maps and threshold maps
210
- - `currency_pricing_mode` of `auto` or `manual`
211
-
212
- ### Current resolver behavior
213
-
214
- `libs/ecommerce/src/lib/shipping/resolver.ts` currently:
215
-
216
- 1. loads active currencies
217
- 2. queries zone locations by destination country
218
- 3. prefers a state match when one exists
219
- 4. otherwise prefers a country-wide match
220
- 5. otherwise falls back to the first zone by `priority_order`
221
- 6. filters methods by the cart total and minimum threshold
222
- 7. converts method prices into the shopper currency
223
- 8. returns only the cheapest valid method
224
-
225
- Important implementation detail: `shipping_zone_locations.postal_code` exists in
226
- schema, but the current resolver does not yet use postal code matching during
227
- runtime resolution.
228
-
229
- The storefront calls this through
230
- `libs/ecommerce/src/lib/server-actions/shipping-actions.ts`.
231
-
232
- ## Stripe Integration
233
-
234
- Stripe is the current payment flow for physical products.
235
-
236
- ### Checkout flow
237
-
238
- `app/api/checkout/route.ts`:
239
-
240
- - verifies the ecommerce package is active
241
- - rejects carts without provider-aware items
242
- - rejects mixed-provider carts
243
- - requires a billing address
244
- - resolves the provider through `getPaymentProvider()`
245
-
246
- `StripeProvider.createCheckoutSession()` then:
247
-
248
- - loads currencies and store settings
249
- - validates products and variants against the database
250
- - validates inventory before session creation when quantity tracking is enabled
251
- - resolves shipping cost from the selected shipping method
252
- - calculates tax in manual or automatic mode
253
- - upserts a Stripe customer when an email is available
254
- - inserts a pending `orders` row plus `order_items`
255
- - stores currency, subtotal, shipping, tax, and exchange-rate data
256
- - creates the Stripe Checkout Session and stores `stripe_session_id`
257
-
258
- ### Webhook flow
259
-
260
- `app/api/webhooks/stripe/route.ts` passes the raw body to
261
- `handleStripeWebhook()`.
262
-
263
- On `checkout.session.completed`, the sync layer:
264
-
265
- - reloads the session with tax breakdown details
266
- - finds the existing order
267
- - stores payment intent, customer details, and finalized totals
268
- - normalizes tax details from Stripe
269
- - updates saved customer addresses
270
- - assigns invoice metadata
271
- - applies inventory deduction
272
-
273
- ## Freemius Licensing and Product Sync
274
-
275
- Freemius currently handles digital-product checkout and product synchronization.
276
-
277
- ### Checkout behavior
278
-
279
- `FreemiusProvider.createCheckoutSession()`:
280
-
281
- - only allows one item per checkout
282
- - loads the product from Supabase
283
- - requires `freemius_product_id` and `freemius_plan_id`
284
- - resolves pricing in the chosen currency
285
- - inserts a pending order and order item
286
- - optionally syncs default addresses and profile fields for the current user
287
- - builds a Freemius checkout URL, including sandbox parameters when enabled
288
-
289
- Supported credential sources include:
290
-
291
- - product-scoped JSON map
292
- - single-product sandbox overrides
293
- - single-product env vars
294
- - legacy shared env vars
295
-
296
- ### Product sync
297
-
298
- `syncFreemiusProductsToSupabase()` and `syncSingleFreemiusProduct()`:
299
-
300
- - call the Freemius API with signed requests
301
- - fetch plugins, plans, and pricing
302
- - upsert digital products into `products`
303
- - upsert related `freemius_plans` and `freemius_pricing`
304
-
305
- These flows are surfaced in the CMS product actions and the sandbox reset route.
306
-
307
- ### Current webhook limitation
308
-
309
- `app/api/webhooks/freemius/route.ts` currently verifies the webhook signature
310
- and acknowledges selected event types, but it does not yet reconcile license or
311
- order state back into the local database.
312
-
313
- ## Inventory Management and Fulfillment
314
-
315
- Inventory behavior is controlled by `trackQuantities`.
316
-
317
- When tracking is enabled:
318
-
319
- - checkout validates requested quantity against `inventory_items`
320
- - if a SKU is not yet cached there, product or variant stock fields are used as
321
- fallback
322
- - a paid order triggers `apply_order_inventory_deduction()`
323
-
324
- The deduction flow is resilient:
325
-
326
- - first it calls the database RPC `apply_order_inventory_deduction`
327
- - if that fails, it falls back to a direct SQL reconciliation path using
328
- `POSTGRES_URL` or `DATABASE_URL`
329
-
330
- Order statuses currently supported in code are:
331
-
332
- - `pending`
333
- - `paid`
334
- - `shipped`
335
- - `cancelled`
336
- - `refunded`
337
-
338
- Manual CMS order status changes also trigger invoice assignment and inventory
339
- deduction when an order is moved to `paid`.
340
-
341
- ## Invoice and Order Presentation
342
-
343
- The order/invoice layer includes:
344
-
345
- - stable invoice numbering through database functions
346
- - `invoice_settings` in `site_settings`
347
- - printable invoice presentation data via `invoice-server.ts`
348
- - UI components such as `InvoiceDocument` and `InvoiceViewerShell`
349
- - customer order history and invoice access through `customer-orders.ts`
350
-
351
- ## CMS Commerce Surfaces
352
-
353
- The active ecommerce CMS surface includes:
354
-
355
- - product list, create, edit, media, attribute, and variation management
356
- - inventory management
357
- - orders list and detail management
358
- - shipping zones and shipping rate management
359
- - payment-provider enablement
360
- - tax settings and manual tax-rate management
361
- - currency settings under `/cms/settings/currencies`
362
-
363
- The CMS shell only exposes these store sections when the ecommerce package is
364
- reported as active.
1
+ # 02 Ecommerce Capabilities
2
+
3
+ ## Scope and Source of Truth
4
+
5
+ The commerce feature set is implemented across:
6
+
7
+ - `libs/ecommerce/src/lib/*`
8
+ - `libs/db/src/supabase/migrations/00000000000003` through `00000000000006`
9
+ - `apps/nextblock/app/api/checkout/route.ts`
10
+ - `apps/nextblock/app/api/webhooks/*`
11
+ - `apps/nextblock/app/cms/products`, `orders`, `shipping`, `payments`, and
12
+ `settings/taxes`
13
+
14
+ In workspace code, the developer-facing import paths are:
15
+
16
+ - `@nextblock-cms/ecommerce`
17
+ - `@nextblock-cms/ecommerce/server`
18
+ - `@nextblock-cms/ecommerce/actions`
19
+
20
+ One packaging discrepancy exists today: `libs/ecommerce/package.json` is still
21
+ named `@nextblock-cms/ecom`, while the workspace and CLI activation flow expose
22
+ the package through the `@nextblock-cms/ecommerce` alias.
23
+
24
+ ## Commerce Data Model
25
+
26
+ The commerce schema spans:
27
+
28
+ - Catalog: `products`, `product_media`, `product_attributes`,
29
+ `product_attribute_terms`, `product_variants`,
30
+ `variant_attribute_mapping`, `categories`, `product_categories`
31
+ (categories and their translations were added by migrations
32
+ `00000000000019` and `00000000000020`)
33
+ - Inventory and licensing: `inventory_items`, `package_activations`,
34
+ `freemius_plans`, `freemius_pricing`
35
+ - Checkout and fulfillment: `orders`, `order_items`, `shipping_zones`,
36
+ `shipping_zone_locations`, `shipping_zone_methods`, `tax_rates`,
37
+ `currencies`
38
+
39
+ `products` can be physical or digital. The current provider selection logic
40
+ resolves:
41
+
42
+ - physical -> Stripe
43
+ - digital -> Freemius
44
+
45
+ Mixed-provider carts are rejected by `app/api/checkout/route.ts`.
46
+
47
+ ## Multi-Currency
48
+
49
+ The multi-currency implementation is real and fairly deep.
50
+
51
+ ### Store currencies
52
+
53
+ `currencies` stores:
54
+
55
+ - ISO code and symbol
56
+ - exchange rate relative to the current store default
57
+ - default and active flags
58
+ - rounding mode, rounding increment, and optional charm ending
59
+ - automatic FX refresh flag
60
+ - automatic product price sync flag
61
+ - last exchange-rate source and refresh timestamp
62
+
63
+ Supported rounding modes in code are:
64
+
65
+ - `none`
66
+ - `nearest`
67
+ - `up`
68
+ - `down`
69
+ - `charm`
70
+
71
+ ### Product and variant pricing
72
+
73
+ Products and variants support:
74
+
75
+ - legacy single-currency `price` and `sale_price`
76
+ - multi-currency `prices` and `sale_prices`
77
+
78
+ The pricing helpers resolve amounts by:
79
+
80
+ 1. looking for an explicit amount in the selected currency
81
+ 2. falling back to the base price
82
+ 3. converting from the default currency when store-managed auto-sync pricing is
83
+ enabled
84
+
85
+ The CMS product editor respects that distinction. Store-managed currencies can
86
+ be displayed in forms, but their saved overrides are stripped before
87
+ persisting.
88
+
89
+ ### Scheduled sales, price changes, and promotions
90
+
91
+ Products and variants carry a scheduled-pricing layer (migration
92
+ `00000000000025_add_sale_schedule_columns.sql`):
93
+
94
+ - `sale_start_at` / `sale_end_at` — the time window during which `sale_price` /
95
+ `sale_prices` apply. Both null = always-on (back-compat with static sales).
96
+ - `scheduled_price` / `scheduled_prices` / `scheduled_price_at` — a pending,
97
+ permanent regular-price change applied once `scheduled_price_at` passes
98
+ (bulk/Stripe-oriented; Freemius regular prices are owned by Freemius).
99
+ - `product_freemius_sale_coupons` — maps a product to an auto-generated,
100
+ time-bounded Freemius coupon so a scheduled Freemius sale is actually enforced
101
+ at Freemius-hosted checkout (Freemius enforces the coupon's start/end dates).
102
+
103
+ **Enforcement is read-time, not cron-driven.** The helpers in
104
+ `libs/ecommerce/src/lib/currency.ts` decide what applies *now*:
105
+
106
+ - `isSaleWindowActive({ saleStartAt, saleEndAt, now })`
107
+ - `resolveEffectivePriceForCurrency({ ..., saleStartAt, saleEndAt, scheduledPrice*, now })`
108
+ — wraps `resolvePriceForCurrency`, gating the sale by its window and swapping in
109
+ a due scheduled price. A sale outside its window resolves to `sale_price: null`.
110
+
111
+ Every place that computes a payable or displayed amount goes through the
112
+ window-aware helper: checkout providers (`providers/stripe.ts`,
113
+ `providers/freemius.ts`), cart/tax/coupon math, and storefront components
114
+ (`ProductCard`, `FeaturedProduct`, `ProductDetailsLayout`, UCP). The CMS edit
115
+ form exposes the window per-product and per-variant (`SaleScheduleFields`); the
116
+ bulk **Promotions** admin (`/cms/promotions`, `apps/nextblock/lib/promotions/`)
117
+ imports/exports sales and price changes via CSV.
118
+
119
+ > **Gotchas for future agents (these caused real revert/display bugs):**
120
+ >
121
+ > 1. **`generateVariantDrafts` (`variation-utils.ts`) must carry over every
122
+ > per-variant field**, including `sale_start_at`/`sale_end_at`. It re-runs on
123
+ > editor mount/attribute change; an omitted field resets to null and is then
124
+ > autosaved away ("dates revert after publish").
125
+ > 2. **Storefront block mappers must pass the window through** on *both* the
126
+ > product and each variant — `ProductGridBlock`, `FeaturedProductBlock`,
127
+ > `app/product/[slug]/page.tsx`. If the window is dropped, `isSaleWindowActive`
128
+ > sees both bounds null and treats the sale as always-on, so an inactive sale
129
+ > price shows (e.g. a "$25 – $32" range before the sale starts).
130
+ > 3. **`getVariantEffectivePriceRange` is window-aware** — pass
131
+ > `sale_start_at`/`sale_end_at` per variant or it ignores the schedule.
132
+ > 4. **Persisting on save/publish does not rely on the RPC.** Products are
133
+ > written via `upsert_product_with_variants`, but some databases run a stale
134
+ > copy of that function. `persistProductSaleSchedule` (`product-actions.ts`)
135
+ > writes the window columns with a direct `update` right after the RPC (same
136
+ > pattern as `persistProductTaxability`), matching variants by SKU.
137
+ > 5. **Autosave must not `reset()` the form or revalidate the edit route.** The
138
+ > edit-form autosave (`ProductForm.tsx`) uses a serialized-snapshot guard to
139
+ > avoid a render loop; `updateProductAction` writes only the draft (no
140
+ > `revalidatePath`). Re-rendering mid-edit resets native datetime inputs.
141
+
142
+ ### FX sync and rebasing
143
+
144
+ `libs/ecommerce/src/lib/currency-sync.ts` implements two separate operations:
145
+
146
+ - `syncStoreCurrencyRates()`: pulls fresh FX rates from `https://api.frankfurter.dev`
147
+ unless `FX_API_BASE_URL` overrides the provider.
148
+ - `rebaseStoreCurrencyExchangeRates()`: when an admin changes the default
149
+ currency, every stored rate is rebased so the new default becomes `1`.
150
+
151
+ The app exposes both through:
152
+
153
+ - CMS currency settings actions in
154
+ `apps/nextblock/app/cms/settings/currencies/actions.ts`
155
+ - `GET /api/cron/sync-currencies`, guarded by `CRON_SECRET`
156
+
157
+ ## Tax Calculation
158
+
159
+ Tax behavior is driven by the `ecommerce_inventory_settings` site setting,
160
+ loaded through `getEcommerceInventorySettings()`.
161
+
162
+ The current settings shape is:
163
+
164
+ - `trackQuantities`
165
+ - `enableTaxes`
166
+ - `taxCalculationMode`
167
+
168
+ Supported tax modes are:
169
+
170
+ - `manual`
171
+ - `automatic`
172
+
173
+ ### Manual mode
174
+
175
+ Manual mode uses `tax_rates` rows keyed by country and optional state/province.
176
+ Multiple rows can exist for the same jurisdiction, so stacked taxes such as GST
177
+ plus PST are supported.
178
+
179
+ During checkout:
180
+
181
+ - only taxable products are included
182
+ - the destination is normalized from shipping or billing data
183
+ - matching `tax_rates` are loaded
184
+ - tax lines are calculated and stored in `orders.tax_details`
185
+
186
+ ### Automatic mode
187
+
188
+ Automatic mode defers final tax calculation to Stripe Tax.
189
+
190
+ In this mode:
191
+
192
+ - checkout still records a tax intent in `orders.tax_details`
193
+ - the Stripe provider marks product and shipping tax codes on line items
194
+ - the webhook resync step replaces provisional tax data with finalized Stripe
195
+ checkout data
196
+
197
+ ## Shipping Zones and Rate Resolution
198
+
199
+ Shipping is backed by:
200
+
201
+ - `shipping_zones`
202
+ - `shipping_zone_locations`
203
+ - `shipping_zone_methods`
204
+
205
+ Each method stores:
206
+
207
+ - base amount and currency
208
+ - localized names
209
+ - per-currency amount maps and threshold maps
210
+ - `currency_pricing_mode` of `auto` or `manual`
211
+
212
+ ### Current resolver behavior
213
+
214
+ `libs/ecommerce/src/lib/shipping/resolver.ts` currently:
215
+
216
+ 1. loads active currencies
217
+ 2. queries zone locations by destination country
218
+ 3. prefers a state match when one exists
219
+ 4. otherwise prefers a country-wide match
220
+ 5. otherwise falls back to the first zone by `priority_order`
221
+ 6. filters methods by the cart total and minimum threshold
222
+ 7. converts method prices into the shopper currency
223
+ 8. returns only the cheapest valid method
224
+
225
+ Important implementation detail: `shipping_zone_locations.postal_code` exists in
226
+ schema, but the current resolver does not yet use postal code matching during
227
+ runtime resolution.
228
+
229
+ The storefront calls this through
230
+ `libs/ecommerce/src/lib/server-actions/shipping-actions.ts`.
231
+
232
+ ## Stripe Integration
233
+
234
+ Stripe is the current payment flow for physical products.
235
+
236
+ ### Checkout flow
237
+
238
+ `app/api/checkout/route.ts`:
239
+
240
+ - verifies the ecommerce package is active
241
+ - rejects carts without provider-aware items
242
+ - rejects mixed-provider carts
243
+ - requires a billing address
244
+ - resolves the provider through `getPaymentProvider()`
245
+
246
+ `StripeProvider.createCheckoutSession()` then:
247
+
248
+ - loads currencies and store settings
249
+ - validates products and variants against the database
250
+ - validates inventory before session creation when quantity tracking is enabled
251
+ - resolves shipping cost from the selected shipping method
252
+ - calculates tax in manual or automatic mode
253
+ - upserts a Stripe customer when an email is available
254
+ - inserts a pending `orders` row plus `order_items`
255
+ - stores currency, subtotal, shipping, tax, and exchange-rate data
256
+ - creates the Stripe Checkout Session and stores `stripe_session_id`
257
+
258
+ ### Webhook flow
259
+
260
+ `app/api/webhooks/stripe/route.ts` passes the raw body to
261
+ `handleStripeWebhook()`.
262
+
263
+ On `checkout.session.completed`, the sync layer:
264
+
265
+ - reloads the session with tax breakdown details
266
+ - finds the existing order
267
+ - stores payment intent, customer details, and finalized totals
268
+ - normalizes tax details from Stripe
269
+ - updates saved customer addresses
270
+ - assigns invoice metadata
271
+ - applies inventory deduction
272
+
273
+ ## Freemius Licensing and Product Sync
274
+
275
+ Freemius currently handles digital-product checkout and product synchronization.
276
+
277
+ ### Checkout behavior
278
+
279
+ `FreemiusProvider.createCheckoutSession()`:
280
+
281
+ - only allows one item per checkout
282
+ - loads the product from Supabase
283
+ - requires `freemius_product_id` and `freemius_plan_id`
284
+ - resolves pricing in the chosen currency
285
+ - inserts a pending order and order item
286
+ - optionally syncs default addresses and profile fields for the current user
287
+ - builds a Freemius checkout URL, including sandbox parameters when enabled
288
+
289
+ Supported credential sources include:
290
+
291
+ - product-scoped JSON map
292
+ - single-product sandbox overrides
293
+ - single-product env vars
294
+ - legacy shared env vars
295
+
296
+ ### Product sync
297
+
298
+ `syncFreemiusProductsToSupabase()` and `syncSingleFreemiusProduct()`:
299
+
300
+ - call the Freemius API with signed requests
301
+ - fetch plugins, plans, and pricing
302
+ - upsert digital products into `products`
303
+ - upsert related `freemius_plans` and `freemius_pricing`
304
+
305
+ These flows are surfaced in the CMS product actions and the sandbox reset route.
306
+
307
+ ### Current webhook limitation
308
+
309
+ `app/api/webhooks/freemius/route.ts` currently verifies the webhook signature
310
+ and acknowledges selected event types, but it does not yet reconcile license or
311
+ order state back into the local database.
312
+
313
+ ## Inventory Management and Fulfillment
314
+
315
+ Inventory behavior is controlled by `trackQuantities`.
316
+
317
+ When tracking is enabled:
318
+
319
+ - checkout validates requested quantity against `inventory_items`
320
+ - if a SKU is not yet cached there, product or variant stock fields are used as
321
+ fallback
322
+ - a paid order triggers `apply_order_inventory_deduction()`
323
+
324
+ The deduction flow is resilient:
325
+
326
+ - first it calls the database RPC `apply_order_inventory_deduction`
327
+ - if that fails, it falls back to a direct SQL reconciliation path using
328
+ `POSTGRES_URL` or `DATABASE_URL`
329
+
330
+ Order statuses currently supported in code are:
331
+
332
+ - `pending`
333
+ - `paid`
334
+ - `shipped`
335
+ - `cancelled`
336
+ - `refunded`
337
+
338
+ Manual CMS order status changes also trigger invoice assignment and inventory
339
+ deduction when an order is moved to `paid`.
340
+
341
+ ## Invoice and Order Presentation
342
+
343
+ The order/invoice layer includes:
344
+
345
+ - stable invoice numbering through database functions
346
+ - `invoice_settings` in `site_settings`
347
+ - printable invoice presentation data via `invoice-server.ts`
348
+ - UI components such as `InvoiceDocument` and `InvoiceViewerShell`
349
+ - customer order history and invoice access through `customer-orders.ts`
350
+
351
+ ## CMS Commerce Surfaces
352
+
353
+ The active ecommerce CMS surface includes:
354
+
355
+ - product list, create, edit, media, attribute, and variation management
356
+ - inventory management
357
+ - orders list and detail management
358
+ - shipping zones and shipping rate management
359
+ - payment-provider enablement
360
+ - tax settings and manual tax-rate management
361
+ - currency settings under `/cms/settings/currencies`
362
+
363
+ The CMS shell only exposes these store sections when the ecommerce package is
364
+ reported as active.