@01.software/init 0.9.2 → 0.10.1

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 (114) hide show
  1. package/dist/ai-docs.d.ts +13 -0
  2. package/dist/browser-auth-CJDrpp5T.d.ts +11 -0
  3. package/dist/{chunk-UA7WNT2F.js → chunk-4LHYICUL.js} +1 -1
  4. package/dist/chunk-4LHYICUL.js.map +1 -0
  5. package/dist/{chunk-R4GGO33X.js → chunk-NJ4X7VNK.js} +1 -1
  6. package/dist/{chunk-R4GGO33X.js.map → chunk-NJ4X7VNK.js.map} +1 -1
  7. package/dist/chunk-STM4DKVZ.js +183 -0
  8. package/dist/chunk-STM4DKVZ.js.map +1 -0
  9. package/dist/{chunk-ENQSB4OF.js → chunk-WDWJ73KP.js} +40 -214
  10. package/dist/chunk-WDWJ73KP.js.map +1 -0
  11. package/dist/create-app-templates/ecommerce/AGENTS.md +88 -0
  12. package/dist/create-app-templates/ecommerce/CHANGELOG.md +48 -0
  13. package/dist/create-app-templates/ecommerce/CLAUDE.md +1 -0
  14. package/dist/create-app-templates/ecommerce/README.md +154 -0
  15. package/dist/create-app-templates/ecommerce/app/api/auth/login/route.ts +30 -0
  16. package/dist/create-app-templates/ecommerce/app/api/auth/logout/route.ts +18 -0
  17. package/dist/create-app-templates/ecommerce/app/api/auth/register/route.ts +41 -0
  18. package/dist/create-app-templates/ecommerce/app/api/cart/clear/route.ts +12 -0
  19. package/dist/create-app-templates/ecommerce/app/api/cart/items/route.ts +45 -0
  20. package/dist/create-app-templates/ecommerce/app/api/cart/route.ts +14 -0
  21. package/dist/create-app-templates/ecommerce/app/api/checkout/payment-return/route.ts +86 -0
  22. package/dist/create-app-templates/ecommerce/app/api/checkout/reconcile/route.ts +50 -0
  23. package/dist/create-app-templates/ecommerce/app/api/checkout/route.ts +41 -0
  24. package/dist/create-app-templates/ecommerce/app/cart/page.tsx +10 -0
  25. package/dist/create-app-templates/ecommerce/app/checkout/page.tsx +10 -0
  26. package/dist/create-app-templates/ecommerce/app/checkout/success/page.tsx +34 -0
  27. package/dist/create-app-templates/ecommerce/app/favicon.ico +0 -0
  28. package/dist/create-app-templates/ecommerce/app/globals.css +67 -0
  29. package/dist/create-app-templates/ecommerce/app/layout.tsx +23 -0
  30. package/dist/create-app-templates/ecommerce/app/login/page.tsx +11 -0
  31. package/dist/create-app-templates/ecommerce/app/page.tsx +5 -0
  32. package/dist/create-app-templates/ecommerce/app/products/[slug]/page.tsx +46 -0
  33. package/dist/create-app-templates/ecommerce/app/products/page.tsx +45 -0
  34. package/dist/create-app-templates/ecommerce/app/register/page.tsx +11 -0
  35. package/dist/create-app-templates/ecommerce/app/webhook/payment/route.ts +20 -0
  36. package/dist/create-app-templates/ecommerce/app-config.ts +54 -0
  37. package/dist/create-app-templates/ecommerce/components/auth/auth-form.tsx +109 -0
  38. package/dist/create-app-templates/ecommerce/components/cart/cart-content.tsx +129 -0
  39. package/dist/create-app-templates/ecommerce/components/checkout/checkout-form.tsx +307 -0
  40. package/dist/create-app-templates/ecommerce/components/checkout/checkout-reconcile.tsx +78 -0
  41. package/dist/create-app-templates/ecommerce/components/layout/account-nav.tsx +48 -0
  42. package/dist/create-app-templates/ecommerce/components/layout/account-slot.tsx +12 -0
  43. package/dist/create-app-templates/ecommerce/components/layout/cart-link.tsx +13 -0
  44. package/dist/create-app-templates/ecommerce/components/layout/page-shell.tsx +11 -0
  45. package/dist/create-app-templates/ecommerce/components/layout/site-header.tsx +22 -0
  46. package/dist/create-app-templates/ecommerce/components/product/add-to-cart.tsx +116 -0
  47. package/dist/create-app-templates/ecommerce/components/product/product-card.tsx +50 -0
  48. package/dist/create-app-templates/ecommerce/components/product/product-gallery.tsx +39 -0
  49. package/dist/create-app-templates/ecommerce/data/mock-catalog.json +173 -0
  50. package/dist/create-app-templates/ecommerce/eslint.config.mjs +18 -0
  51. package/dist/create-app-templates/ecommerce/lib/cart/cookie.ts +40 -0
  52. package/dist/create-app-templates/ecommerce/lib/cart/normalize.ts +32 -0
  53. package/dist/create-app-templates/ecommerce/lib/cart/parse-cart-request.ts +56 -0
  54. package/dist/create-app-templates/ecommerce/lib/cart/route-helpers.ts +17 -0
  55. package/dist/create-app-templates/ecommerce/lib/cart/select-provider.ts +44 -0
  56. package/dist/create-app-templates/ecommerce/lib/cart/server-cart.ts +135 -0
  57. package/dist/create-app-templates/ecommerce/lib/cart/sync-on-login.server.ts +34 -0
  58. package/dist/create-app-templates/ecommerce/lib/cart/use-cart.tsx +151 -0
  59. package/dist/create-app-templates/ecommerce/lib/checkout/checkout-errors.ts +22 -0
  60. package/dist/create-app-templates/ecommerce/lib/checkout/checkout-provider.ts +28 -0
  61. package/dist/create-app-templates/ecommerce/lib/checkout/parse-checkout-payload.ts +86 -0
  62. package/dist/create-app-templates/ecommerce/lib/checkout/start-checkout.ts +73 -0
  63. package/dist/create-app-templates/ecommerce/lib/checkout/types.ts +6 -0
  64. package/dist/create-app-templates/ecommerce/lib/commerce/adapters/mock.ts +346 -0
  65. package/dist/create-app-templates/ecommerce/lib/commerce/adapters/software-mappers.ts +312 -0
  66. package/dist/create-app-templates/ecommerce/lib/commerce/adapters/software.ts +930 -0
  67. package/dist/create-app-templates/ecommerce/lib/commerce/product-summary.ts +37 -0
  68. package/dist/create-app-templates/ecommerce/lib/commerce/provider.server.ts +60 -0
  69. package/dist/create-app-templates/ecommerce/lib/commerce/provider.ts +96 -0
  70. package/dist/create-app-templates/ecommerce/lib/commerce/stock.ts +37 -0
  71. package/dist/create-app-templates/ecommerce/lib/commerce/types.ts +208 -0
  72. package/dist/create-app-templates/ecommerce/lib/commerce/variant-selection.ts +23 -0
  73. package/dist/create-app-templates/ecommerce/lib/customer/auth-actions.ts +131 -0
  74. package/dist/create-app-templates/ecommerce/lib/customer/cart-sync.ts +44 -0
  75. package/dist/create-app-templates/ecommerce/lib/customer/client.server.ts +109 -0
  76. package/dist/create-app-templates/ecommerce/lib/customer/current-customer.ts +15 -0
  77. package/dist/create-app-templates/ecommerce/lib/customer/route-guard.ts +58 -0
  78. package/dist/create-app-templates/ecommerce/lib/customer/route-helpers.ts +75 -0
  79. package/dist/create-app-templates/ecommerce/lib/customer/session.ts +108 -0
  80. package/dist/create-app-templates/ecommerce/lib/format.ts +7 -0
  81. package/dist/create-app-templates/ecommerce/lib/payment/adapters/mock.ts +84 -0
  82. package/dist/create-app-templates/ecommerce/lib/payment/adapters/portone.ts +254 -0
  83. package/dist/create-app-templates/ecommerce/lib/payment/adapters/tosspayments.ts +287 -0
  84. package/dist/create-app-templates/ecommerce/lib/payment/amount-gate.ts +86 -0
  85. package/dist/create-app-templates/ecommerce/lib/payment/provider.server.ts +51 -0
  86. package/dist/create-app-templates/ecommerce/lib/payment/provider.ts +18 -0
  87. package/dist/create-app-templates/ecommerce/lib/payment/sync-order-payment.ts +96 -0
  88. package/dist/create-app-templates/ecommerce/lib/payment/types.ts +71 -0
  89. package/dist/create-app-templates/ecommerce/lib/server-only-guard.ts +20 -0
  90. package/dist/create-app-templates/ecommerce/next-env.d.ts +6 -0
  91. package/dist/create-app-templates/ecommerce/next.config.ts +16 -0
  92. package/dist/create-app-templates/ecommerce/package.json +33 -0
  93. package/dist/create-app-templates/ecommerce/postcss.config.mjs +7 -0
  94. package/dist/create-app-templates/ecommerce/tests/customer-auth.test.ts +263 -0
  95. package/dist/create-app-templates/ecommerce/tests/customer-cart.test.ts +401 -0
  96. package/dist/create-app-templates/ecommerce/tests/domain.test.ts +1632 -0
  97. package/dist/create-app-templates/ecommerce/tsconfig.json +35 -0
  98. package/dist/create-app-templates/registry.json +66 -0
  99. package/dist/create-app.d.ts +40 -0
  100. package/dist/create-app.js +652 -0
  101. package/dist/create-app.js.map +1 -0
  102. package/dist/detect-Bjxp9wcS.d.ts +13 -0
  103. package/dist/file-ops.d.ts +21 -0
  104. package/dist/file-ops.js +1 -1
  105. package/dist/index.d.ts +2 -0
  106. package/dist/index.js +4 -3
  107. package/dist/index.js.map +1 -1
  108. package/dist/init.d.ts +40 -0
  109. package/dist/init.js +4 -3
  110. package/dist/templates.d.ts +27 -0
  111. package/dist/templates.js +1 -1
  112. package/package.json +18 -3
  113. package/dist/chunk-ENQSB4OF.js.map +0 -1
  114. package/dist/chunk-UA7WNT2F.js.map +0 -1
@@ -0,0 +1,48 @@
1
+ # ecommerce
2
+
3
+ ## 0.1.2
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies [81ef105]
8
+ - Updated dependencies [9789528]
9
+ - Updated dependencies [9b7afab]
10
+ - Updated dependencies [db8bd4b]
11
+ - Updated dependencies [b95f6ac]
12
+ - Updated dependencies [c60ec7f]
13
+ - Updated dependencies [6c71a71]
14
+ - Updated dependencies [3aa6239]
15
+ - Updated dependencies [5d7b26f]
16
+ - Updated dependencies [294b7d1]
17
+ - Updated dependencies [f87fe09]
18
+ - Updated dependencies [4362b14]
19
+ - @01.software/sdk@0.43.0
20
+
21
+ ## 0.1.1
22
+
23
+ ### Patch Changes
24
+
25
+ - Updated dependencies [b011902]
26
+ - Updated dependencies [9d1b632]
27
+ - Updated dependencies [d28ad57]
28
+ - Updated dependencies [9cb8a41]
29
+ - Updated dependencies [a36d724]
30
+ - Updated dependencies [426c3ed]
31
+ - Updated dependencies [9261a15]
32
+ - Updated dependencies [8d8e700]
33
+ - Updated dependencies [9f3186c]
34
+ - Updated dependencies [a2fd60b]
35
+ - Updated dependencies [b1bdd49]
36
+ - Updated dependencies [e3b16a5]
37
+ - Updated dependencies [27a6502]
38
+ - Updated dependencies [5aa136f]
39
+ - Updated dependencies [87da012]
40
+ - Updated dependencies [3f261a6]
41
+ - Updated dependencies [17afb8e]
42
+ - Updated dependencies [a968dfd]
43
+ - Updated dependencies [4a13b7b]
44
+ - Updated dependencies [1d8ee45]
45
+ - Updated dependencies [923b0a2]
46
+ - Updated dependencies [2986732]
47
+ - Updated dependencies [8ded73e]
48
+ - @01.software/sdk@0.42.0
@@ -0,0 +1 @@
1
+ @AGENTS.md
@@ -0,0 +1,154 @@
1
+ # Ecommerce Template
2
+
3
+ Minimal Next.js commerce storefront template with a pluggable payment-adapter
4
+ seam. A single source scaffolds either supported payment provider; the active
5
+ provider for a generated app is recorded in `app-config.ts` and its keys live in
6
+ `.env.local.example`.
7
+
8
+ ## What is included
9
+
10
+ - App Router storefront routes for product listing, product detail, cart,
11
+ checkout, and payment success.
12
+ - Adapter boundaries under `lib/commerce` and `lib/payment`.
13
+ - Demo commerce and payment adapters backed by `data/mock-catalog.json`.
14
+ - Automatic `@01.software/sdk/server` commerce adapter when 01.software SDK
15
+ credentials are present.
16
+ - A payment-adapter registry (`lib/payment/provider.server.ts`) that selects a
17
+ provider from environment credentials, with first-class adapters for:
18
+ - **PortOne** — `@portone/browser-sdk/v2` on the client, `@portone/server-sdk`
19
+ on the server.
20
+ - **TossPayments** — the TossPayments browser SDK on the client, the
21
+ TossPayments payment REST API on the server.
22
+ - A theming seam: a CSS-variable token layer in `app/globals.css` plus Tailwind.
23
+ - Server-side order pricing, stock checks, and payment sync invariants.
24
+ - Server-authoritative cart operations through the template's own route handlers,
25
+ backed by `commerce.cart.*`.
26
+ - An HttpOnly cart cookie containing only the unguessable `cartToken`; client
27
+ JavaScript receives rendered cart views but never owns the authoritative cart
28
+ or token, and no cart contents are persisted in localStorage.
29
+ - Checkout through `orders.checkout({ cartId })`, with orders resolved after
30
+ payment by `orders.getByPaymentId`.
31
+
32
+ ## Scaffolding vs. this in-repo source
33
+
34
+ This directory is the single in-repo source and intentionally ships **both**
35
+ payment adapters so it builds and tests with every provider present.
36
+ `create-01-software-app` copies it, prunes the unused payment adapter (and its
37
+ dependency), generates `app-config.ts` and `.env.local.example` from
38
+ `templates/registry.json`, and pins the SDK version — producing a self-contained
39
+ single-provider app.
40
+
41
+ To add a new payment provider, see **Add a payment provider** below.
42
+
43
+ ## Run locally
44
+
45
+ ```bash
46
+ pnpm install
47
+ pnpm dev
48
+ ```
49
+
50
+ Without SDK or payment credentials, the app uses demo providers:
51
+
52
+ ```bash
53
+ # 01.software keys omitted -> demo catalog plus in-memory cart/checkout
54
+ # payment keys omitted -> local demo payment completion
55
+ ```
56
+
57
+ The mock adapter emulates the server cart and checkout flow in memory for
58
+ zero-backend demos. Redirect and success routes resolve the same in-process
59
+ checkout model without writing a local order index.
60
+
61
+ To use the Console ecommerce SDK adapter, set both SDK keys:
62
+
63
+ ```bash
64
+ NEXT_PUBLIC_SOFTWARE_PUBLISHABLE_KEY=pk_...
65
+ SOFTWARE_SECRET_KEY=sk_...
66
+ SOFTWARE_API_URL=https://your-console-origin.example
67
+ SOFTWARE_SHIPPING_AMOUNT=3000
68
+ SOFTWARE_FREE_SHIPPING_ABOVE_AMOUNT=100000
69
+ ```
70
+
71
+ `SOFTWARE_API_URL`, `SOFTWARE_SHIPPING_AMOUNT`, and
72
+ `SOFTWARE_FREE_SHIPPING_ABOVE_AMOUNT` are optional. The SDK adapter uses Console
73
+ cart and order APIs for the full server-authoritative flow: route handlers call
74
+ `commerce.cart.*`, checkout calls `orders.checkout({ cartId })`, and
75
+ return/webhook/success reconciliation resolves orders by payment id through
76
+ `orders.getByPaymentId`.
77
+
78
+ ## Payment provider keys
79
+
80
+ ### PortOne
81
+
82
+ ```bash
83
+ PORTONE_API_SECRET=...
84
+ NEXT_PUBLIC_PORTONE_STORE_ID=store_...
85
+ NEXT_PUBLIC_PORTONE_CHANNEL_KEY=channel-key-...
86
+ NEXT_PUBLIC_PORTONE_PAY_METHOD=CARD
87
+ PORTONE_WEBHOOK_SECRET=...
88
+ ```
89
+
90
+ `NEXT_PUBLIC_PORTONE_PAY_METHOD` is optional and defaults to `CARD`.
91
+ `PORTONE_WEBHOOK_SECRET` is required in production. For local development without
92
+ signed webhooks, set `PORTONE_ALLOW_UNSIGNED_WEBHOOKS=true`. When the secret is
93
+ present, the webhook route verifies PortOne webhook signatures through
94
+ `@portone/server-sdk`.
95
+
96
+ ### TossPayments
97
+
98
+ ```bash
99
+ TOSSPAYMENTS_SECRET_KEY=test_sk_...
100
+ NEXT_PUBLIC_TOSSPAYMENTS_CLIENT_KEY=test_ck_...
101
+ NEXT_PUBLIC_TOSSPAYMENTS_CUSTOMER_KEY=customer_...
102
+ ```
103
+
104
+ `NEXT_PUBLIC_TOSSPAYMENTS_CUSTOMER_KEY` is optional. If omitted, the browser SDK
105
+ uses TossPayments' anonymous customer key. TossPayments redirects back with
106
+ `paymentKey`, `orderId`, and `amount`; `/api/checkout/payment-return` re-fetches
107
+ the provider payment, rejects a tampered redirect amount, captures with the
108
+ PG-verified amount, then redirects to the success page. Success-page
109
+ reconciliation re-fetches the provider payment again and places the Console
110
+ checkout through `orders.confirmPayment`, where the open checkout quote remains
111
+ the authoritative amount check.
112
+ `TOSSPAYMENTS_API_BASE_URL` is optional and defaults to
113
+ `https://api.tosspayments.com/v1`.
114
+
115
+ ## Add a payment provider
116
+
117
+ 1. Add `lib/payment/adapters/<pg>.ts` implementing the `PaymentProvider` port
118
+ and exporting `create<Pg>Provider` plus `has<Pg>Credentials`.
119
+ 2. Register it in `lib/payment/provider.server.ts` inside a matching pair of
120
+ `scaffold:provider:<id>` markers (import + registry entry).
121
+ 3. Add a `ClientPaymentRequest` union case in `lib/payment/types.ts`.
122
+ 4. Add a client branch (and its `scaffold:provider:<id>` markers) in
123
+ `components/checkout/checkout-form.tsx`.
124
+ 5. Add the provider to `templates/registry.json` so the scaffolder offers it.
125
+
126
+ The template depends on the published `@01.software/sdk` package so it can be
127
+ copied out of this monorepo and installed as a standalone Next.js app.
128
+
129
+ The starter `software` product listing uses a simple products query followed by
130
+ per-product detail lookups so the UI can share one normalized product-detail
131
+ shape. It isolates individual detail failures with `Promise.allSettled`. For a
132
+ larger storefront, replace that listing path with the Console tenant's preferred
133
+ catalog/listing API shape.
134
+
135
+ ## Verify
136
+
137
+ ```bash
138
+ pnpm test
139
+ pnpm check-types
140
+ pnpm lint
141
+ pnpm build
142
+ ```
143
+
144
+ ## Important boundary
145
+
146
+ The browser never owns the authoritative cart or cart capability tokens. It
147
+ talks to the template's cart route handlers, which keep the cart
148
+ server-authoritative and store only an HttpOnly cart cookie in the browser.
149
+ Client components may keep rendered cart views in memory, but cart mutations and
150
+ checkout always return to the server cart. Checkout attaches customer and
151
+ shipping details to that server cart, converts it with
152
+ `orders.checkout({ cartId })`, and then synchronizes payment state. After
153
+ payment, order lookup uses `orders.getByPaymentId`, so the template does not
154
+ need a file-backed order index.
@@ -0,0 +1,30 @@
1
+ import { syncActiveCartCookieOnLogin } from "@/lib/cart/sync-on-login.server.ts";
2
+ import { login } from "@/lib/customer/auth-actions";
3
+ import { assertSameOriginJson } from "@/lib/customer/route-guard.ts";
4
+ import { authErrorResponse, parseLogin } from "@/lib/customer/route-helpers.ts";
5
+ import { setSessionToken } from "@/lib/customer/session";
6
+
7
+ // Log in: delegate to `client.customer.auth.login`, then stamp the customer JWT
8
+ // into the HttpOnly session cookie and slave the active-cart cookie to the
9
+ // customer's server-owned cart (union a pre-login guest cart, else load theirs).
10
+ // CSRF-guarded (same-origin + JSON).
11
+ export async function POST(request: Request) {
12
+ try {
13
+ assertSameOriginJson(request);
14
+ const input = parseLogin(await request.json());
15
+ const result = await login(input);
16
+ if (!result.ok) {
17
+ return Response.json(
18
+ { code: "login_failed", message: result.message },
19
+ { status: result.status },
20
+ );
21
+ }
22
+ if (result.token) {
23
+ await setSessionToken(result.token);
24
+ await syncActiveCartCookieOnLogin(result.token);
25
+ }
26
+ return Response.json({ customer: result.customer });
27
+ } catch (error) {
28
+ return authErrorResponse(error);
29
+ }
30
+ }
@@ -0,0 +1,18 @@
1
+ import { assertSameOriginJson } from "@/lib/customer/route-guard.ts";
2
+ import { authErrorResponse } from "@/lib/customer/route-helpers.ts";
3
+ import { clearSessionAndCart } from "@/lib/customer/session";
4
+
5
+ // Log out: clear the HttpOnly session cookie AND the active-cart cookie together.
6
+ // The customer JWT is stateless, so dropping the cookie ends the session
7
+ // client-side; the cart-token handle is slaved to the session, so it is dropped
8
+ // atomically too (no later session inherits this identity's cart). CSRF-guarded
9
+ // (same-origin + JSON) so a cross-site request cannot force-logout the customer.
10
+ export async function POST(request: Request) {
11
+ try {
12
+ assertSameOriginJson(request);
13
+ await clearSessionAndCart();
14
+ return Response.json({ ok: true });
15
+ } catch (error) {
16
+ return authErrorResponse(error);
17
+ }
18
+ }
@@ -0,0 +1,41 @@
1
+ import { syncActiveCartCookieOnLogin } from "@/lib/cart/sync-on-login.server.ts";
2
+ import { login, register } from "@/lib/customer/auth-actions";
3
+ import { assertSameOriginJson } from "@/lib/customer/route-guard.ts";
4
+ import {
5
+ authErrorResponse,
6
+ parseRegister,
7
+ } from "@/lib/customer/route-helpers.ts";
8
+ import { setSessionToken } from "@/lib/customer/session";
9
+
10
+ // Register: delegate to `client.customer.auth.register`, then log the new
11
+ // customer in to mint a session JWT (register returns no token) and stamp it
12
+ // into the HttpOnly session cookie. CSRF-guarded (same-origin + JSON).
13
+ export async function POST(request: Request) {
14
+ try {
15
+ assertSameOriginJson(request);
16
+ const input = parseRegister(await request.json());
17
+ const created = await register(input);
18
+ if (!created.ok) {
19
+ return Response.json(
20
+ { code: "register_failed", message: created.message },
21
+ { status: created.status },
22
+ );
23
+ }
24
+
25
+ // Registration may require verification before login succeeds; if the
26
+ // follow-up login fails, the account still exists — report success without a
27
+ // session so the UI can route the customer to the login page.
28
+ const session = await login({
29
+ email: input.email,
30
+ password: input.password,
31
+ });
32
+ if (session.ok && session.token) {
33
+ await setSessionToken(session.token);
34
+ await syncActiveCartCookieOnLogin(session.token);
35
+ return Response.json({ customer: session.customer });
36
+ }
37
+ return Response.json({ customer: created.customer, requiresLogin: true });
38
+ } catch (error) {
39
+ return authErrorResponse(error);
40
+ }
41
+ }
@@ -0,0 +1,12 @@
1
+ import { clearActiveCart } from "@/lib/cart/server-cart";
2
+ import { cartErrorResponse } from "@/lib/cart/route-helpers.ts";
3
+
4
+ // Empty the owned cart (no-op when none is owned).
5
+ export async function POST() {
6
+ try {
7
+ const cart = await clearActiveCart();
8
+ return Response.json({ cart });
9
+ } catch (error) {
10
+ return cartErrorResponse(error);
11
+ }
12
+ }
@@ -0,0 +1,45 @@
1
+ import {
2
+ addItemToCart,
3
+ removeCartItemFromCart,
4
+ updateCartItemQuantity,
5
+ } from "@/lib/cart/server-cart";
6
+ import {
7
+ parseAddItem,
8
+ parseRemoveItem,
9
+ parseUpdateItem,
10
+ } from "@/lib/cart/parse-cart-request.ts";
11
+ import { cartErrorResponse } from "@/lib/cart/route-helpers.ts";
12
+
13
+ // Add a line. Creates + stamps the guest cart cookie when none is owned yet.
14
+ export async function POST(request: Request) {
15
+ try {
16
+ const cart = await addItemToCart(parseAddItem(await request.json()));
17
+ return Response.json({ cart });
18
+ } catch (error) {
19
+ return cartErrorResponse(error);
20
+ }
21
+ }
22
+
23
+ // Update a line quantity by its cart-item handle.
24
+ export async function PATCH(request: Request) {
25
+ try {
26
+ const cart = await updateCartItemQuantity(
27
+ parseUpdateItem(await request.json()),
28
+ );
29
+ return Response.json({ cart });
30
+ } catch (error) {
31
+ return cartErrorResponse(error);
32
+ }
33
+ }
34
+
35
+ // Remove a line by its cart-item handle.
36
+ export async function DELETE(request: Request) {
37
+ try {
38
+ const cart = await removeCartItemFromCart(
39
+ parseRemoveItem(await request.json()),
40
+ );
41
+ return Response.json({ cart });
42
+ } catch (error) {
43
+ return cartErrorResponse(error);
44
+ }
45
+ }
@@ -0,0 +1,14 @@
1
+ import { getCartFromCookie } from "@/lib/cart/server-cart";
2
+ import { cartErrorResponse } from "@/lib/cart/route-helpers.ts";
3
+
4
+ // The cart lives server-side (`commerce.cart.*`); this returns the current
5
+ // guest cart resolved from the HttpOnly ownership cookie, or `{ cart: null }`
6
+ // when none is owned yet.
7
+ export async function GET() {
8
+ try {
9
+ const cart = await getCartFromCookie();
10
+ return Response.json({ cart });
11
+ } catch (error) {
12
+ return cartErrorResponse(error);
13
+ }
14
+ }
@@ -0,0 +1,86 @@
1
+ import { redirect } from "next/navigation";
2
+ import {
3
+ AmountMismatchError,
4
+ assertAmountEquals,
5
+ } from "@/lib/payment/amount-gate";
6
+ import { getPaymentProvider } from "@/lib/payment/provider.server";
7
+
8
+ /**
9
+ * PG redirect return. For providers that require a server-side capture (e.g.
10
+ * Toss `/payments/confirm` with the returned `paymentKey`), this runs the shared
11
+ * amount-equality gate, captures, then hands off to the success page which
12
+ * reconciles the order via `syncOrderPayment` (server re-fetch →
13
+ * `orders.confirmPayment`).
14
+ *
15
+ * The redirect query is fully client-tamperable, so its `amount` is NEVER
16
+ * forwarded to the capture. We re-fetch the payment from the PG, assert the
17
+ * query amount (when present) equals the re-fetched amount (#1544 / I6 amount
18
+ * gate), and then capture with the PG-verified amount. The order is ultimately
19
+ * placed against the Console quote, never this query value.
20
+ */
21
+ export async function GET(request: Request) {
22
+ const { searchParams } = new URL(request.url);
23
+ const paymentId =
24
+ searchParams.get("paymentId") ?? searchParams.get("orderId");
25
+ const orderNumber = searchParams.get("orderNumber") ?? undefined;
26
+ const providerPaymentId = searchParams.get("paymentKey") ?? undefined;
27
+ const amountValue = searchParams.get("amount");
28
+ const clientAmount = amountValue ? Number(amountValue) : undefined;
29
+
30
+ if (!paymentId) {
31
+ return Response.json({ code: "missing_payment_id" }, { status: 400 });
32
+ }
33
+ if (amountValue && !Number.isFinite(clientAmount)) {
34
+ return Response.json({ code: "invalid_amount" }, { status: 400 });
35
+ }
36
+
37
+ // Capture is required only when the provider returns a capture token
38
+ // (`paymentKey`) or a client amount to validate. Providers without a
39
+ // return-side capture (PortOne, mock) fall straight through to the success
40
+ // page, where `syncOrderPayment` re-fetches, gates, and places.
41
+ if (providerPaymentId || clientAmount != null) {
42
+ try {
43
+ const provider = getPaymentProvider();
44
+ // Re-fetch the authoritative amount from the PG — the trust anchor.
45
+ const verified = await provider.getPayment(paymentId);
46
+ if (!verified) {
47
+ return Response.json(
48
+ { code: "payment_not_found" },
49
+ { status: 404 },
50
+ );
51
+ }
52
+ // Amount gate: a tampered redirect query is rejected before any capture.
53
+ if (clientAmount != null) {
54
+ assertAmountEquals({
55
+ context: `return:${provider.provider}:${paymentId}`,
56
+ expected: verified.amount,
57
+ actual: clientAmount,
58
+ });
59
+ }
60
+ // Capture with the PG-verified amount, never the client query value.
61
+ await provider.confirmPayment({
62
+ paymentId,
63
+ providerPaymentId,
64
+ amount: verified.amount,
65
+ });
66
+ } catch (error) {
67
+ if (error instanceof AmountMismatchError) {
68
+ return Response.json(
69
+ { code: "amount_mismatch" },
70
+ { status: 422 },
71
+ );
72
+ }
73
+ const message =
74
+ error instanceof Error ? error.message : "Payment confirmation failed";
75
+ return Response.json(
76
+ { code: "payment_confirmation_failed", message },
77
+ { status: 422 },
78
+ );
79
+ }
80
+ }
81
+
82
+ const params = new URLSearchParams({ paymentId });
83
+ if (orderNumber) params.set("orderNumber", orderNumber);
84
+
85
+ redirect(`/checkout/success?${params.toString()}`);
86
+ }
@@ -0,0 +1,50 @@
1
+ import { getCommerceProvider } from "@/lib/commerce/provider.server";
2
+ import { AmountMismatchError } from "@/lib/payment/amount-gate";
3
+ import { getPaymentProvider } from "@/lib/payment/provider.server";
4
+ import { syncOrderPayment } from "@/lib/payment/sync-order-payment.ts";
5
+
6
+ /**
7
+ * Order reconciliation, moved off the success-page RSC render into a route
8
+ * handler (#1544 / I6). Mutating during Server Component render is unsound
9
+ * (renders may run more than once); the success page now renders a client
10
+ * trigger that POSTs here exactly once. `syncOrderPayment` is itself idempotent
11
+ * — the Console dedups by `pgPaymentId` / `providerEventId` — so a retry or a
12
+ * concurrent webhook still collapses to a single `paid` transition. The shared
13
+ * amount-equality gate runs inside `syncOrderPayment` on this success path.
14
+ */
15
+ export async function POST(request: Request) {
16
+ let body: { paymentId?: unknown; orderNumber?: unknown };
17
+ try {
18
+ body = (await request.json()) as typeof body;
19
+ } catch {
20
+ return Response.json({ code: "invalid_request" }, { status: 400 });
21
+ }
22
+
23
+ const paymentId =
24
+ typeof body.paymentId === "string" ? body.paymentId : undefined;
25
+ const orderNumber =
26
+ typeof body.orderNumber === "string" ? body.orderNumber : undefined;
27
+ if (!paymentId) {
28
+ return Response.json({ code: "missing_payment_id" }, { status: 400 });
29
+ }
30
+
31
+ try {
32
+ const result = await syncOrderPayment({
33
+ paymentId,
34
+ orderNumber,
35
+ commerceProvider: getCommerceProvider(),
36
+ paymentProvider: getPaymentProvider(),
37
+ });
38
+ return Response.json(result);
39
+ } catch (error) {
40
+ if (error instanceof AmountMismatchError) {
41
+ return Response.json({ code: "amount_mismatch" }, { status: 422 });
42
+ }
43
+ const message =
44
+ error instanceof Error ? error.message : "Reconciliation failed";
45
+ return Response.json(
46
+ { code: "reconciliation_failed", message },
47
+ { status: 422 },
48
+ );
49
+ }
50
+ }
@@ -0,0 +1,41 @@
1
+ import { getCheckoutCommerceProvider } from "@/lib/checkout/checkout-provider";
2
+ import {
3
+ getCheckoutErrorMessage,
4
+ getCheckoutErrorStatus,
5
+ } from "@/lib/checkout/checkout-errors";
6
+ import { startCheckout } from "@/lib/checkout/start-checkout.ts";
7
+ import {
8
+ dropCartCookie,
9
+ getCheckoutCartToken,
10
+ } from "@/lib/cart/server-cart";
11
+ import { getPaymentProvider } from "@/lib/payment/provider.server";
12
+
13
+ export async function POST(request: Request) {
14
+ const cartToken = await getCheckoutCartToken();
15
+ if (!cartToken) {
16
+ return Response.json(
17
+ { code: "cart_not_found", message: "No active cart to check out" },
18
+ { status: 409 },
19
+ );
20
+ }
21
+
22
+ try {
23
+ const result = await startCheckout({
24
+ cartToken,
25
+ payload: await request.json(),
26
+ commerceProvider: await getCheckoutCommerceProvider(),
27
+ paymentProvider: getPaymentProvider(),
28
+ });
29
+
30
+ // Checkout consumed the cart (Shopify cart→checkout boundary). Drop the
31
+ // ownership cookie so the next visit starts a fresh cart.
32
+ await dropCartCookie();
33
+
34
+ return Response.json(result);
35
+ } catch (error) {
36
+ const message = getCheckoutErrorMessage(error);
37
+ const status = getCheckoutErrorStatus(error);
38
+
39
+ return Response.json({ code: "checkout_failed", message }, { status });
40
+ }
41
+ }
@@ -0,0 +1,10 @@
1
+ import { CartContent } from "@/components/cart/cart-content";
2
+ import { PageShell } from "@/components/layout/page-shell";
3
+
4
+ export default function CartPage() {
5
+ return (
6
+ <PageShell>
7
+ <CartContent />
8
+ </PageShell>
9
+ );
10
+ }
@@ -0,0 +1,10 @@
1
+ import { CheckoutForm } from "@/components/checkout/checkout-form";
2
+ import { PageShell } from "@/components/layout/page-shell";
3
+
4
+ export default function CheckoutPage() {
5
+ return (
6
+ <PageShell>
7
+ <CheckoutForm />
8
+ </PageShell>
9
+ );
10
+ }
@@ -0,0 +1,34 @@
1
+ import { PageShell } from "@/components/layout/page-shell";
2
+ import { CheckoutReconcile } from "@/components/checkout/checkout-reconcile";
3
+
4
+ /**
5
+ * Payment return landing. Reconciliation is intentionally NOT performed during
6
+ * render — a Server Component render must be side-effect free and may run more
7
+ * than once. The client `CheckoutReconcile` trigger POSTs to the reconcile
8
+ * route handler once on mount instead (#1544 / I6).
9
+ */
10
+ export default async function CheckoutSuccessPage({
11
+ searchParams,
12
+ }: {
13
+ searchParams: Promise<{ paymentId?: string; orderNumber?: string }>;
14
+ }) {
15
+ const params = await searchParams;
16
+ const paymentId = params.paymentId;
17
+
18
+ if (!paymentId) {
19
+ return (
20
+ <PageShell>
21
+ <section>
22
+ <h1>Missing payment</h1>
23
+ <p>The return URL did not include a payment ID.</p>
24
+ </section>
25
+ </PageShell>
26
+ );
27
+ }
28
+
29
+ return (
30
+ <PageShell>
31
+ <CheckoutReconcile paymentId={paymentId} orderNumber={params.orderNumber} />
32
+ </PageShell>
33
+ );
34
+ }
@@ -0,0 +1,67 @@
1
+ @import "tailwindcss";
2
+
3
+ /*
4
+ * Theming seam.
5
+ *
6
+ * The CSS-variable token layer below is the rebrand surface: change a value in
7
+ * `:root` and every page that consumes the token re-skins. The `@theme inline`
8
+ * block exposes the same tokens to Tailwind utilities (e.g. `bg-background`,
9
+ * `text-brand`) so component styling can be added without leaving this layer.
10
+ * The storefront keeps its semantic-HTML / a11y baseline; styling is additive.
11
+ */
12
+ @theme inline {
13
+ --color-background: var(--background);
14
+ --color-foreground: var(--foreground);
15
+ --color-muted: var(--muted);
16
+ --color-muted-foreground: var(--muted-foreground);
17
+ --color-border: var(--border);
18
+ --color-brand: var(--brand);
19
+ --color-brand-foreground: var(--brand-foreground);
20
+ --font-sans: var(--font-sans);
21
+ }
22
+
23
+ :root {
24
+ /* Brand */
25
+ --brand: #1f2937;
26
+ --brand-foreground: #ffffff;
27
+
28
+ /* Surfaces */
29
+ --background: #ffffff;
30
+ --foreground: #111827;
31
+ --muted: #f3f4f6;
32
+ --muted-foreground: #6b7280;
33
+ --border: rgba(17, 24, 39, 0.12);
34
+
35
+ /* Type */
36
+ --font-sans:
37
+ ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue",
38
+ Arial, sans-serif;
39
+
40
+ /* Spacing rhythm shared by page sections */
41
+ --space-page: 1.5rem;
42
+ --measure: 72ch;
43
+ }
44
+
45
+ @media (prefers-color-scheme: dark) {
46
+ :root {
47
+ --brand: #e5e7eb;
48
+ --brand-foreground: #111827;
49
+ --background: #0b0f19;
50
+ --foreground: #f3f4f6;
51
+ --muted: #161b27;
52
+ --muted-foreground: #9ca3af;
53
+ --border: rgba(243, 244, 246, 0.14);
54
+ }
55
+ }
56
+
57
+ body {
58
+ background: var(--background);
59
+ color: var(--foreground);
60
+ font-family: var(--font-sans);
61
+ line-height: 1.5;
62
+ -webkit-font-smoothing: antialiased;
63
+ }
64
+
65
+ main {
66
+ padding: var(--space-page);
67
+ }