Skip to content

M9: Stripe Payments – t9.1–t9.6 (checkout, webhook, E2E)#13

Merged
eskobar95 merged 12 commits into
stagingfrom
task/m9-stripe
Feb 27, 2026
Merged

M9: Stripe Payments – t9.1–t9.6 (checkout, webhook, E2E)#13
eskobar95 merged 12 commits into
stagingfrom
task/m9-stripe

Conversation

@eskobar95
Copy link
Copy Markdown
Owner

@eskobar95 eskobar95 commented Feb 27, 2026

Summary

  • Stripe Payment Module (t9.1): Medusa config + env vars (STRIPE_API_KEY, STRIPE_WEBHOOK_SECRET, NEXT_PUBLIC_STRIPE_KEY).
  • Checkout + PaymentElement (t9.2–t9.3): Stripe i Denmark-regionen; PaymentElement i storefront; cart-based checkout med shipping method + order completion.
  • Webhook + E2E (t9.4–t9.5): Webhook-dokumentation for deployed miljø; fuldt E2E-flow med test-kort gennemført lokalt (order oprettes, charge.succeeded).
  • Spec (t9.6): Adyen erstattet med Stripe i spec-filer og openmemory.

Changes

  • apps/commerce: Stripe-modul, seed (region + fulfillment/shipping), STRIPE-SETUP.md, webhook-dokumentation.
  • apps/storefront: Cart server actions (lib/cart.ts), ProductPurchaseSection, CartItems, CheckoutWithStripe (cookie-cart, shipping, payment session), order-confirmation med order-id.
  • work/backlog: M9 tasks t9.1–t9.6 done; m9-linear-log + tasks.local opdateret.

Test plan

  • Lokal E2E: add to cart → checkout → shipping method tilføjet → Stripe test-kort → order oprettet (order_01KJGENGQ87T89JRB5RZGWAF1B).
  • Stripe CLI webhook: payment_intent.created → amount_capturable_updated → charge.succeeded.
  • Efter merge: verificer webhook på staging med Stripe Dashboard secret; evt. smoke-test checkout på staging.

Notes

  • t9.7–t9.9 (recurring/mandate, renewal, staging gate) er stadig backlog.
  • STRIPE_WEBHOOK_SECRET skal sættes til staging webhook secret på Railway; lokalt bruges stripe listen secret.

Made with Cursor

Summary by CodeRabbit

  • New Features

    • Stripe payments integrated end-to-end (client UI, webhook setup, env vars)
    • Dynamic cart APIs and UI with add/update/remove and realtime totals
    • Checkout flow with Stripe payment form and payment-init handling
    • Order confirmation flow that finalizes orders and redirects
    • Product purchase section with variant/quantity controls; remote images allowed from Supabase
  • Documentation

    • Stripe setup guide and updated specs, acceptance criteria, and backlog tasks
  • Bug Fixes

    • Replaced static placeholders with live cart/checkout data

- spec/03, 04, 05, 06, 09: Adyen → Stripe
- env.example: Adyen vars → Stripe vars
- CheckoutSteps placeholder text: Adyen → Stripe
- payload-plugins-research, DESIGN-TO-STOREFRONT-GAP: Adyen → Stripe

Made-with: Cursor
- Register @medusajs/medusa/payment-stripe when STRIPE_API_KEY is set
- Document STRIPE_API_KEY, STRIPE_WEBHOOK_SECRET in commerce env.template
- Document NEXT_PUBLIC_STRIPE_KEY in storefront env.template

Made-with: Cursor
- Seed: when STRIPE_API_KEY set, enable pp_stripe_stripe for Denmark
- Update existing Denmark region with updateRegionsWorkflow
- Add STRIPE-SETUP.md with env vars and Admin alternative

Made-with: Cursor
- Add @stripe/react-stripe-js, @stripe/stripe-js
- CheckoutWithStripe: create cart, initiate payment session, mount PaymentElement
- StripePaymentForm: PaymentElement + confirmPayment with return_url
- API /api/checkout/init: fetch region_id and variant_id from Medusa
- Order confirmation page: complete cart on return from Stripe, redirect to order
- CheckoutSteps: paymentContent prop, onStepChange for step 3

Made-with: Cursor
Medusa Stripe module provides /hooks/payment/stripe_stripe.
Document URL, events, STRIPE_WEBHOOK_SECRET in STRIPE-SETUP.md

Made-with: Cursor
- Checkout uses cookie cart, adds shipping method, completes order
- Seed: fulfillment provider + service zone + shipping option (DKK)
- Cart: getCart/getCartId, CartItems, ProductPurchaseSection
- Checkout page: real cart data, cartId prop
- tasks.local.md: t9.5 status → done
- m9-linear-log.md: t9.5 → Done
- Linear GUA-91 set to Done

Made-with: Cursor
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Feb 27, 2026

Warning

Rate limit exceeded

@eskoubar95 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 2 minutes and 18 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between 5b491a7 and d82924c.

📒 Files selected for processing (2)
  • apps/commerce/src/scripts/seed.ts
  • apps/storefront/src/lib/format.ts
📝 Walkthrough

Walkthrough

Adds Stripe payment integration across commerce and storefront: env vars, Medusa conditional Stripe provider, seed updates for Denmark (payments/fulfillment), storefront cart APIs and components, Stripe Elements checkout flow, i18n and docs updates replacing Adyen with Stripe.

Changes

Cohort / File(s) Summary
Stripe Config & Env
apps/commerce/medusa-config.ts, apps/commerce/env.template, env.example, apps/storefront/env.template
Introduce STRIPE_API_KEY, STRIPE_WEBHOOK_SECRET, NEXT_PUBLIC_STRIPE_KEY; conditional Stripe payment provider registration in Medusa config.
Commerce Seed & Scripts
apps/commerce/src/scripts/seed.ts
Seed updates to set Denmark region payment_providers based on STRIPE_API_KEY, update regions workflow, and ensure manual fulfillment, zone, and shipping option for Denmark.
Stripe Docs
apps/commerce/docs/STRIPE-SETUP.md
New Stripe setup/runbook documenting env vars, webhook config, enablement options, and end-to-end test steps.
Storefront Dependencies & Config
apps/storefront/package.json, apps/storefront/next.config.ts
Add Stripe JS deps and Next.js image remotePatterns for Supabase storage.
Checkout & Payment UI
apps/storefront/src/components/CheckoutWithStripe.tsx, apps/storefront/src/components/StripePaymentForm.tsx, apps/storefront/src/components/CheckoutSteps.tsx
New CheckoutWithStripe + StripePaymentForm; CheckoutSteps extended to accept custom payment content and paymentReady gating.
Cart API & UI
apps/storefront/src/lib/cart.ts, apps/storefront/src/components/cart/CartItems.tsx, apps/storefront/src/app/[locale]/cart/page.tsx
Server-side cart helpers (create/read/add/update/remove), CartItems component with quantity controls, cart page switched to dynamic cart data.
Product Purchase
apps/storefront/src/components/product/ProductPurchaseSection.tsx, apps/storefront/src/app/[locale]/products/[handle]/page.tsx
New ProductPurchaseSection consolidating variant/quantity/add-to-cart logic; product page updated to use it.
Order Confirmation & Routes
apps/storefront/src/app/[locale]/order-confirmation/*, apps/storefront/src/app/api/checkout/init/route.ts
Add order-confirmation page and OrderConfirmationComplete component; new API route /api/checkout/init returning region and variant for checkout bootstrapping.
i18n & Formatting
apps/storefront/src/i18n/dictionaries.*, apps/storefront/src/lib/format.ts
Add translation keys for products/cart/orderConfirmation and new formatPrice helper.
Docs & Specs
apps/cms/docs/payload-plugins-research.md, apps/storefront/docs/..., spec/*.md, work/backlog/*
Replace Adyen references with Stripe across docs; add subscription backlog items (SetupIntent, renewal/webhook tasks) and related planning updates.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant Storefront as Storefront (Browser)
    participant Backend as Medusa Backend
    participant Stripe as Stripe API

    User->>Storefront: Open checkout
    Storefront->>Backend: GET cart / update cart (address, email)
    Backend-->>Storefront: Cart + shipping options
    Storefront->>Backend: Add shipping method
    Storefront->>Backend: Create/initiate Stripe payment session
    Backend->>Stripe: Create PaymentIntent / Session
    Stripe-->>Backend: client_secret
    Backend-->>Storefront: client_secret
    Storefront->>Storefront: Render Stripe Elements (client_secret)
    User->>Storefront: Submit payment via Elements
    Storefront->>Stripe: Confirm payment (client-side)
    Stripe-->>Storefront: Payment result
    Storefront->>Backend: Complete cart / capture order
    Backend-->>Storefront: Order ID
    Storefront->>User: Navigate to order confirmation
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰 Hop-hop, Stripe lights the way,
Cards and wallets come to play;
Carts now talk to Medusa's core,
Denmark shipping, payment, more.
Subscriptions queued — let’s code and sway!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 8.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main changes: implementing Stripe payment support for tasks t9.1–t9.6, covering checkout, webhook, and end-to-end integration.
Description check ✅ Passed The description follows the template structure with Summary, Changes, Test plan, and Notes sections. It covers the main accomplishments, modified/added files, testing performed, and remaining work, meeting the template requirements.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch task/m9-stripe

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 10

🧹 Nitpick comments (15)
apps/storefront/src/app/[locale]/order-confirmation/OrderConfirmationComplete.tsx (3)

11-11: Unused dict prop – hardcoded strings used instead.

The dict prop is passed to this component but never used. Lines 49, 62-63, and 69 use hardcoded locale-specific strings instead of dictionary entries. This is inconsistent with the localization pattern used elsewhere in the codebase.

♻️ Suggested fix to use dictionary entries
 if (status === "loading") {
   return (
     <div className="min-h-full">
       <main className="container mx-auto max-w-2xl px-4 py-8 text-center">
         <p className="text-muted-foreground">
-          {locale === "da" ? "Bekræfter ordre..." : "Confirming order..."}
+          {dict.confirming ?? "Confirming order..."}
         </p>
       </main>
     </div>
   );
 }

 if (status === "error") {
   return (
     <div className="min-h-full">
       <main className="container mx-auto max-w-2xl px-4 py-8 text-center">
         <p className="text-destructive">
-          {locale === "da"
-            ? "Kunne ikke bekræfte ordren."
-            : "Could not confirm order."}
+          {dict.errorMessage ?? "Could not confirm order."}
         </p>
         <Link
           href={`/${locale}/checkout`}
           className="mt-4 inline-block text-primary hover:underline"
         >
-          {locale === "da" ? "Tilbage til checkout" : "Back to checkout"}
+          {dict.backToCheckout ?? "Back to checkout"}
         </Link>
       </main>
     </div>
   );
 }

Ensure the dictionary entries (confirming, errorMessage, backToCheckout) are added to orderConfirmation in the dictionaries file.

Also applies to: 17-17, 49-49, 61-63, 69-69

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/storefront/src/app/`[locale]/order-confirmation/OrderConfirmationComplete.tsx
at line 11, The component OrderConfirmationComplete currently accepts a prop
named dict but ignores it, using hardcoded strings instead; update the JSX in
OrderConfirmationComplete (replace hardcoded texts at the render locations that
currently use the strings for "confirming", "errorMessage", and
"backToCheckout") to read from dict.orderConfirmation.confirming,
dict.orderConfirmation.errorMessage, and dict.orderConfirmation.backToCheckout
respectively, and add those three keys under orderConfirmation in the
dictionaries file so the translations exist.

25-42: Consider adding error logging and abort handling.

  1. The .catch(() => setStatus("error")) silently swallows errors, making debugging difficult in production.
  2. If the component unmounts during the async call (e.g., user navigates away), the state update will be attempted on an unmounted component.
♻️ Suggested improvements
 useEffect(() => {
   if (!cartId) {
     setStatus("error");
     return;
   }
+  let cancelled = false;
   medusa.store.cart
     .complete(cartId)
     .then((res) => {
+      if (cancelled) return;
       if (res.type === "order" && "order" in res && res.order?.id) {
-        setOrderId(res.order.id);
         setStatus("success");
         router.replace(`/${locale}/order-confirmation/${res.order.id}`);
       } else {
         setStatus("error");
       }
     })
-    .catch(() => setStatus("error"));
+    .catch((err) => {
+      if (cancelled) return;
+      console.error("Failed to complete cart:", err);
+      setStatus("error");
+    });
+  return () => { cancelled = true; };
 }, [cartId, locale, router]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/storefront/src/app/`[locale]/order-confirmation/OrderConfirmationComplete.tsx
around lines 25 - 42, The effect calling medusa.store.cart.complete should log
errors and avoid state updates after unmount: modify the useEffect that wraps
medusa.store.cart.complete to capture and log the caught error (include error
object in processLogger/console.error) and add an abort/isMounted guard (e.g.,
an AbortController or let mounted = true / mounted = false in cleanup) so that
setStatus, setOrderId and router.replace only run when mounted; ensure you
reference the same symbols (useEffect, medusa.store.cart.complete, setStatus,
setOrderId, router.replace) when adding the guard and logging so the async
callback safely handles unmount and surfaces error details.

23-23: Unused orderId state variable.

The orderId state is set on Line 34 but never read or rendered. Since the component immediately redirects on success (Line 36) and returns null (Line 76), this state serves no purpose.

🧹 Remove unused state
-const [orderId, setOrderId] = useState<string | null>(null);
 
 useEffect(() => {
   // ...
     if (res.type === "order" && "order" in res && res.order?.id) {
-      setOrderId(res.order.id);
       setStatus("success");
       router.replace(`/${locale}/order-confirmation/${res.order.id}`);
     }
   // ...
 }, [cartId, locale, router]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/storefront/src/app/`[locale]/order-confirmation/OrderConfirmationComplete.tsx
at line 23, The orderId state (const [orderId, setOrderId] = useState<string |
null>(null)) in the OrderConfirmationComplete component is unused; remove the
state declaration and any calls to setOrderId so the component doesn't maintain
dead state, and if that removes the only use of useState in the file, also
remove the useState import; locate symbols orderId, setOrderId and the
OrderConfirmationComplete component to make these removals.
apps/storefront/src/components/product/ProductPurchaseSection.tsx (1)

51-51: Clear the success-reset timer before rescheduling and on unmount.

At Line 51, the timeout can outlive navigation/unmount or race with a later add action, causing stale added flips. Use a ref + cleanup.

Proposed patch
-import { useState, useTransition } from "react";
+import { useEffect, useRef, useState, useTransition } from "react";
@@
   const [isPending, startTransition] = useTransition();
   const router = useRouter();
+  const addedTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
+
+  useEffect(() => {
+    return () => {
+      if (addedTimerRef.current) clearTimeout(addedTimerRef.current);
+    };
+  }, []);
@@
-        setTimeout(() => setAdded(false), 2000);
+        if (addedTimerRef.current) clearTimeout(addedTimerRef.current);
+        addedTimerRef.current = setTimeout(() => setAdded(false), 2000);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/storefront/src/components/product/ProductPurchaseSection.tsx` at line
51, The timeout that flips `added` via setTimeout in ProductPurchaseSection can
outlive the component or race with subsequent adds; fix it by storing the timer
id in a ref (e.g., addedTimeoutRef) so you can clearTimeout before scheduling a
new timer and also clearTimeout in a useEffect cleanup/unmount; update the logic
around setAdded(false) to first clear any existing addedTimeoutRef.current,
assign the new timer id to addedTimeoutRef.current, and add a useEffect cleanup
that clears addedTimeoutRef.current to prevent stale flips after unmount.
apps/storefront/src/app/[locale]/products/[handle]/page.tsx (1)

175-178: Move inline DA/EN literals to dictionary keys.

Line 175, Line 178, and Line 183 use inline locale ternaries. Prefer dict.products.* keys so translations stay centralized and consistent with the rest of the page.

Proposed direction
             <ProductPurchaseSection
               variants={variants}
-              sizeLabel={locale === "da" ? "Størrelse" : "Size"}
+              sizeLabel={dict.products.size}
               quantityLabel={dict.products.quantity}
               addToCartLabel={dict.products.addToCart}
-              addedLabel={locale === "da" ? "Tilføjet ✓" : "Added ✓"}
+              addedLabel={dict.products.added}
+              decreaseQuantityAriaLabel={dict.products.decreaseQuantity}
+              increaseQuantityAriaLabel={dict.products.increaseQuantity}
             >
@@
-                  {locale === "da" ? "Købsmuligheder" : "Purchase options"}
+                  {dict.products.purchaseOptions}

Also applies to: 183-183

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/storefront/src/app/`[locale]/products/[handle]/page.tsx around lines 175
- 178, The inline Danish/English literals passed as props (sizeLabel, addedLabel
and another locale ternary around line 183) should be replaced by centralized
keys on the existing dict.products object; update the JSX to use
dict.products.sizeLabel (or dict.products.size), dict.products.addedLabel (or
dict.products.added) and the corresponding key for the other literal instead of
ternaries, and add those keys to the product dictionary (dict.products) used by
the page so translations remain centralized; locate the prop usages (sizeLabel,
addedLabel, and the prop at ~183) in the product page component and swap the
ternaries for dict.products.<key> entries and add the new keys to the dict used
by the component.
apps/storefront/src/app/api/checkout/init/route.ts (2)

20-23: Consider adding fetch timeouts for resilience.

These parallel fetches to the Medusa backend have no timeout configured, which could cause the request to hang if the backend is unresponsive.

🔧 Proposed improvement using AbortController
+    const controller = new AbortController();
+    const timeoutId = setTimeout(() => controller.abort(), 10000);
+
     const [regionsRes, productsRes] = await Promise.all([
-      fetch(`${MEDUSA_URL}/store/regions?currency_code=dkk`, { headers }),
-      fetch(`${MEDUSA_URL}/store/products?limit=1`, { headers }),
+      fetch(`${MEDUSA_URL}/store/regions?currency_code=dkk`, { 
+        headers, 
+        signal: controller.signal 
+      }),
+      fetch(`${MEDUSA_URL}/store/products?limit=1`, { 
+        headers, 
+        signal: controller.signal 
+      }),
     ]);
+
+    clearTimeout(timeoutId);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/storefront/src/app/api/checkout/init/route.ts` around lines 20 - 23, The
parallel fetches that populate regionsRes and productsRes (the two fetch calls
to `${MEDUSA_URL}/store/regions?currency_code=dkk` and
`${MEDUSA_URL}/store/products?limit=1`) lack timeouts and can hang; wrap these
calls with an AbortController and a timeout (clearTimeout on success) and pass
controller.signal into each fetch, then abort the controller after the timeout
to reject hanging requests and handle the aborted error path in the route
handler.

53-57: Consider logging the error for observability.

The catch block silently discards the error, which can make debugging production issues difficult.

🔧 Proposed improvement
-  } catch {
+  } catch (error) {
+    console.error("[checkout/init] Unexpected error:", error);
     return NextResponse.json(
       { message: "Checkout init failed" },
       { status: 500 }
     );
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/storefront/src/app/api/checkout/init/route.ts` around lines 53 - 57, The
catch block in the checkout init route currently swallows errors; change the
anonymous catch to capture the exception (e.g., catch (err)) and log the error
before returning the 500 response — use the app’s logger if available or
console.error(err, "Checkout init failed") so the NextResponse.json call remains
but now includes an observability-friendly logged error; reference the catch
block around the NextResponse.json call and the NextResponse.json usage to
locate the spot to update.
apps/storefront/next.config.ts (1)

4-12: Consider making the Supabase hostname configurable.

The hardcoded Supabase project ID works for the current setup, but may require changes when deploying to different environments (staging vs production).

🔧 Proposed improvement
+const SUPABASE_PROJECT_ID = process.env.NEXT_PUBLIC_SUPABASE_PROJECT_ID || "tknxlzoejhauuzloezfi";
+
 const nextConfig: NextConfig = {
   images: {
     remotePatterns: [
       {
         protocol: "https",
-        hostname: "tknxlzoejhauuzloezfi.supabase.co",
+        hostname: `${SUPABASE_PROJECT_ID}.supabase.co`,
         pathname: "/storage/v1/object/public/**",
       },
     ],
   },
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/storefront/next.config.ts` around lines 4 - 12, The
images.remotePatterns hostname is hardcoded; make it configurable by reading an
environment variable (e.g., process.env.SUPABASE_HOSTNAME or
process.env.NEXT_PUBLIC_SUPABASE_HOST) and use that variable in the
remotePatterns hostname entry inside next.config.ts (where images.remotePatterns
is defined); keep the current string as a fallback if the env var is unset and
ensure the env var is documented for different environments
(staging/production).
apps/storefront/src/components/cart/CartItems.tsx (2)

8-19: Consider adding proper types for cart items.

Using any[] bypasses TypeScript's type checking. Consider defining an interface for cart items to catch type errors at compile time.

🔧 Suggested type definition
interface CartItem {
  id: string;
  thumbnail?: string;
  product_title?: string;
  title?: string;
  variant_title?: string;
  variant?: {
    product?: { thumbnail?: string };
    title?: string;
  };
  unit_price?: number;
  quantity?: number;
  total?: number;
}

interface CartItemsProps {
  items: CartItem[];
  locale: string;
  dict: {
    cart: {
      remove: string;
      oneTimePurchase: string;
      subscribe: string;
    };
  };
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/storefront/src/components/cart/CartItems.tsx` around lines 8 - 19,
Replace the loose any[] in CartItemsProps with a concrete CartItem interface and
update CartItemsProps.items to use CartItem[]; define a CartItem type (e.g., id,
thumbnail?, product_title?, title?, variant_title?, variant?: { product?: {
thumbnail?: string }; title?: string }, unit_price?: number, quantity?: number,
total?: number) and use that in the component props and anywhere the items are
consumed (search for CartItemsProps and where items is mapped/used in the
CartItems component) so TypeScript can type-check access to properties like
thumbnail, title, variant, unit_price, quantity, and total.

89-105: Add aria-labels for quantity control buttons.

The increment/decrement buttons only contain icons without accessible labels, making them unclear to screen reader users.

♿ Proposed accessibility improvement
                   <button
                     type="button"
                     onClick={() => handleQuantityChange(item.id, quantity - 1)}
                     disabled={isPending || quantity <= 1}
                     className="rounded p-1 hover:bg-muted disabled:opacity-40"
+                    aria-label={locale === "da" ? "Reducer antal" : "Decrease quantity"}
                   >
                     <Minus className="h-3.5 w-3.5" />
                   </button>
                   <span className="w-6 text-center text-sm">{quantity}</span>
                   <button
                     type="button"
                     onClick={() => handleQuantityChange(item.id, quantity + 1)}
                     disabled={isPending}
                     className="rounded p-1 hover:bg-muted disabled:opacity-40"
+                    aria-label={locale === "da" ? "Øg antal" : "Increase quantity"}
                   >
                     <Plus className="h-3.5 w-3.5" />
                   </button>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/storefront/src/components/cart/CartItems.tsx` around lines 89 - 105, The
quantity increment/decrement buttons in CartItems.tsx use only icons
(Minus/Plus) which lack accessible labels; update the two button elements that
call handleQuantityChange(item.id, ...) to include descriptive aria-label
attributes (e.g., aria-label="Decrease quantity for {item.name or item.id}" and
aria-label="Increase quantity for {item.name or item.id}") so screen readers can
identify their purpose, and keep the existing disabled={isPending || quantity <=
1} and disabled={isPending} logic intact.
apps/storefront/src/lib/cart.ts (1)

17-25: Consider handling edge cases in region response parsing.

The current logic handles both data.regions array and direct data response, but if the API returns an empty array or unexpected format, regions[0]?.id returns undefined. This is handled gracefully by returning null, but adding a log for debugging failed region lookups could help troubleshoot issues.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/storefront/src/lib/cart.ts` around lines 17 - 25, The getRegionId
function may return undefined when the regions array is empty or the response
shape is unexpected; update getRegionId to explicitly handle empty arrays and
malformed responses by checking that regions is an array with at least one
element or an object with an id, logging a clear debug/error message (using your
app logger) when no valid id is found, and then returning null; reference the
getRegionId function, the fetch call using MEDUSA_URL and headers(), and ensure
the new logic returns null for missing ids while logging the response or
relevant details to aid troubleshooting.
apps/storefront/src/components/StripePaymentForm.tsx (1)

35-40: Hardcoded billing country limits flexibility.

The billing address country is hardcoded to "DK". While appropriate for the Denmark-focused MVP, this will need adjustment if expanding to other markets.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/storefront/src/components/StripePaymentForm.tsx` around lines 35 - 40,
The payment_method_data in StripePaymentForm.tsx hardcodes
billing_details.address.country to "DK"; change it to use a dynamic value (e.g.,
a prop or state) instead—locate the StripePaymentForm component and where
createPaymentMethod/confirmCardPayment is assembled (look for
payment_method_data and billing_details in that file) and replace the literal
"DK" with a supplied billing country like props.billingAddress.country or a
resolved locale (with a sensible fallback such as undefined or a configurable
default) so the billing country is flexible for other markets.
apps/storefront/src/app/[locale]/cart/page.tsx (1)

30-31: Minor: Duplicated formatPrice utility.

The same formatPrice function is defined here and in CartItems.tsx. Consider extracting to a shared utility in @/lib/format.ts for consistency and maintainability.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/storefront/src/app/`[locale]/cart/page.tsx around lines 30 - 31, The
formatPrice implementation in page.tsx duplicates the one in CartItems.tsx;
extract the formatter into a shared utility file (e.g., create and export a
function in `@/lib/format.ts` named formatPrice that accepts (amount: number,
locale?: string) or at minimum (amount: number) and returns the Intl-formatted
string), update page.tsx and CartItems.tsx to import formatPrice from
'@/lib/format' and remove the local definitions so both use the single shared
function; ensure the exported function signature matches usages in both page.tsx
and CartItems.tsx (adjust call sites if you add an optional locale parameter).
apps/storefront/src/app/[locale]/checkout/page.tsx (2)

66-85: Inline item type annotation is verbose.

Consider extracting the cart item type for reusability and cleaner code. This would also help remove the any[] cast on line 27.

♻️ Suggested type extraction
// Could be placed in a shared types file or at the top of this file
interface CartItem {
  id: string;
  title: string;
  variant_title?: string;
  thumbnail?: string;
  quantity: number;
  total: number;
}

Then use:

-  // eslint-disable-next-line `@typescript-eslint/no-explicit-any`
-  const items = (cart?.items ?? []) as any[];
+  const items = (cart?.items ?? []) as CartItem[];
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/storefront/src/app/`[locale]/checkout/page.tsx around lines 66 - 85,
Extract the inline item type into a reusable interface (e.g., CartItem) and
replace the verbose inline annotation in the items.map callback with that
interface; update the source of items (where it's typed or cast) to use
CartItem[] instead of any[] so the cast is removed — look for the items
variable/prop and the items.map call in page.tsx to change the types
accordingly.

23-24: Redundant cart ID fetch.

getCart() internally calls getCartId() (see lib/cart.ts lines 113-128), so the cart ID is already retrieved. You can use cart?.id instead of a separate getCartId() call to avoid reading the cookie twice.

♻️ Suggested simplification
   const cart = await getCart();
-  const cartId = await getCartId();
+  const cartId = cart?.id ?? null;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/storefront/src/app/`[locale]/checkout/page.tsx around lines 23 - 24, The
code redundantly calls getCartId() after getCart(); remove the separate
getCartId() call and use the ID from the cart object (cart?.id) instead. Update
any usage of cartId to read from cart?.id and delete the getCartId() invocation
so the cookie isn’t read twice (refer to getCart() and getCartId() to locate the
logic and replace cartId with cart?.id).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/commerce/src/scripts/seed.ts`:
- Around line 193-201: The catch is currently swallowing all errors from
link.create (called with Modules.STOCK_LOCATION and Modules.FULFILLMENT) and
always logging success; update the try/catch to inspect the thrown error from
link.create and only treat it as a benign “already linked” case when the error
type/message matches that condition, otherwise rethrow or log the actual error
(preserving stack) so unexpected failures (e.g., invalid provider id or
permissions) are surfaced; use the caught error object in the catch and adjust
logger.info/logger.error accordingly.
- Around line 203-210: There is a duplicate const declaration for query
(resolved from ContainerRegistrationKeys.QUERY); remove the second `const query
= ...` (the redeclaration) and reuse the original `query` instance from earlier
in the file, or alternatively rename the later variable (e.g., `otherQuery`)
where it's used; if a narrower type is required at the later use, apply an
inline cast or type assertion when calling methods (e.g., on the existing
`query`) instead of redeclaring the const.

In `@apps/storefront/package.json`:
- Around line 15-16: Update the `@stripe/stripe-js` dependency version in
package.json to the actual published release by replacing the incorrect "^8.8.0"
entry with "^8.7.0" so it matches the real npm package; keep the
"@stripe/react-stripe-js": "^5.6.0" entry unchanged. Ensure the package.json
dependency key "@stripe/stripe-js" is updated and run your package manager to
refresh lockfiles.

In `@apps/storefront/src/app/`[locale]/order-confirmation/page.tsx:
- Line 27: Replace the unsafe cast "locale as Locale" when calling getDictionary
by first validating locale with isValidLocale(locale) (import from i18n/config),
handle the invalid case (redirect or graceful fallback) and then call
getDictionary(locale) after the guard so TypeScript narrows locale to Locale;
reference the getDictionary, isValidLocale and Locale symbols and update the
logic in page.tsx accordingly.

In `@apps/storefront/src/components/cart/CartItems.tsx`:
- Around line 32-45: Both handleRemove and handleQuantityChange swallow
exceptions inside the startTransition callbacks; wrap the async bodies passed to
startTransition in try/catch and handle errors from removeLineItem and
updateLineItem: catch the error, log it, and surface user feedback (e.g., call
your existing toast/notification or set an error state) so failures aren’t
silent, then still call router.refresh or perform any cleanup in finally.
Specifically update the async callbacks inside startTransition in the
handleRemove and handleQuantityChange functions to await the operation in a try
block, handle errors in catch, and use finally for router.refresh/cleanup.

In `@apps/storefront/src/components/CheckoutSteps.tsx`:
- Around line 283-290: The Place Order Link (rendered when !paymentReady) can be
clicked during payment initialization and should be gated; update the
conditional around the Link in CheckoutSteps.tsx to only render the
confirmationHref Link when there is no paymentContent provided (i.e.,
fallback/demo mode) by checking both !paymentReady and !paymentContent, or
alternatively when paymentContent exists keep the Link hidden or render a
disabled/annotated control until paymentReady becomes true; refer to the
paymentReady and paymentContent variables, the confirmationHref Link and
checkout.placeOrder label when making this change.

In `@apps/storefront/src/components/CheckoutWithStripe.tsx`:
- Around line 53-72: Replace the hardcoded email/address in CheckoutWithStripe
(the medusa.store.cart.update call) with actual user-entered data: accept the
customer address/email from CheckoutSteps (either via props, context, or by
returning data from ensureCartAndPayment) and pass that object into
medusa.store.cart.update instead of the fixed values; validate the address
object (required fields like email, shipping_address, billing_address) and
fallback to a safe no-op or error path if missing so production orders never get
placeholder data.
- Around line 78-82: The code blindly uses shipping_options[0].id; change it to
use the user's persisted selection (e.g., a prop like selectedShippingOptionId
or a persisted value on the cart such as
cart.metadata.selected_shipping_option_id) when calling
medusa.store.cart.addShippingMethod(cartId, { option_id: ... }), and only fall
back to shipping_options[0].id if that persisted id is missing; also validate
the chosen id exists in shipping_options before calling the addShippingMethod
API (use shipping_options.find(...) and pass its id).
- Around line 50-51: The early return in ensureCartAndPayment currently exits
whenever cart is truthy, which blocks recovery if clientSecret is null after a
previous failure; change the guard so it only returns when there is neither
cartId nor need to initialize payment (e.g., replace "if (!cartId || cart)
return" with a check that returns only when no cartId and no cart OR when cart
exists AND clientSecret is already set), and ensure ensureCartAndPayment
continues to run when cart exists but clientSecret is null so it can
re-request/initialize the payment intent and set clientSecret; adjust associated
logic that creates the payment intent/usePayment API to be idempotent within
ensureCartAndPayment to avoid duplicate side effects.

In `@apps/storefront/src/components/product/ProductPurchaseSection.tsx`:
- Around line 96-97: The hardcoded English aria-labels on the quantity buttons
inside the ProductPurchaseSection component must be localized; replace the
literal strings used for the decrease/increase button aria-label props with
calls to your app's i18n utility (e.g., useTranslation().t or
intl.formatMessage) and add corresponding translation keys like
"product.decreaseQuantity" and "product.increaseQuantity"; update the JSX for
the decrease button and the increase button in ProductPurchaseSection.tsx to use
these localized strings and import/use the translation hook or util at the top
of the component.

---

Nitpick comments:
In `@apps/storefront/next.config.ts`:
- Around line 4-12: The images.remotePatterns hostname is hardcoded; make it
configurable by reading an environment variable (e.g.,
process.env.SUPABASE_HOSTNAME or process.env.NEXT_PUBLIC_SUPABASE_HOST) and use
that variable in the remotePatterns hostname entry inside next.config.ts (where
images.remotePatterns is defined); keep the current string as a fallback if the
env var is unset and ensure the env var is documented for different environments
(staging/production).

In `@apps/storefront/src/app/`[locale]/cart/page.tsx:
- Around line 30-31: The formatPrice implementation in page.tsx duplicates the
one in CartItems.tsx; extract the formatter into a shared utility file (e.g.,
create and export a function in `@/lib/format.ts` named formatPrice that accepts
(amount: number, locale?: string) or at minimum (amount: number) and returns the
Intl-formatted string), update page.tsx and CartItems.tsx to import formatPrice
from '@/lib/format' and remove the local definitions so both use the single
shared function; ensure the exported function signature matches usages in both
page.tsx and CartItems.tsx (adjust call sites if you add an optional locale
parameter).

In `@apps/storefront/src/app/`[locale]/checkout/page.tsx:
- Around line 66-85: Extract the inline item type into a reusable interface
(e.g., CartItem) and replace the verbose inline annotation in the items.map
callback with that interface; update the source of items (where it's typed or
cast) to use CartItem[] instead of any[] so the cast is removed — look for the
items variable/prop and the items.map call in page.tsx to change the types
accordingly.
- Around line 23-24: The code redundantly calls getCartId() after getCart();
remove the separate getCartId() call and use the ID from the cart object
(cart?.id) instead. Update any usage of cartId to read from cart?.id and delete
the getCartId() invocation so the cookie isn’t read twice (refer to getCart()
and getCartId() to locate the logic and replace cartId with cart?.id).

In
`@apps/storefront/src/app/`[locale]/order-confirmation/OrderConfirmationComplete.tsx:
- Line 11: The component OrderConfirmationComplete currently accepts a prop
named dict but ignores it, using hardcoded strings instead; update the JSX in
OrderConfirmationComplete (replace hardcoded texts at the render locations that
currently use the strings for "confirming", "errorMessage", and
"backToCheckout") to read from dict.orderConfirmation.confirming,
dict.orderConfirmation.errorMessage, and dict.orderConfirmation.backToCheckout
respectively, and add those three keys under orderConfirmation in the
dictionaries file so the translations exist.
- Around line 25-42: The effect calling medusa.store.cart.complete should log
errors and avoid state updates after unmount: modify the useEffect that wraps
medusa.store.cart.complete to capture and log the caught error (include error
object in processLogger/console.error) and add an abort/isMounted guard (e.g.,
an AbortController or let mounted = true / mounted = false in cleanup) so that
setStatus, setOrderId and router.replace only run when mounted; ensure you
reference the same symbols (useEffect, medusa.store.cart.complete, setStatus,
setOrderId, router.replace) when adding the guard and logging so the async
callback safely handles unmount and surfaces error details.
- Line 23: The orderId state (const [orderId, setOrderId] = useState<string |
null>(null)) in the OrderConfirmationComplete component is unused; remove the
state declaration and any calls to setOrderId so the component doesn't maintain
dead state, and if that removes the only use of useState in the file, also
remove the useState import; locate symbols orderId, setOrderId and the
OrderConfirmationComplete component to make these removals.

In `@apps/storefront/src/app/`[locale]/products/[handle]/page.tsx:
- Around line 175-178: The inline Danish/English literals passed as props
(sizeLabel, addedLabel and another locale ternary around line 183) should be
replaced by centralized keys on the existing dict.products object; update the
JSX to use dict.products.sizeLabel (or dict.products.size),
dict.products.addedLabel (or dict.products.added) and the corresponding key for
the other literal instead of ternaries, and add those keys to the product
dictionary (dict.products) used by the page so translations remain centralized;
locate the prop usages (sizeLabel, addedLabel, and the prop at ~183) in the
product page component and swap the ternaries for dict.products.<key> entries
and add the new keys to the dict used by the component.

In `@apps/storefront/src/app/api/checkout/init/route.ts`:
- Around line 20-23: The parallel fetches that populate regionsRes and
productsRes (the two fetch calls to
`${MEDUSA_URL}/store/regions?currency_code=dkk` and
`${MEDUSA_URL}/store/products?limit=1`) lack timeouts and can hang; wrap these
calls with an AbortController and a timeout (clearTimeout on success) and pass
controller.signal into each fetch, then abort the controller after the timeout
to reject hanging requests and handle the aborted error path in the route
handler.
- Around line 53-57: The catch block in the checkout init route currently
swallows errors; change the anonymous catch to capture the exception (e.g.,
catch (err)) and log the error before returning the 500 response — use the app’s
logger if available or console.error(err, "Checkout init failed") so the
NextResponse.json call remains but now includes an observability-friendly logged
error; reference the catch block around the NextResponse.json call and the
NextResponse.json usage to locate the spot to update.

In `@apps/storefront/src/components/cart/CartItems.tsx`:
- Around line 8-19: Replace the loose any[] in CartItemsProps with a concrete
CartItem interface and update CartItemsProps.items to use CartItem[]; define a
CartItem type (e.g., id, thumbnail?, product_title?, title?, variant_title?,
variant?: { product?: { thumbnail?: string }; title?: string }, unit_price?:
number, quantity?: number, total?: number) and use that in the component props
and anywhere the items are consumed (search for CartItemsProps and where items
is mapped/used in the CartItems component) so TypeScript can type-check access
to properties like thumbnail, title, variant, unit_price, quantity, and total.
- Around line 89-105: The quantity increment/decrement buttons in CartItems.tsx
use only icons (Minus/Plus) which lack accessible labels; update the two button
elements that call handleQuantityChange(item.id, ...) to include descriptive
aria-label attributes (e.g., aria-label="Decrease quantity for {item.name or
item.id}" and aria-label="Increase quantity for {item.name or item.id}") so
screen readers can identify their purpose, and keep the existing
disabled={isPending || quantity <= 1} and disabled={isPending} logic intact.

In `@apps/storefront/src/components/product/ProductPurchaseSection.tsx`:
- Line 51: The timeout that flips `added` via setTimeout in
ProductPurchaseSection can outlive the component or race with subsequent adds;
fix it by storing the timer id in a ref (e.g., addedTimeoutRef) so you can
clearTimeout before scheduling a new timer and also clearTimeout in a useEffect
cleanup/unmount; update the logic around setAdded(false) to first clear any
existing addedTimeoutRef.current, assign the new timer id to
addedTimeoutRef.current, and add a useEffect cleanup that clears
addedTimeoutRef.current to prevent stale flips after unmount.

In `@apps/storefront/src/components/StripePaymentForm.tsx`:
- Around line 35-40: The payment_method_data in StripePaymentForm.tsx hardcodes
billing_details.address.country to "DK"; change it to use a dynamic value (e.g.,
a prop or state) instead—locate the StripePaymentForm component and where
createPaymentMethod/confirmCardPayment is assembled (look for
payment_method_data and billing_details in that file) and replace the literal
"DK" with a supplied billing country like props.billingAddress.country or a
resolved locale (with a sensible fallback such as undefined or a configurable
default) so the billing country is flexible for other markets.

In `@apps/storefront/src/lib/cart.ts`:
- Around line 17-25: The getRegionId function may return undefined when the
regions array is empty or the response shape is unexpected; update getRegionId
to explicitly handle empty arrays and malformed responses by checking that
regions is an array with at least one element or an object with an id, logging a
clear debug/error message (using your app logger) when no valid id is found, and
then returning null; reference the getRegionId function, the fetch call using
MEDUSA_URL and headers(), and ensure the new logic returns null for missing ids
while logging the response or relevant details to aid troubleshooting.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 59c0ff8 and 4eb5f1f.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (31)
  • apps/cms/docs/payload-plugins-research.md
  • apps/commerce/docs/STRIPE-SETUP.md
  • apps/commerce/env.template
  • apps/commerce/medusa-config.ts
  • apps/commerce/src/scripts/seed.ts
  • apps/storefront/docs/DESIGN-TO-STOREFRONT-GAP-ANALYSIS.md
  • apps/storefront/env.template
  • apps/storefront/next.config.ts
  • apps/storefront/package.json
  • apps/storefront/src/app/[locale]/cart/page.tsx
  • apps/storefront/src/app/[locale]/checkout/page.tsx
  • apps/storefront/src/app/[locale]/order-confirmation/OrderConfirmationComplete.tsx
  • apps/storefront/src/app/[locale]/order-confirmation/page.tsx
  • apps/storefront/src/app/[locale]/products/[handle]/page.tsx
  • apps/storefront/src/app/api/checkout/init/route.ts
  • apps/storefront/src/components/CheckoutSteps.tsx
  • apps/storefront/src/components/CheckoutWithStripe.tsx
  • apps/storefront/src/components/StripePaymentForm.tsx
  • apps/storefront/src/components/cart/CartItems.tsx
  • apps/storefront/src/components/product/ProductPurchaseSection.tsx
  • apps/storefront/src/lib/cart.ts
  • env.example
  • spec/03-risks.md
  • spec/04-open-questions.md
  • spec/05-decisions.md
  • spec/06-acceptance.md
  • spec/09-sitemap.md
  • work/backlog/M9-SUBSCRIPTION-TODO.md
  • work/backlog/m9-linear-log.md
  • work/backlog/milestones.md
  • work/backlog/tasks.local.md
📜 Review details
🧰 Additional context used
🧬 Code graph analysis (8)
apps/storefront/src/app/[locale]/order-confirmation/OrderConfirmationComplete.tsx (1)
apps/storefront/src/lib/medusa.ts (1)
  • medusa (13-17)
apps/storefront/src/app/[locale]/order-confirmation/page.tsx (4)
apps/storefront/src/app/[locale]/checkout/page.tsx (1)
  • generateMetadata (12-18)
apps/storefront/src/i18n/dictionaries.ts (1)
  • getDictionary (251-253)
apps/storefront/src/i18n/config.ts (1)
  • Locale (9-9)
apps/storefront/src/app/[locale]/order-confirmation/OrderConfirmationComplete.tsx (1)
  • OrderConfirmationComplete (14-77)
apps/storefront/src/components/product/ProductPurchaseSection.tsx (1)
apps/storefront/src/lib/cart.ts (1)
  • addToCart (66-82)
apps/storefront/src/components/cart/CartItems.tsx (1)
apps/storefront/src/lib/cart.ts (2)
  • removeLineItem (101-112)
  • updateLineItem (84-99)
apps/storefront/src/components/CheckoutWithStripe.tsx (3)
apps/storefront/src/lib/medusa.ts (1)
  • medusa (13-17)
apps/storefront/src/components/StripePaymentForm.tsx (1)
  • StripePaymentForm (13-68)
apps/storefront/src/components/CheckoutSteps.tsx (1)
  • CheckoutSteps (47-296)
apps/storefront/src/app/[locale]/checkout/page.tsx (2)
apps/storefront/src/lib/cart.ts (2)
  • getCart (114-129)
  • getCartId (27-30)
apps/storefront/src/components/CheckoutWithStripe.tsx (1)
  • CheckoutWithStripe (40-136)
apps/storefront/src/app/[locale]/cart/page.tsx (2)
apps/storefront/src/lib/cart.ts (1)
  • getCart (114-129)
apps/storefront/src/components/cart/CartItems.tsx (1)
  • CartItems (21-124)
apps/storefront/src/app/[locale]/products/[handle]/page.tsx (2)
apps/storefront/src/components/product/ProductPurchaseSection.tsx (1)
  • ProductPurchaseSection (23-129)
apps/storefront/src/components/SubscriptionSelector.tsx (1)
  • SubscriptionSelector (15-177)
🪛 Biome (2.4.4)
apps/commerce/src/scripts/seed.ts

[error] 205-205: This variable is used before its declaration.

(lint/correctness/noInvalidUseBeforeDeclaration)

🪛 LanguageTool
work/backlog/tasks.local.md

[grammar] ~774-~774: Ensure spelling is correct
Context: ...er Stripe som payment provider i Denmark-regionen via Medusa Admin. Workspace: apps/...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)


[grammar] ~861-~861: Ensure spelling is correct
Context: ...GUA-91 ## Task: t9.6 Description: Opdater spec-filer: erstatt Adyen med Stripe i ...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)


[style] ~880-~880: The word ‘til’ with one ‘l’ is informal. Consider replacing it.
Context: ...on:** Stripe recurring: betalingsmetode til abonnement (mandate/setup) ved subscrip...

(TIL)


[style] ~880-~880: The word ‘til’ with one ‘l’ is informal. Consider replacing it.
Context: ...p) ved subscription-checkout; gem token til genbrug ved renewal. Workspace: ap...

(TIL)


[style] ~894-~894: The word ‘til’ with one ‘l’ is informal. Consider replacing it.
Context: ...for recurring. - Betalingsmetode gemmes til renewal (token/customer payment method)...

(TIL)

work/backlog/milestones.md

[grammar] ~178-~178: Ensure spelling is correct
Context: ...et i Medusa - Stripe aktiveret i Denmark-regionen - Stripe PaymentElement i storefront check...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)


[style] ~182-~182: The word ‘til’ with one ‘l’ is informal. Consider replacing it.
Context: ...e) ved subscription-checkout; gem token til renewal - Subscription renewal charge (...

(TIL)


[style] ~192-~192: The word ‘til’ with one ‘l’ is informal. Consider replacing it.
Context: ...funded) - Subscription: betalingsmetode til abonnement ved checkout; renewal flow m...

(TIL)

🔇 Additional comments (30)
apps/storefront/src/app/[locale]/order-confirmation/page.tsx (2)

6-9: LGTM on the props interface.

The interface correctly uses the Next.js 15+ async params and searchParams pattern, which is consistent with the checkout page implementation.


11-19: LGTM on metadata generation.

The metadata follows the established pattern from the checkout page, with appropriate robots: { index: false } to prevent indexing of transient confirmation pages.

apps/storefront/src/components/product/ProductPurchaseSection.tsx (1)

23-129: Good consolidation of purchase interactions into one component.

This unifies variant, quantity, and add-to-cart behavior cleanly and simplifies product-page integration.

apps/storefront/src/app/[locale]/products/[handle]/page.tsx (1)

11-11: Nice integration of the new purchase wrapper into PDP.

The page-level composition is cleaner after replacing scattered purchase controls with ProductPurchaseSection.

Also applies to: 173-191

apps/cms/docs/payload-plugins-research.md (1)

55-55: LGTM!

Documentation correctly updated to reflect Stripe as the payment provider, consistent with the broader Adyen-to-Stripe migration in this PR.

apps/storefront/docs/DESIGN-TO-STOREFRONT-GAP-ANALYSIS.md (1)

20-20: LGTM!

Gap analysis correctly updated to reflect Stripe integration status. The "Stripe-placeholder" and "Stripe-integration" references align with the completed t9.1-t9.6 tasks and remaining t9.7-t9.9 backlog items.

Also applies to: 137-137

env.example (1)

32-35: LGTM!

Stripe environment variables are correctly structured:

  • STRIPE_API_KEY (server-side secret key)
  • STRIPE_WEBHOOK_SECRET (for webhook signature verification)
  • NEXT_PUBLIC_STRIPE_KEY (client-safe publishable key with proper NEXT_PUBLIC_ prefix)
work/backlog/m9-linear-log.md (1)

1-23: LGTM!

Linear sync log accurately reflects the task status documented in PR objectives. Clear separation between completed (t9.1-t9.6) and backlog (t9.7-t9.9) tasks.

apps/storefront/src/app/api/checkout/init/route.ts (1)

37-43: LGTM!

Good defensive handling of the response structure variations. This accommodates different Medusa response shapes gracefully.

work/backlog/M9-SUBSCRIPTION-TODO.md (1)

1-31: LGTM!

Clear implementation notes for the remaining subscription tasks. Good documentation of:

  • SetupIntent usage for recurring payments (t9.7)
  • Renewal charge logic with retry strategy (t9.8)
  • Staging gate requirements (t9.9)

The task dependencies and Medusa/Stripe references provide useful context for implementation.

work/backlog/milestones.md (1)

174-192: LGTM!

The M9 milestone updates clearly document the expanded Stripe scope including subscription recurring payment features (mandate setup, renewal handling, staging gate). The exit criteria align well with the in-scope items.

work/backlog/tasks.local.md (1)

750-943: LGTM!

The M9 task updates are well-structured with clear descriptions, acceptance criteria, estimates, and Linear issue references. The progression from t9.1-t9.6 (done) to t9.7-t9.9 (backlog) properly reflects the completed checkout/webhook work and remaining subscription recurring billing tasks.

apps/storefront/env.template (1)

11-13: LGTM!

The Stripe publishable key environment variable is correctly prefixed with NEXT_PUBLIC_ for client-side access, and the comment clearly indicates it should be a pk_* key from the Stripe Dashboard.

apps/storefront/src/app/[locale]/cart/page.tsx (1)

25-35: LGTM!

The cart data fetching and derivation logic correctly handles the dynamic cart state from Medusa. The fallback values (?? 0, ?? []) provide sensible defaults when cart data is incomplete.

apps/storefront/src/lib/cart.ts (1)

75-81: ⚠️ Potential issue | 🔴 Critical

Bug: Response body consumed twice causing error.

The response body is consumed on Line 76 (res.json()) to extract the error, then consumed again on Line 80 (res.json()) for the cart. Streams can only be read once, so Line 80 will fail with an error like "body stream already read".

🐛 Proposed fix
   if (!res.ok) {
     const err = await res.json().catch(() => ({}));
     throw new Error(err.message || "Failed to add item to cart");
   }

-  const { cart } = await res.json();
+  const data = await res.json();
+  const { cart } = data;
   return cart;

Or restructure the entire block:

-  if (!res.ok) {
-    const err = await res.json().catch(() => ({}));
-    throw new Error(err.message || "Failed to add item to cart");
-  }
-
-  const { cart } = await res.json();
-  return cart;
+  const data = await res.json().catch(() => ({}));
+  if (!res.ok) {
+    throw new Error(data.message || "Failed to add item to cart");
+  }
+  return data.cart;

Likely an incorrect or invalid review comment.

apps/storefront/src/components/CheckoutWithStripe.tsx (1)

86-91: Verify provider_id format with Medusa documentation.

The hardcoded provider_id: "pp_stripe_stripe" is used consistently in both the checkout and seed script, but verify this matches Medusa's Stripe plugin provider ID convention. While the nested optional chaining on lines 90–91 is present, error handling is already in place: the code checks if (secret) before using it and sets an error state if missing, along with a try-catch wrapper for the entire operation. Consider adding a more explicit log or validation error if payment session structure is unexpectedly missing to improve debuggability.

apps/storefront/src/components/StripePaymentForm.tsx (1)

23-50: Stripe payment flow is correctly implemented.

The code properly uses stripe.confirmPayment() with Elements and confirmParams, following the official @stripe/react-stripe-js v5 pattern. The return URL construction with cart_id query parameter allows the order confirmation page to complete checkout. Error handling securely exposes only the error message without internal details, and the SSR safety check on window.location.origin is in place. The implementation is production-ready.

apps/storefront/src/components/CheckoutSteps.tsx (1)

36-38: LGTM on the new callback props.

The onStepChange, paymentContent, and paymentReady props provide good extensibility for integrating different payment providers. The handleStepChange helper cleanly separates state update from callback invocation.

Also applies to: 51-53

spec/09-sitemap.md (1)

83-83: Documentation update looks correct.

The payment method reference is updated from Adyen to Stripe, consistent with the PR's integration changes.

spec/03-risks.md (1)

42-42: Risk documentation updated appropriately.

The risk item now reflects Stripe as the payment provider with appropriate notes about DK-specific payment methods and subscription lifecycle alignment.

spec/04-open-questions.md (1)

8-9: Staging acceptance gates updated correctly.

Clear requirements for Stripe recurring capabilities validation (tokenization/mandates + renewal simulation) and payment method availability checks.

spec/06-acceptance.md (1)

8-8: Acceptance criteria updated correctly for Stripe integration.

All payment provider references are consistently updated from Adyen to Stripe across MVP definition, security requirements, and quality gates. The "when enabled" qualifier for MobilePay/Klarna appropriately reflects their optional nature in Stripe.

Also applies to: 50-50, 59-59

apps/commerce/env.template (1)

83-91: Stripe environment configuration is well documented.

Good separation of concerns with clear comments explaining each variable's purpose and where storefront keys should be configured. The optional nature of STRIPE_WEBHOOK_SECRET for local development is appropriately indicated by commenting it out.

apps/commerce/medusa-config.ts (1)

63-82: Stripe payment module integration is correct.

The conditional registration pattern prevents initialization errors when Stripe keys are missing. The automatic_payment_methods: true option is properly configured for supporting cards, Apple Pay, and Google Pay. Configuration options match the Medusa v2 documentation schema: apiKey is required, webhookSecret is optional for development (required in production), and automatic_payment_methods is a valid optional setting that defaults to false.

apps/commerce/docs/STRIPE-SETUP.md (1)

1-61: Well-structured Stripe setup documentation.

The documentation covers the essential setup steps clearly: environment variables, region enablement options (seed script vs Admin UI), webhook configuration, and E2E testing procedure. The webhook URL pattern /hooks/payment/stripe_stripe correctly follows Medusa's payment module convention.

apps/commerce/src/scripts/seed.ts (2)

135-168: LGTM!

The Stripe payment provider configuration is well-implemented:

  • Conditionally enables Stripe based on STRIPE_API_KEY presence.
  • Idempotent handling for both new region creation and existing region updates.
  • Clear logging indicates whether Stripe was enabled.

213-252: LGTM!

The service zone and shipping option setup is well-structured:

  • Idempotent zone creation with sensible name matching.
  • Shipping option creation with proper linkage to profile and provider.
  • Helpful warning message when fulfillment set is missing.
spec/05-decisions.md (3)

103-106: Documentation updates align with Stripe integration.

The decision log correctly reflects the switch from Adyen to Stripe, with updated payment method mentions and configuration references.


273-279: Good generalization of section title.

Renaming from "Adyen risk/3DS..." to "Payment provider risk/3DS..." makes the section more provider-agnostic while the content remains applicable to Stripe's 3DS handling.


375-379: Stripe staging gate criteria properly documented.

The staging gate explicitly requires Stripe recurring capability and payment method availability confirmation before release, which aligns with the E2E testing approach documented in STRIPE-SETUP.md.

Comment thread apps/commerce/src/scripts/seed.ts
Comment thread apps/commerce/src/scripts/seed.ts Outdated
Comment thread apps/storefront/package.json
Comment thread apps/storefront/src/app/[locale]/order-confirmation/page.tsx
Comment thread apps/storefront/src/components/cart/CartItems.tsx
Comment thread apps/storefront/src/components/CheckoutSteps.tsx Outdated
Comment thread apps/storefront/src/components/CheckoutWithStripe.tsx Outdated
Comment thread apps/storefront/src/components/CheckoutWithStripe.tsx
Comment thread apps/storefront/src/components/CheckoutWithStripe.tsx
Comment thread apps/storefront/src/components/product/ProductPurchaseSection.tsx Outdated
@eskobar95
Copy link
Copy Markdown
Owner Author

Addressed CodeRabbit review comments:

  • cart.ts: Single res.json() parse in addToCart to avoid body consumption issues; seed.ts: Removed duplicate query declaration, improved link.create catch to only treat duplicate as benign
  • OrderConfirmationComplete: Use dict for confirming/errorMessage/backToCheckout, removed unused orderId state, added cancelled guard + console.error on complete failure
  • ProductPurchaseSection: Timer ref + cleanup on unmount; localized aria-labels via dict.products (size, added, purchaseOptions, decreaseQuantity, increaseQuantity)
  • CartItems: CartItem interface, shared formatPrice from @/lib/format, aria-labels from dict.cart
  • Checkout page: cartId from cart?.id, CartItem[] type, shared formatPrice
  • CheckoutWithStripe: Early return only when cart && clientSecret (allows retry when secret failed); CheckoutSteps: Place Order link only when !paymentContent || paymentReady
  • next.config.ts: Supabase hostname from NEXT_PUBLIC_SUPABASE_PROJECT_ID with fallback
  • i18n: Added orderConfirmation.confirming/errorMessage/backToCheckout; products.size/added/purchaseOptions/decreaseQuantity/increaseQuantity; cart.decreaseQuantity/increaseQuantity

All 10 review threads marked resolved.

- cart.ts: single res.json() in addToCart; seed: remove duplicate query, improve link catch
- OrderConfirmationComplete: dict for confirming/error/backToCheckout, remove orderId, abort guard + log
- ProductPurchaseSection: timer ref + cleanup; localized aria-labels via dict
- CartItems: CartItem type, formatPrice from @/lib/format, aria-labels from dict.cart
- Checkout page: cartId from cart?.id, CartItem[], shared formatPrice
- CheckoutWithStripe: retry when clientSecret null; CheckoutSteps: gate Place Order link
- next.config: Supabase host from NEXT_PUBLIC_SUPABASE_PROJECT_ID
- i18n: orderConfirmation + products + cart keys for accessibility and copy

Made-with: Cursor
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

♻️ Duplicate comments (1)
apps/storefront/src/components/CheckoutSteps.tsx (1)

283-290: ⚠️ Potential issue | 🟠 Major

Keep “Place Order” hidden whenever Stripe payment content is mounted.

This still allows a direct navigation path when paymentReady becomes true, which can be misleading or bypass payment confirmation unless paymentReady strictly means “payment already confirmed”. Prefer showing this Link only in fallback mode (!paymentContent) and letting Stripe UI own the final submit action.

#!/bin/bash
# Verify what "paymentReady" means in the Stripe flow.
# Expected safe behavior: paymentReady is only true after successful payment confirmation.
# Risky behavior: paymentReady is true when Stripe client secret / element is merely initialized.

set -euo pipefail

TARGET=$(fd 'CheckoutWithStripe\.tsx$' | head -n 1)
echo "Inspecting: ${TARGET}"

rg -n -C3 'paymentReady|setPaymentReady|onStepChange|CheckoutSteps|confirm|complete|confirmationHref' "$TARGET"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/storefront/src/components/CheckoutSteps.tsx` around lines 283 - 290, The
Link rendering currently allows navigation when paymentReady is true even if
paymentContent (Stripe UI) is mounted; update the conditional in CheckoutSteps
to render the confirmation Link only when paymentContent is absent (i.e.,
replace {(!paymentContent || paymentReady) && (...)} with {(!paymentContent) &&
(...)}), and if the Stripe flow relies on paymentReady as a true-confirmation
flag, add a TODO or assertion in the CheckoutWithStripe component where
setPaymentReady is called to verify it only becomes true after successful
payment confirmation (refer to symbols paymentContent, paymentReady,
confirmationHref, CheckoutSteps, and CheckoutWithStripe).
🧹 Nitpick comments (8)
apps/storefront/src/components/CheckoutSteps.tsx (1)

258-270: Move new Stripe placeholder copy into dict keys for consistency.

These newly added strings are inline and bypass the existing dictionary pattern, which makes localization maintenance harder.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/storefront/src/components/CheckoutSteps.tsx` around lines 258 - 270, The
new inline Stripe placeholder strings in the CheckoutSteps component bypass the
existing localization dictionary; move both messages into the shared dict and
reference them instead of using the locale ternaries. Update CheckoutSteps to
use dict keys like stripePlaceholderTitle and stripePlaceholderSubtitle (or
similar) where paymentContent is rendered, and add those keys with Danish and
English values to the existing dict object used by this component so
localization remains consistent.
apps/storefront/next.config.ts (1)

3-4: Avoid hardcoding a fallback project ID.

The hardcoded fallback "tknxlzoejhauuzloezfi" could cause silent failures if NEXT_PUBLIC_SUPABASE_PROJECT_ID is missing in production, as images would attempt to load from the wrong Supabase project. This also exposes a specific project ID in source code.

Consider failing explicitly at build time if the environment variable is not set:

♻️ Proposed fix
-const supabaseProjectId =
-  process.env.NEXT_PUBLIC_SUPABASE_PROJECT_ID || "tknxlzoejhauuzloezfi";
+const supabaseProjectId = process.env.NEXT_PUBLIC_SUPABASE_PROJECT_ID;
+
+if (!supabaseProjectId) {
+  throw new Error("NEXT_PUBLIC_SUPABASE_PROJECT_ID environment variable is required");
+}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/storefront/next.config.ts` around lines 3 - 4, The current const
supabaseProjectId in next.config.ts uses a hardcoded fallback which can silently
point to the wrong project; remove the default string and instead assert that
process.env.NEXT_PUBLIC_SUPABASE_PROJECT_ID is present (e.g. throw an Error or
exit during config evaluation) so the build fails fast if the variable is
missing; update any usage of supabaseProjectId to read from the validated env
var and include the symbol name supabaseProjectId in your change so it’s easy to
locate.
apps/storefront/src/app/[locale]/checkout/page.tsx (1)

61-66: Consider using Next.js Image component for thumbnails.

Using native <img> bypasses Next.js image optimization (lazy loading, responsive sizing, format optimization). For cart item thumbnails, the next/image component with the configured Supabase remote pattern would provide better performance.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/storefront/src/app/`[locale]/checkout/page.tsx around lines 61 - 66,
Replace the native <img> in the checkout page component with Next.js' Image to
leverage built-in optimization: import Image from "next/image" in
apps/storefront/src/app/[locale]/checkout/page.tsx, swap the <img
src={item.thumbnail} alt={item.title} ... /> usage for <Image> using appropriate
props (src=item.thumbnail, alt=item.title, use layout/fill or width/height and
className="object-cover" as needed) so thumbnails are lazy-loaded and
responsive, and ensure your Supabase remote image pattern is listed in
next.config.js's images.domains or remotePatterns so the external thumbnail URLs
are allowed.
apps/storefront/src/components/product/ProductPurchaseSection.tsx (1)

35-35: Handle empty variants array gracefully.

If variants is an empty array, variants[0]?.id returns undefined, and selectedVariantId will be an empty string. While the add-to-cart button is correctly disabled when !selectedVariantId, consider adding an early return or explicit handling for this edge case to make the behavior clearer.

♻️ Proposed defensive check
 export function ProductPurchaseSection({
   variants,
   // ...props
 }: ProductPurchaseSectionProps) {
+  if (!variants.length) {
+    return <p className="text-muted-foreground">No variants available</p>;
+  }
+
   const [selectedVariantId, setSelectedVariantId] = useState(variants[0]?.id ?? "");
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/storefront/src/components/product/ProductPurchaseSection.tsx` at line
35, ProductPurchaseSection currently initializes selectedVariantId from
variants[0]?.id which silently yields an empty string for an empty variants
array; update the component to explicitly handle an empty variants edge case by
checking variants.length === 0 and returning a clear “out of stock” or disabled
purchase UI (or early return) instead of relying on the falsy selectedVariantId;
also change the state initialization and update logic for selectedVariantId (and
setter setSelectedVariantId) to be explicit (e.g., null or undefined initial
state and a useEffect that sets it when variants change) so the component’s
disabled/early-return behavior is obvious and robust.
apps/storefront/src/app/[locale]/cart/page.tsx (3)

47-54: Hardcoded locale-specific text should use the dictionary.

Line 52 uses a hardcoded locale check instead of the dictionary, which is inconsistent with other strings on this page that use dict.cart.

♻️ Suggested refactor

Add a key like browseAllProducts to the cart dictionary:

                <Link
                  href={`/${locale}/categories`}
                  className="text-sm font-medium text-primary hover:underline"
                >
-                  {locale === "da" ? "Se alle produkter →" : "Browse all products →"}
+                  {dict.cart.browseAllProducts}
                </Link>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/storefront/src/app/`[locale]/cart/page.tsx around lines 47 - 54, The
page uses a hardcoded locale ternary for the "Browse all products" link instead
of the shared dictionary; add a new key (e.g. browseAllProducts) to the cart
dictionary (dict.cart.browseAllProducts) for both locales and replace the inline
conditional in the Link component (where locale and dict.cart are used) with
that dictionary value so the text is consistent with other strings on the page.

90-106: Additional hardcoded locale strings should use the dictionary.

Lines 91 and 104 contain hardcoded locale-specific text. For maintainability and consistency, these should be moved to the dictionary alongside the other strings flagged above.

♻️ Suggested dictionary keys
                  <p className="mt-4 text-center text-xs text-muted-foreground">
-                    {locale === "da" ? "Gratis levering på alle ordrer over 399 DKK" : "Free shipping on all orders over 399 DKK"}
+                    {dict.cart.freeShippingThreshold}
                  </p>
              <Link
                href={`/${locale}/categories`}
                className="mt-4 inline-flex rounded-lg bg-primary px-6 py-3 text-sm font-medium text-primary-foreground hover:opacity-90 border-2 border-transparent focus-visible:border-primary focus-visible:outline-none"
              >
-                {locale === "da" ? "Gå til butikken" : "Go to shop"}
+                {dict.cart.goToShop}
              </Link>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/storefront/src/app/`[locale]/cart/page.tsx around lines 90 - 106,
Replace the two hardcoded locale strings in the JSX (the free shipping line
using locale === "da" and the Link label using locale === "da") with entries
from the existing dict object: add keys like dict.cart.freeShipping and
dict.cart.goToShop (or similar names consistent with existing dict structure)
and use those in place of the ternary expressions, keeping the locale variable
and dict import usage intact so the component reads dict.cart.freeShipping and
dict.cart.goToShop instead of locale === "da" ? "...".

74-77: Hardcoded shipping text should use the dictionary.

The "Calculated at checkout" / "Beregnes" text is hardcoded. Consider adding a dictionary key for consistency.

♻️ Suggested refactor
                      <dd className="text-sm font-medium text-foreground">
-                        {shipping === 0 ? (locale === "da" ? "Beregnes" : "Calculated at checkout") : formatPrice(shipping, locale)}
+                        {shipping === 0 ? dict.cart.shippingCalculated : formatPrice(shipping, locale)}
                      </dd>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/storefront/src/app/`[locale]/cart/page.tsx around lines 74 - 77, Replace
the hardcoded "Calculated at checkout" / "Beregnes" string with a dictionary
lookup: add a new key (e.g., cart.calculatedAtCheckout) to the locale
dictionaries and use dict.cart.calculatedAtCheckout instead of the ternary on
locale in the JSX where shipping is rendered (the block that references
shipping, locale, and formatPrice). Ensure existing logic (shipping === 0 ? ...
: formatPrice(shipping, locale)) remains unchanged and update all locale files
to include the new key with the correct translations.
apps/storefront/src/components/cart/CartItems.tsx (1)

77-81: Consider using dictionary for the "No image" fallback text.

The fallback text uses a hardcoded locale check, which is inconsistent with the rest of the component that uses dict.cart for localized strings. Moving this to the dictionary improves maintainability.

♻️ Suggested refactor

Add a noImage key to the cart dictionary and use it here:

              ) : (
                <div className="flex h-full w-full items-center justify-center text-muted-foreground text-xs">
-                  {locale === "da" ? "Intet billede" : "No image"}
+                  {dict.cart.noImage}
                </div>
              )}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/storefront/src/components/cart/CartItems.tsx` around lines 77 - 81,
Replace the hardcoded locale check in the CartItems component fallback (the div
that currently renders {locale === "da" ? "Intet billede" : "No image"}) with a
localized string from dict.cart (e.g., dict.cart.noImage); add a noImage key to
the cart translation dictionaries used by dict to provide the Danish and English
text, then use dict.cart.noImage in the fallback rendering so it matches the
rest of the component’s localization approach.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/commerce/src/scripts/seed.ts`:
- Around line 211-217: The code dereferences locData[0] after calling
query.graph which can be empty; update the block around the query.graph call
(query.graph, locData, locSets, shippingSet, stockLocation.id) to first verify
locData is an array with at least one element before accessing locData[0]; if
locData is empty, set locSets/shippingSet to undefined (or log/warn and
continue) so the seed run degrades gracefully instead of throwing.
- Around line 156-167: The current logic only calls updateRegionsWorkflow.run
when dkPaymentProviders.length > 0, which leaves a stale Stripe entry if
STRIPE_API_KEY is removed; always synchronize Denmark's payment_providers to the
computed dkPaymentProviders array. Replace the conditional so
updateRegionsWorkflow(container).run({ input: { selector: { id: dkRegion.id },
update: { payment_providers: dkPaymentProviders } } }) is invoked
unconditionally (using dkRegion and dkPaymentProviders), and adjust the logger
to reflect that payment_providers were synced (or unchanged) after the update.
- Around line 236-255: The current check uses existingOptions.length === 0 after
calling fulfillmentModule.listShippingOptions, which will skip creating
"Standard Levering" if any other option exists in the zone; instead, inspect the
returned existingOptions for the specific option attributes (e.g. option.name
=== "Standard Levering" and matching provider_id "manual_manual" and
shipping_profile_id equal to shippingProfile.id) and only call
createShippingOptionsWorkflow(...).run(...) when no matching option is found;
update the logger to reflect whether the specific "Standard Levering" option was
created or already existed (reference existingOptions, listShippingOptions,
createShippingOptionsWorkflow, and the "Standard Levering" name/provider/profile
identifiers).

In `@apps/storefront/src/app/`[locale]/checkout/page.tsx:
- Line 75: The route locale ("da" / "en") must be mapped to a proper Intl locale
before calling formatPrice; add a small mapping helper (e.g., const localeMap =
{ da: "da-DK", en: "en-US" } or similar) or expand formatPrice to resolve short
locales, then replace direct uses of formatPrice(item.total ?? 0, locale) with
formatPrice(item.total ?? 0, localeMap[locale] ?? locale) (update all
occurrences including where formatPrice is called in the checkout page render).
Ensure the mapping is used wherever formatPrice is invoked so Intl.NumberFormat
receives a full locale string.

---

Duplicate comments:
In `@apps/storefront/src/components/CheckoutSteps.tsx`:
- Around line 283-290: The Link rendering currently allows navigation when
paymentReady is true even if paymentContent (Stripe UI) is mounted; update the
conditional in CheckoutSteps to render the confirmation Link only when
paymentContent is absent (i.e., replace {(!paymentContent || paymentReady) &&
(...)} with {(!paymentContent) && (...)}), and if the Stripe flow relies on
paymentReady as a true-confirmation flag, add a TODO or assertion in the
CheckoutWithStripe component where setPaymentReady is called to verify it only
becomes true after successful payment confirmation (refer to symbols
paymentContent, paymentReady, confirmationHref, CheckoutSteps, and
CheckoutWithStripe).

---

Nitpick comments:
In `@apps/storefront/next.config.ts`:
- Around line 3-4: The current const supabaseProjectId in next.config.ts uses a
hardcoded fallback which can silently point to the wrong project; remove the
default string and instead assert that
process.env.NEXT_PUBLIC_SUPABASE_PROJECT_ID is present (e.g. throw an Error or
exit during config evaluation) so the build fails fast if the variable is
missing; update any usage of supabaseProjectId to read from the validated env
var and include the symbol name supabaseProjectId in your change so it’s easy to
locate.

In `@apps/storefront/src/app/`[locale]/cart/page.tsx:
- Around line 47-54: The page uses a hardcoded locale ternary for the "Browse
all products" link instead of the shared dictionary; add a new key (e.g.
browseAllProducts) to the cart dictionary (dict.cart.browseAllProducts) for both
locales and replace the inline conditional in the Link component (where locale
and dict.cart are used) with that dictionary value so the text is consistent
with other strings on the page.
- Around line 90-106: Replace the two hardcoded locale strings in the JSX (the
free shipping line using locale === "da" and the Link label using locale ===
"da") with entries from the existing dict object: add keys like
dict.cart.freeShipping and dict.cart.goToShop (or similar names consistent with
existing dict structure) and use those in place of the ternary expressions,
keeping the locale variable and dict import usage intact so the component reads
dict.cart.freeShipping and dict.cart.goToShop instead of locale === "da" ?
"...".
- Around line 74-77: Replace the hardcoded "Calculated at checkout" / "Beregnes"
string with a dictionary lookup: add a new key (e.g., cart.calculatedAtCheckout)
to the locale dictionaries and use dict.cart.calculatedAtCheckout instead of the
ternary on locale in the JSX where shipping is rendered (the block that
references shipping, locale, and formatPrice). Ensure existing logic (shipping
=== 0 ? ... : formatPrice(shipping, locale)) remains unchanged and update all
locale files to include the new key with the correct translations.

In `@apps/storefront/src/app/`[locale]/checkout/page.tsx:
- Around line 61-66: Replace the native <img> in the checkout page component
with Next.js' Image to leverage built-in optimization: import Image from
"next/image" in apps/storefront/src/app/[locale]/checkout/page.tsx, swap the
<img src={item.thumbnail} alt={item.title} ... /> usage for <Image> using
appropriate props (src=item.thumbnail, alt=item.title, use layout/fill or
width/height and className="object-cover" as needed) so thumbnails are
lazy-loaded and responsive, and ensure your Supabase remote image pattern is
listed in next.config.js's images.domains or remotePatterns so the external
thumbnail URLs are allowed.

In `@apps/storefront/src/components/cart/CartItems.tsx`:
- Around line 77-81: Replace the hardcoded locale check in the CartItems
component fallback (the div that currently renders {locale === "da" ? "Intet
billede" : "No image"}) with a localized string from dict.cart (e.g.,
dict.cart.noImage); add a noImage key to the cart translation dictionaries used
by dict to provide the Danish and English text, then use dict.cart.noImage in
the fallback rendering so it matches the rest of the component’s localization
approach.

In `@apps/storefront/src/components/CheckoutSteps.tsx`:
- Around line 258-270: The new inline Stripe placeholder strings in the
CheckoutSteps component bypass the existing localization dictionary; move both
messages into the shared dict and reference them instead of using the locale
ternaries. Update CheckoutSteps to use dict keys like stripePlaceholderTitle and
stripePlaceholderSubtitle (or similar) where paymentContent is rendered, and add
those keys with Danish and English values to the existing dict object used by
this component so localization remains consistent.

In `@apps/storefront/src/components/product/ProductPurchaseSection.tsx`:
- Line 35: ProductPurchaseSection currently initializes selectedVariantId from
variants[0]?.id which silently yields an empty string for an empty variants
array; update the component to explicitly handle an empty variants edge case by
checking variants.length === 0 and returning a clear “out of stock” or disabled
purchase UI (or early return) instead of relying on the falsy selectedVariantId;
also change the state initialization and update logic for selectedVariantId (and
setter setSelectedVariantId) to be explicit (e.g., null or undefined initial
state and a useEffect that sets it when variants change) so the component’s
disabled/early-return behavior is obvious and robust.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 4eb5f1f and 5b491a7.

📒 Files selected for processing (15)
  • apps/commerce/src/scripts/seed.ts
  • apps/storefront/next.config.ts
  • apps/storefront/src/app/[locale]/cart/page.tsx
  • apps/storefront/src/app/[locale]/checkout/page.tsx
  • apps/storefront/src/app/[locale]/order-confirmation/OrderConfirmationComplete.tsx
  • apps/storefront/src/app/[locale]/products/[handle]/page.tsx
  • apps/storefront/src/components/CheckoutSteps.tsx
  • apps/storefront/src/components/CheckoutWithStripe.tsx
  • apps/storefront/src/components/cart/CartItems.tsx
  • apps/storefront/src/components/product/ProductPurchaseSection.tsx
  • apps/storefront/src/i18n/dictionaries.ts
  • apps/storefront/src/i18n/dictionaries/da.json
  • apps/storefront/src/i18n/dictionaries/en.json
  • apps/storefront/src/lib/cart.ts
  • apps/storefront/src/lib/format.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • apps/storefront/src/lib/cart.ts
  • apps/storefront/src/app/[locale]/products/[handle]/page.tsx
  • apps/storefront/src/components/CheckoutWithStripe.tsx
📜 Review details
🧰 Additional context used
🧬 Code graph analysis (4)
apps/storefront/src/components/cart/CartItems.tsx (2)
apps/storefront/src/lib/cart.ts (2)
  • removeLineItem (99-110)
  • updateLineItem (82-97)
apps/storefront/src/lib/format.ts (1)
  • formatPrice (6-12)
apps/storefront/src/components/product/ProductPurchaseSection.tsx (1)
apps/storefront/src/lib/cart.ts (1)
  • addToCart (66-80)
apps/storefront/src/app/[locale]/order-confirmation/OrderConfirmationComplete.tsx (1)
apps/storefront/src/lib/medusa.ts (1)
  • medusa (13-17)
apps/storefront/src/app/[locale]/checkout/page.tsx (6)
apps/storefront/src/i18n/dictionaries.ts (1)
  • getDictionary (261-263)
apps/storefront/src/i18n/config.ts (1)
  • Locale (9-9)
apps/storefront/src/lib/cart.ts (1)
  • getCart (112-127)
apps/storefront/src/components/cart/CartItems.tsx (1)
  • CartItem (9-22)
apps/storefront/src/components/CheckoutWithStripe.tsx (1)
  • CheckoutWithStripe (40-137)
apps/storefront/src/lib/format.ts (1)
  • formatPrice (6-12)
🔇 Additional comments (13)
apps/storefront/src/components/CheckoutSteps.tsx (1)

58-61: Good refactor for step transitions.

handleStepChange removes duplicated step logic and keeps parent step notifications consistent.

Also applies to: 214-214, 237-237, 244-244, 278-278

apps/commerce/src/scripts/seed.ts (1)

193-207: Good catch handling for idempotent link creation.

Nice improvement: duplicate-link cases are treated as benign while unexpected failures are surfaced.

apps/storefront/src/i18n/dictionaries.ts (1)

110-114: LGTM!

The new dictionary keys for accessibility labels (decreaseQuantity, increaseQuantity), product UI (size, added, purchaseOptions), and order confirmation states (confirming, errorMessage, backToCheckout) are well-structured and properly typed. These additions align with the new Stripe checkout components.

Also applies to: 154-155, 222-224

apps/storefront/src/lib/format.ts (1)

1-12: Verify amount unit handling and consider currency flexibility.

The JSDoc states the amount is in "minor units (e.g. øre)", but the function formats amount directly without dividing by 100. If Medusa returns amounts in minor units (which is common for payment processing), prices will display 100x their actual value.

Additionally, the currency is hardcoded to DKK, which may limit future multi-currency support.

[raise_major_issue, request_verification]

🐛 Proposed fix if amounts are in minor units
-export function formatPrice(amount: number, locale = "da-DK"): string {
+export function formatPrice(amount: number, locale = "da-DK", currency = "DKK"): string {
   return new Intl.NumberFormat(locale, {
     style: "currency",
-    currency: "DKK",
+    currency,
     minimumFractionDigits: 0,
-  }).format(amount);
+  }).format(amount / 100);
 }

Run the following script to check what unit Medusa returns for cart totals:

#!/bin/bash
# Search for how cart totals are used elsewhere to understand the unit convention
rg -n -C3 'cart\.(total|subtotal|shipping_total)' --type=ts
apps/storefront/src/i18n/dictionaries/da.json (1)

113-117: LGTM!

Danish translations are appropriate and consistent with the existing localization style.

Also applies to: 157-158, 225-227

apps/storefront/src/i18n/dictionaries/en.json (1)

113-117: LGTM!

English translations are well-written and consistent with the existing localization style.

Also applies to: 157-158, 225-227

apps/storefront/src/app/[locale]/order-confirmation/OrderConfirmationComplete.tsx (1)

28-53: Cart completion may fail on page refresh.

If a user refreshes this page after the cart has been completed (but before the redirect occurs or completes), medusa.store.cart.complete(cartId) will be called again on an already-completed cart. Depending on Medusa's behavior, this could result in an error state being shown even though the order was successfully placed.

Consider adding a check for the cart's completion status before attempting to complete it, or handling the "already completed" error gracefully.

[raise_minor_issue, request_verification]

#!/bin/bash
# Check if there's any existing handling for completed cart state in the codebase
rg -n "cart.*complete|already.*completed|cart.*status" --type=ts -C3
apps/storefront/src/components/product/ProductPurchaseSection.tsx (2)

43-68: LGTM on timer cleanup and add-to-cart flow.

The timer ref cleanup on unmount properly prevents state updates after unmount. The startTransition usage for the async add-to-cart operation is correct, and error handling appropriately captures and displays errors.


31-32: No action needed. The ProductPurchaseSection caller in apps/storefront/src/app/[locale]/products/[handle]/page.tsx correctly passes localized ARIA labels from the dict object (dict.products.decreaseQuantity and dict.products.increaseQuantity), ensuring proper accessibility across all supported locales.

apps/storefront/src/components/cart/CartItems.tsx (3)

1-7: LGTM!

Imports are well-organized and appropriate for the component's functionality. Using useTransition for non-blocking cart updates is the correct React pattern.


9-36: LGTM!

The interfaces are well-structured. The CartItem interface appropriately handles different data shapes from the Medusa API with fallback paths, and CartItemsProps has explicit typing for the dictionary keys.


42-55: Missing error handling for cart operations.

The handlers for removeLineItem and updateLineItem don't catch errors. If these operations fail, users won't receive any feedback, and the UI won't update as expected.

Consider wrapping the async operations in try/catch blocks and surfacing errors to the user.
[duplicate_comment, raise_minor_issue]

apps/storefront/src/app/[locale]/cart/page.tsx (1)

27-33: LGTM!

Data fetching and defaults are handled appropriately. The nullish coalescing provides safe fallbacks for the cart totals.

Comment thread apps/commerce/src/scripts/seed.ts Outdated
Comment thread apps/commerce/src/scripts/seed.ts Outdated
Comment thread apps/commerce/src/scripts/seed.ts
Comment thread apps/storefront/src/app/[locale]/checkout/page.tsx
- seed: Always sync Denmark payment_providers (enable + disable when key removed)
- seed: Guard empty locData from query.graph; check 'Standard Levering' by name
- format: Map route locale (da/en) to Intl locale (da-DK/en-US) in formatPrice

Made-with: Cursor
@eskobar95 eskobar95 merged commit 6a2663f into staging Feb 27, 2026
6 checks passed
eskobar95 added a commit that referenced this pull request Feb 27, 2026
@eskobar95 eskobar95 deleted the task/m9-stripe branch February 27, 2026 23:01
eskobar95 added a commit that referenced this pull request Apr 15, 2026
* t1.6: staging branch strategy (main=production) (#2)

* t1.6: add staging branch strategy (main=production)

Create staging branch workflow docs: PRs target staging; releases promote staging to main.

* [t1.6] chore: add .sdd/git-config.json for branch strategy

Commands now automatically use staging as the default PR target.
This config tells all SDD helpers and skills that:
- default_branch = staging (PR target)
- production_branch = main (releases)

* feat: SDD guardrails + dynamic batch scheduler (#3)

* feat: add SDD guardrails + dynamic batch scheduler

- Add GitHub Actions: CI (SDD sanity) + PR policy (block feature→main)
- Add PR template for consistent PR descriptions
- Add branch protection setup guide (.github/BRANCH-PROTECTION.md)
- Implement dynamic batch scheduler with worktree support (max 2 parallel)
  - Scheduler script: .cursor/scripts/sdd-scheduler.cjs
  - Worktree helper: .cursor/commands/_shared/worktree-scheduler.md
- Add /task/promote command for staging→main releases
- Update SDD commands/skills to use staging as default PR target
  - Updated: task/start.md, task/validate.md, task/batch.md
  - Updated: sdd-pr-create-or-update/SKILL.md
- Update batch-runner agent for worktree parallel execution
- Add milestone-ready checklist (generic, not milestone-specific)
- Update documentation (spec/08-infrastructure.md, README.md, openmemory.md)
- Update .gitignore to exclude .sdd/worktrees/

* config: enable CodeRabbit auto-review on staging branch

- Add .coderabbit.yaml configuration
- Enable auto-review on both main (default) and staging branches
- CodeRabbit will now automatically review PRs targeting staging

* docs: update branch protection guide with GitHub Rulesets UI

* fix: move review settings under reviews section in coderabbit.yaml

- Move review_status, review_details, high_level_summary under reviews:
- Fixes CodeRabbit validation warning about unrecognized properties

* docs: add JSDoc docstrings to sdd-scheduler functions

- Add docstrings to all exported and internal functions
- Fixes CodeRabbit docstring coverage warning (0% -> should meet 80% threshold)

* fix: address CodeRabbit review issues

- Fix worktree-scheduler.md: correct parallel example (different workspaces)
- Fix worktree-scheduler.md: add language specifier to code block
- Fix task/promote.md: use default_branch instead of development_branch (matches config)
- Fix task/validate.md: add guard to prevent PRs when defaultBranch = main
- Fix sdd-pr-create-or-update skill: use correct config key names (default_branch/production_branch)
- Fix pr-policy.yml: add validation for required config fields
- Fix pr-policy.yml: use env vars for PR data (security)
- Fix sdd-scheduler.cjs: add dependency cycle detection
- Fix sdd-scheduler.cjs: improve touchesGlobalLock with tag check

* feat: Linear agent-ok Cloud Agent delegation pilot (#7)

* feat: add Linear agent-ok Cloud Agent delegation pilot

- Create Linear sync config (work/linear/sync-config.md)
- Add Linear helpers + automation docs for SDD
- Add skill sdd-linear-delegate-cloud-agent (strict @cursor prompt)
- Add /task/delegate command and integrate delegation into /task/start and /task/batch
- Update CI to require work/backlog only on staging (or when present)
- Document pilot guardrails in openmemory + milestone checklist

* fix: avoid shell interpolation of PR body in PR policy

- Pass pull_request.body via env to prevent bash interpreting backticks
- Keeps PR description validation while avoiding injection/errors

* feat(cms): M2 — Payload CMS with content models and homepage builder (#8)

* feat(cms): scaffold Payload v3 CMS with Supabase Postgres adapter

- Add Payload v3 Next.js app in apps/cms workspace
- Configure postgres adapter with schemaName: 'payload' for isolation
- Set up Users and Media collections as baseline
- Add REST API and GraphQL routes
- Configure monorepo workspace (pnpm-workspace.yaml, root package.json)

Task: t2.1

* feat(cms): add content types, PDP guidance, and homepage builder

- Add Pages collection with SEO meta, slug auto-generation, drafts
- Add Articles collection with categories, featured image, author relation
- Add Navigation global for main menu and CTA button
- Add Footer global with link columns, legal links, social links
- Add ProductGuidance collection with skin types, concerns, ingredients,
  how-to steps, AM/PM routine, and pair-with recommendations
- Add Homepage global builder with 8 section types, max 10 sections:
  hero, featured-products, categories, testimonials, content-block,
  newsletter, blog-carousel, brands-banner

Tasks: t2.2, t2.3, t2.4

* fix(cms): address CodeRabbit review suggestions

- Add baseUrl to tsconfig.json for valid paths config (TS5064)
- Add production check for PAYLOAD_SECRET env var (security)
- Remove redundant status field from Pages collection (use built-in _status)

* fix(cms): add validation to Navigation link fields

Add conditional validate functions to page and url fields in Navigation
global to prevent saving items with empty link targets. UI conditions
only hide fields but don't prevent invalid data from being saved.

CodeRabbit: PRRT_kwDORB5x-M5rSHX9

* Upgrade batch-runner and task/batch workflow

- Add pre-flight checks (environment, dependencies, database, git state)
- Add Linear integration with status updates per task and retry queue
- Add comprehensive error handling (retry/skip/abort, cleanup)
- Improve progress reporting (real-time updates, time estimates)
- Add dependency validation before each task
- Add branch naming strategy (milestone-level or per-task)
- Add post-execution validation (build, tests, environment)
- Add state tracking with checkpoints and resume support
- Improve cleanup logic with branch deletion confirmation
- Add confirmation gates in task/batch (execution plan, PR strategy, merge strategy)
- Add pre-flight validation step in task/batch
- Add error handling instructions for batch-runner

* feat: M3 Commerce + M4 Storefront scaffolding (#9)

* chore: update .gitignore with OpenMemory rules and node_modules

- Add .cursor/rules/openmemory.mdc to ignore list (IDE-specific)
- Add node_modules to root gitignore

* feat(commerce): scaffold Medusa backend (t3.1)

- Add apps/commerce with Medusa v2 project structure
- Configure medusa-config.ts with Redis modules:
  - Event bus (BullMQ)
  - Workflow engine
  - Caching
  - Locking
- Configure Supabase PostgreSQL with schema separation (medusa)
- Add seed script for initial data (sales channel, regions, shipping)
- Update env.example with Medusa variables
- Install Medusa dependencies

Part of M3 milestone - Commerce (Medusa): core domain + DB wiring

Refs: GUA-13

* feat(storefront): scaffold Next.js with i18n routing (t4.1)

- Create apps/storefront with Next.js 16, TypeScript, Tailwind CSS
- Implement /da and /en locale routing with middleware
- Add i18n configuration with Danish (default) and English locales
- Create dictionaries for UI translations
- Set up Medusa JS SDK integration
- Create homepage with header, hero, featured sections, footer
- Add placeholder structure for future pages

Acceptance:
- [x] /da and /en routes exist and render
- [x] Language routing strategy matches spec

Part of M4 milestone - Storefront (Next.js): MVP pages + core journeys

Refs: GUA-17

* feat(batch-2): schema separation docs + content pages

## t3.2 (GUA-14): Validate Supabase schema separation
- Document schema separation strategy (medusa schema)
- databaseSchema: "medusa" already configured in medusa-config.ts
- Add SCHEMA_SEPARATION.md with verification steps

## t4.6 (GUA-22): Content pages
- Add blog index and article pages (/[locale]/blog)
- Add policy pages (terms, privacy, cookies, returns)
- Add support pages (FAQ, contact)
- All pages support da/en locales with translations

Part of M3+M4 batch execution

Refs: GUA-14, GUA-22

* feat(commerce): implement core commerce flows (t3.3)

- Update medusa-config.ts to support development without Redis
  - Uses in-memory modules when REDIS_URL is not set
  - Production uses Redis-based event bus and workflow engine
- Enhance seed script with comprehensive test data:
  - Sales channel, stock location, shipping profile
  - Denmark + Europe regions with currencies
  - Product categories (skincare, cleansers, serums, etc.)
  - 4 test products with variants and prices
  - Inventory items with stock levels
- Update env.template with detailed documentation

Medusa provides Store API out-of-the-box:
- GET /store/products - List products
- POST /store/carts - Create cart
- POST /store/carts/:id/line-items - Add to cart
- Full checkout flow with payment collections

Acceptance:
- [x] Products can be fetched/listed
- [x] Cart can be created/updated
- [x] Checkout can create an order (in local/test flow)

Refs: GUA-15

* feat(batch-4): accounts baseline + PLP pages

## t3.4 (GUA-16): Accounts baseline
- Document guest checkout flow (Medusa out-of-box)
- Document authenticated checkout for subscriptions
- Add CHECKOUT_FLOWS.md with implementation notes
- Frontend gating recommended for MVP (prompt login for subs)

## t4.2 (GUA-18): PLP + search + filters
- Categories index + detail pages (/categories, /categories/[handle])
- Brands index + detail pages (/brands, /brands/[handle])
- Concerns index + detail pages (/concerns, /concerns/[handle])
- Search results page (/search) with query + category filters
- Canonical URLs on all PLP pages (per spec)
- Sort dropdown (featured, price, newest)
- Filter UI placeholders

All pages support da/en locales with proper metadata.

Refs: GUA-16, GUA-18

* feat(storefront): implement PDP with subscription selector (t4.3)

## Product Detail Page
- Full product page layout with image gallery placeholder
- Variant selector (size/options)
- Breadcrumb navigation

## Subscription Selector Component
- One-time vs subscription toggle
- 4, 8, 12 week cycle options
- 5% subscription discount display
- Benefit list (save %, flexible delivery, cancel after 2)
- Full da/en localization

## Product Guidance (from CMS)
- Skin types tags
- Target concerns
- Key ingredients with benefits
- How to use instructions
- When to use (AM/PM badge)
- Pair with product recommendations

Ready for Medusa + Payload data integration.

Refs: GUA-19

* feat(storefront): cart, checkout + account area shell (t4.4, t4.5)

## t4.4: Cart + Checkout Shell
- Cart page with line items, quantity controls, subscription badges
- Order summary with subtotal, shipping, total
- Checkout page with multi-step progress (contact, shipping, payment)
- Shipping address form
- Shipping method selection
- Payment placeholder for Adyen integration
- Full da/en localization

## t4.5: Account Area Shell
- Account dashboard with stats and navigation
- Orders page with status badges and order history
- Subscriptions page with management controls:
  - Skip next delivery
  - Pause/resume subscription
  - Change frequency
  - Cancel (after minimum commitment)
- Benefits reminder card
- Full da/en localization

## i18n Updates
- Added account section to dictionaries
- Added cart.summary and checkout.contact fields
- Updated Dictionary type interface

Refs: GUA-20, GUA-21

* fix(commerce): schema separation + idempotent seed script

- Fixed database schema separation (134 tables now in medusa schema)
- Made seed script fully idempotent (safe to run multiple times)
- Added @medusajs/admin-shared dependency for admin dashboard
- Key fix: ALTER DATABASE/ROLE SET search_path TO medusa, public

Closes: t3.2 (schema separation validation)

* chore: mark M3 + M4 tasks as done

Completed tasks:
- t3.1: Scaffold Medusa server + worker
- t3.2: Schema separation (medusa schema)
- t3.3: Core commerce flows
- t3.4: Accounts baseline
- t4.1: Next.js storefront + i18n
- t4.2: PLP + search pages
- t4.3: PDP + subscription selector
- t4.4: Cart + checkout shell
- t4.5: Account area shell
- t4.6: Content pages

* fix: address CodeRabbit review comments

- Add production guard in medusa-config.ts (REDIS_URL, JWT_SECRET, COOKIE_SECRET)
- Pin @medusajs/* dependencies to exact version 2.13.1
- Fix seed.ts: parent_category_id for child categories
- Fix storefront: formatPrice currency, statusLabels fallback, date formatting
- Fix URL encoding in search page
- Preserve query params in middleware redirects
- Fix typo in blog slug page (ingredienserper → ingredienser)
- Remove unreachable empty-state check in categories page
- Update cookie policy to reflect unimplemented trackers
- Update README with correct locale path

Addresses CodeRabbit actionable comments

* ci: add test jobs for commerce and storefront typecheck

* fix: address remaining CodeRabbit comments

- Remove continue-on-error from commerce unit tests in CI
- Fix undefined categoryNames in seed summary (use childCategoryNames)

* fix(ci): make test-commerce and typecheck-storefront pass

- Add --passWithNoTests to commerce test:unit (no tests yet)
- Update pnpm-lock.yaml to match pinned @medusajs/* versions in commerce

* fix(ci): use pnpm version from package.json (packageManager)

Remove hardcoded version: 8 to avoid conflict with packageManager: pnpm@9.15.0

* fix(commerce): idempotent seed – sales-channel link + inventory for all test products

- Always link sales channel to stock location (even when location already exists)
- Ensure inventory for all test products (created + existing), not only newly created
- Addresses CodeRabbit unresolved comments

* chore: restore spec commands (init, refine, plan, audit, evolve, sync)

Co-authored-by: Cursor <cursoragent@cursor.com>

* spec + backlog: design source (Figma repo), M6 milestone, t6.1–t6.8 tasks

- spec/05: design source decision (Ecommercestorefrontdesign)
- spec/07: design implementation source reference
- spec/03: Figma export code quality risk
- milestones: M6 Storefront design system-integration
- tasks: M6 section with t6.1–t6.8 (journey-based: Discovery, Purchase, Account & content, data+touch-ups)

Co-authored-by: Cursor <cursoragent@cursor.com>

* t6.1: design repo as submodule at design/Ecommercestorefrontdesign, document in spec/05

Co-authored-by: Cursor <cursoragent@cursor.com>

* t6.2: design-repo analysis (structure, components, tokens, mockup, sitemap mapping)

Co-authored-by: Cursor <cursoragent@cursor.com>

* t6.3: theme/tokens in globals.css, root layout uses tokens, spec/07 primary confirmed

Co-authored-by: Cursor <cursoragent@cursor.com>

* t6.4: core UI (Header, Footer, Button, Card, Input) token-driven, focus-border, layout integrated

Co-authored-by: Cursor <cursoragent@cursor.com>

* t6.5–t6.8: Home/PLP/Checkout design integration, tokens, layout; M6 tasks done

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix: alle sider bruger shared layout + design tokens (fjern duplikerede header/footer)

Co-authored-by: Cursor <cursoragent@cursor.com>

* chore(M6): checkpoint design 1:1 — update spec, gap analysis, backlog, Linear

- spec/07-design-system.md: add M6 implementation status (checkpoint) and next phase (backend data + wiring)
- DESIGN-TO-STOREFRONT-GAP-ANALYSIS.md: add section 6 (næste fase: backend data + storefront wiring)
- work/backlog/tasks.local.md: M6 checkpoint note; new M6 data/wiring tasks t6.d1–t6.d3 (Medusa seed, Payload handles, Storefront wiring) with Linear GUA-48/49/50
- Linear: created GUA-48 (t6.d1), GUA-49 (t6.d2), GUA-50 (t6.d3) in Backlog for next phase

Co-authored-by: Cursor <cursoragent@cursor.com>

* [M7] Payload–Medusa sync, simplified Products, SkinTypes/Concerns (#10)

* fix(cms): t7.1 Payload opstart + admin layout (GUA-51)

- Root layout: kun children; (site) route group med html/body
- (payload)/layout: import @payloadcms/next/css; custom.scss overrides
- DATABASE_URL check, env.template, migrations + payload CLI
- PAYLOAD_SECRET build placeholder; @payloadcms/ui direct dep
- README + spec/03-risks; tasks.local + milestones M7

Co-authored-by: Cursor <cursoragent@cursor.com>

* docs(t7.2): Payload E2E checklist + schema verification (GUA-53)

- work/backlog/t7.2-log.md: E2E checklist (login, 5 collections, 3 globals, persistence)
- Supabase schema payload verified via MCP (all 8 tables present, writable)
- tasks.local.md t7.2 done; Linear GUA-53 Done

Co-authored-by: Cursor <cursoragent@cursor.com>

* chore(t7.2): update log with Done status and final entry

Co-authored-by: Cursor <cursoragent@cursor.com>

* docs(cms): t7.3 i18n + SEO requirements + Articles meta.image (GUA-52)

- apps/cms/docs/i18n-seo-requirements.md: i18n da/en, SEO meta, structured data
- Articles: add meta.image (upload) + admin descriptions for SEO/social
- work/backlog/t7.3-log.md, tasks.local.md t7.3 done; Linear GUA-52 Done

Co-authored-by: Cursor <cursoragent@cursor.com>

* docs(cms): reference Payload SEO plugin and Localization in i18n-seo-requirements

- Add Official Payload features: @payloadcms/plugin-seo, Localization (content) vs I18n (admin UI)
- Align i18n options with Payload Localization (built-in), not 'i18n plugin'
- Note SEO plugin for future migration (generateTitle/Description/Image, preview)
- Future work: Localization + SEO plugin (t7.4)

Co-authored-by: Cursor <cursoragent@cursor.com>

* docs(cms): t7.4 Payload v3 plugins research (GUA-54)

- apps/cms/docs/payload-plugins-research.md: SEO, Localization, storage-s3/Supabase
- Anbefalinger: SEO plugin M7 eller kort efter; Localization når da/en; storage når media i Supabase
- work/backlog/t7.4-log.md, tasks.local.md t7.4 done; Linear GUA-54 Done

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(commerce): product_type, categories with rank, product_tags (GUA-55)

- Seed product types: cleanser, toner, serum, moisturizer, SPF, eye cream, face mask
- Seed product tags: skin types + concerns (oily, dry, acne, hydration, etc.)
- Categories: Skincare parent + 7 children with rank 0–6; update rank on existing
- Products get type_id, categories, tags via create/update workflows
- Idempotent seed; t7.5 done

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(commerce): product metadata brand, primary_skin_type, primary_concern (GUA-56)

- Brand + primary skin type/concern in product.metadata (not collection)
- Docs: apps/commerce/docs/product-metadata-decisions.md
- Seed: metadata on all four test products; create + update; idempotent
- t7.6 done

Co-authored-by: Cursor <cursoragent@cursor.com>

* M7: Payload–Medusa integration (t7.8–t7.13)

- Env + CMS Medusa proxy: MEDUSA_STORE_URL, /api/medusa/*, 5min cache
- Commerce GET /store/brands
- Collections: Products, Ingredients, Routines, Beneficials, Categories, Brands
- Homepage globals: relationships til Products/Categories/Brands + fallbacks
- Docs: spec/10, medusa-proxy.md; backlog notes + t7.8–t7.13 log

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(commerce,cms): Payload–Medusa sync for products, categories, brands, product types

- Commerce: Payload module (API client), workflows + subscribers for sync
- Commerce: Admin Settings > Payload with sync buttons and status
- CMS: Categories, Brands, ProductTypes collections; access + medusa_id
- CMS: Categories sync with unique handles, name fallback, parent hierarchy
- CMS: create-payload-items step: strict body, dedupe handles, parent as number
- Docs: payload-medusa-sync-plan, payload-api-key-for-medusa, data-model
- Spec: 10-cms-commerce-synergy and related updates

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(cms): add localization (da/en) and i18n admin UI

- payload.config: localization (da default, en), i18n with @payloadcms/translations
- Collections/globals: localized fields (Pages, Articles, Products, Categories, Brands, ProductTypes, Media, ProductGuidance, Ingredients, Routines, Beneficials, Navigation, Footer, Homepage)
- Migrations: 20260205_163856, 20260205_164613, 20260205_165847 (locale tables + baseline doc)

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(M7): Payload–Medusa sync, simplified Products, SkinTypes/Concerns

- Products: simplified schema (skinTypes, concerns, ingredients), update sync only sku/ean
- Categories: create+update sync, never overwrite localized name (handle, parent only)
- Add SkinTypes + Concerns collections; remove ProductGuidance, Routines, Beneficials
- Sidebar: Product Content group, order Products→Categories→Brands→ProductTypes→SkinTypes→Concerns→Ingredients
- Commerce: update-payload-products, update-payload-categories workflows
- CMS: fix React 19/Next.js 15 LayoutProps type in layout.tsx; remove unused generateTitle param

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix: address CodeRabbit PR #10 feedback

- Align Payload packages to 3.74.0 (payload, db-postgres, next, plugin-seo, richtext-lexical, storage-s3, translations, ui)
- Harden isFromMedusa: only accept x-medusa-sync-secret header in production; query param allowed in development only (Products, Categories, Brands, ProductTypes)
- Fix pagination in products-sync-payload: use hasMore = products.length === BATCH instead of offset < total when metadata.count is undefined

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix: address CodeRabbit PR #10 round 2 feedback

- Extract isFromMedusa to shared lib/access.ts; import in Products, Categories, Brands, ProductTypes
- Brands: add readOnly to brandKey, validate brandKey on update when changed
- Categories/Products: remove invalid defaultValue from tabs fields
- Categories: add comment documenting fail-open behavior when Medusa unavailable
- medusaProductHandleExists: add console.warn when Medusa returns empty (validation bypass)
- Products: add unique: true to medusa_id for 1:1 Medusa mapping

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>

* GUA-50 M6 — Storefront wiring (Medusa + Payload) (#11)

* feat(storefront,commerce): PDP reviews, breadcrumbs, brands & Medusa integration

- Commerce: @lambdacurry/medusa-product-reviews plugin, custom POST /store/product-reviews/submit (auth required, no order)
- Storefront: Product reviews section (Matas-style layout, star distribution, review form); submit via /api/product-reviews (auth in separate task)
- PDP: Breadcrumbs Hudpleje/Skincare > Category > Product; category from Medusa
- Commerce: Brand module, store/admin brands API, product-brand link, Payload sync
- Storefront: Medusa brands/categories/products libs, PLP sort, variant selector, quantity selector, payload product by handle
- CMS: Product migrations (description, subtitle), storefront product API, clean-brands
- i18n: reviews labels, breadcrumbRoot (Hudpleje/Skincare)

Auth (login + token for review submit) to be implemented in a follow-up task.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix: address Code Rabbit PR review (security, bugs, UX, migrations, prices)

- Security: clean-brands auth via header only, require SECRET in prod; strip PII email from reviews; remove debug-payload route
- Bugs: medusa-categories don't cache null; product-brand-ingredients fallback on remove; emit brand.deleted after deleteBrands; validate brand name/link; validate new brand before dismiss
- CMS migration: include da/en in title/subtitle WHERE
- UX: Footer bg-surface; Header Heart aria-label Konto; brands [handle] aria-label Breadcrumb; hide brand productCount when 0
- Prices: document major units; mock data in cart/checkout/orders/subscriptions use major units
- create-payload-products: remove unused brand.handle; fetchProductsByCategory/Brand: pass order param; Next.js 16.1.6

Co-authored-by: Cursor <cursoragent@cursor.com>

* chore: update pnpm-lock after Next.js 16.1.6 (fix CI frozen-lockfile)

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(commerce): pin @medusajs/ui to 4.1.1 for Railway npm build

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(commerce): add Dockerfile so Railway includes medusa admin build (.medusa)

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(commerce): disable Medusa admin by default in production so server starts on Railway

Co-authored-by: Cursor <cursoragent@cursor.com>

* chore(commerce): add Railway config (railway.toml + railway-worker.toml, beauty-shop pattern)

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(commerce): disable Railway healthcheck (Medusa has no /health)

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(commerce): Dockerfile validate admin build, NODE_ENV=production; doc index.html troubleshooting

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(commerce): do not set NODE_ENV=production in Docker builder (Medusa requires REDIS/JWT/COOKIE at build else)

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(commerce): validate dist/public/admin and copy to public/admin for runtime loader

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(storefront): force-dynamic for brands/categories to avoid build timeout on Railway

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(commerce): pass MEDUSA_BACKEND_URL at Docker build so admin login uses correct API URL

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(commerce): move ARG/ENV after COPY in Dockerfile to avoid circular stage dependency

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(commerce): rename Docker stage deps to install-deps to avoid Railway circular dependency

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(commerce): add Plunk Next API for invite emails (transactional email)

Co-authored-by: Cursor <cursoragent@cursor.com>

* chore(commerce): add invite-email success log and Plunk troubleshooting doc

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(commerce): strip leading slash from admin path in invite URL

Made-with: Cursor

* Plan: M8 Auth, M9 Stripe, M10 Shipmondo + spec Adyen→Stripe

- milestones.md: add M8 (Customer Auth), M9 (Stripe), M10 (Shipmondo); update M5 scope
- tasks.local.md: add M8/M9/M10 tasks; cancel t5.1–t5.3 (replaced by M9/M10); add Linear refs for t5.4–t5.9
- spec: switch payment provider from Adyen to Stripe (00, 01, 02, 08, openmemory.md)
- spec/03-risks.md: add auth/payment/shipping risks; update time-to-market to Stripe

Made-with: Cursor

* M8 — Customer Authentication (#12)

* chore(M8): add Linear IDs GUA-64–69 to M8 tasks in tasks.local.md

Made-with: Cursor

* feat(M8/t8.1): configure Medusa Auth module (emailpass + Google)

- Add auth module with emailpass and google providers in medusa-config.ts
- Document GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, GOOGLE_CALLBACK_URL in env.template

Made-with: Cursor

* feat(M8/t8.2): add login page (email/password + Google)

- /[locale]/login with email/password form and Log ind med Google button
- LoginForm client component using medusa.auth.login (emailpass + google)
- Auth i18n keys (da/en)

Made-with: Cursor

* feat(M8/t8.3): add register page (email, password, name via Medusa Store API)

- /[locale]/register with form; auth.register + store.customer.create
- Register auth i18n keys

Made-with: Cursor

* feat(M8/t8.4): add Google OAuth callback page

- /[locale]/auth/google/callback: validateCallback, create customer if new, refresh, redirect to account

Made-with: Cursor

* feat(M8/t8.5): auth state management and protected account routes

- AuthProvider + useAuth (customer, loading, isAuthenticated, refetch)
- AccountGate: redirect to login when not authenticated
- Account layout wrapped with AccountGate

Made-with: Cursor

* feat(M8/t8.6): wire account sign out and customer data

- AuthContext.signOut() clears session via medusa.auth.logout
- AccountHeader: sign out button + show customer email
- Unauthenticated redirect already handled by AccountGate

Made-with: Cursor

* docs(M8): add Google OAuth setup guide and env.template pointer

- GOOGLE-OAUTH-SETUP.md: full guide for Client ID/Secret and redirect URIs
- env.template: short pointer to guide for GOOGLE_* vars

Made-with: Cursor

* fix: address CodeRabbit review (OAuth locale, register flow, callback email, i18n, a11y, AuthContext)

- LoginForm: pass callback_url with locale for Google OAuth; auth.orDivider i18n
- RegisterForm: skip customer.create when existing user (shouldCreateCustomer flag)
- Google callback: require valid email when shouldCreateCustomer, throw otherwise
- AccountGate: loadingLabel prop, role=status aria-live=polite, dict.account.loading
- AuthContext: refetch sets loading true; request version guard vs signOut; useAuth throws outside provider
- env.template: note that storefront passes callback_url per request

Made-with: Cursor

* fix(docs,auth): CodeRabbit – Google Identity Services + account layout comment

- GOOGLE-OAUTH-SETUP: replace deprecated Google+ API with Google Identity Services / People API
- account/layout: document client-side auth (AccountGate/JWT); no server-side redirect

Made-with: Cursor

* M9: Stripe Payments – t9.1–t9.6 (checkout, webhook, E2E) (#13)

* M9: scope + subscription tasks (t9.7–t9.9) + m9-linear-log

Made-with: Cursor

* M9: Linear issues GUA-87–GUA-95 oprettet og knyttet til tasks

Made-with: Cursor

* chore(t9.6): replace Adyen with Stripe in spec files and env inventory

- spec/03, 04, 05, 06, 09: Adyen → Stripe
- env.example: Adyen vars → Stripe vars
- CheckoutSteps placeholder text: Adyen → Stripe
- payload-plugins-research, DESIGN-TO-STOREFRONT-GAP: Adyen → Stripe

Made-with: Cursor

* feat(t9.1): add Stripe Payment Module to medusa-config + env vars

- Register @medusajs/medusa/payment-stripe when STRIPE_API_KEY is set
- Document STRIPE_API_KEY, STRIPE_WEBHOOK_SECRET in commerce env.template
- Document NEXT_PUBLIC_STRIPE_KEY in storefront env.template

Made-with: Cursor

* feat(t9.2): enable Stripe in Denmark region via seed + doc

- Seed: when STRIPE_API_KEY set, enable pp_stripe_stripe for Denmark
- Update existing Denmark region with updateRegionsWorkflow
- Add STRIPE-SETUP.md with env vars and Admin alternative

Made-with: Cursor

* feat(t9.3): integrate Stripe PaymentElement in checkout

- Add @stripe/react-stripe-js, @stripe/stripe-js
- CheckoutWithStripe: create cart, initiate payment session, mount PaymentElement
- StripePaymentForm: PaymentElement + confirmPayment with return_url
- API /api/checkout/init: fetch region_id and variant_id from Medusa
- Order confirmation page: complete cart on return from Stripe, redirect to order
- CheckoutSteps: paymentContent prop, onStepChange for step 3

Made-with: Cursor

* docs(t9.4): document Stripe webhook for deployed environments

Medusa Stripe module provides /hooks/payment/stripe_stripe.
Document URL, events, STRIPE_WEBHOOK_SECRET in STRIPE-SETUP.md

Made-with: Cursor

* docs(t9.5): add E2E payment test checklist; M9 subscription todo for t9.7–t9.9

Made-with: Cursor

* chore: update M9 task statuses (t9.1-t9.4,t9.6 done; t9.5 in progress)

Made-with: Cursor

* M9: t9.5 done – E2E Stripe checkout + shipping fix, tasks/Linear sync

- Checkout uses cookie cart, adds shipping method, completes order
- Seed: fulfillment provider + service zone + shipping option (DKK)
- Cart: getCart/getCartId, CartItems, ProductPurchaseSection
- Checkout page: real cart data, cartId prop
- tasks.local.md: t9.5 status → done
- m9-linear-log.md: t9.5 → Done
- Linear GUA-91 set to Done

Made-with: Cursor

* Address CodeRabbit review: cart/seed/checkout/i18n/format

- cart.ts: single res.json() in addToCart; seed: remove duplicate query, improve link catch
- OrderConfirmationComplete: dict for confirming/error/backToCheckout, remove orderId, abort guard + log
- ProductPurchaseSection: timer ref + cleanup; localized aria-labels via dict
- CartItems: CartItem type, formatPrice from @/lib/format, aria-labels from dict.cart
- Checkout page: cartId from cart?.id, CartItem[], shared formatPrice
- CheckoutWithStripe: retry when clientSecret null; CheckoutSteps: gate Place Order link
- next.config: Supabase host from NEXT_PUBLIC_SUPABASE_PROJECT_ID
- i18n: orderConfirmation + products + cart keys for accessibility and copy

Made-with: Cursor

* CodeRabbit follow-up: seed sync/guards, formatPrice Intl locale

- seed: Always sync Denmark payment_providers (enable + disable when key removed)
- seed: Guard empty locData from query.graph; check 'Standard Levering' by name
- format: Map route locale (da/en) to Intl locale (da-DK/en-US) in formatPrice

Made-with: Cursor

* docs(backlog): post-merge note M9 PR #13, workspace ready for t9.7–t9.9

Made-with: Cursor

* M10 — Shipmondo Shipping (t10.1–t10.5 + fix) (#14)

* docs(commerce): add SHIPMONDO.md API research (t10.1 / GUA-76)

- Auth (Basic with API user + key), sandbox vs production URLs
- Endpoints: pickup_points, products, shipments (create, get, list)
- Product codes GLSDK_SD (GLS Pakkeshop), request/response examples
- Env vars and error handling notes

Made-with: Cursor

* feat(commerce): add Shipmondo Fulfillment Module Provider (t10.2 / GUA-77)

- src/modules/shipmondo/service.ts: AbstractFulfillmentProviderService
  - getFulfillmentOptions (GLS + DAO pakkeshop), validateOption, validateFulfillmentData
  - calculatePrice 39 DKK flat, createFulfillment (Shipmondo API), getFulfillmentDocuments
  - cancelFulfillment, createReturnFulfillment stub
- src/modules/shipmondo/index.ts: ModuleProvider FULFILLMENT
- fix seed.ts logger.warn signature for build

Made-with: Cursor

* feat(commerce): register Shipmondo in medusa-config + env (t10.3 / GUA-78)

- Fulfillment module with manual + Shipmondo providers (when SHIPMONDO_* set)
- env.template: SHIPMONDO_API_USER, SHIPMONDO_API_KEY, SHIPMONDO_SANDBOX
- seed: link shipmondo_shipmondo + create Pakkeshop (39 kr) option

Made-with: Cursor

* feat(checkout): pakkeshop search and selection in checkout (t10.4 / GUA-79)

- Commerce: GET /store/pickup-points proxy to Shipmondo (carrier_code, zipcode)
- Storefront: fetchPickupPoints(), CheckoutSteps parcel UI (zipcode search, list, select)
- CheckoutWithStripe: shipping options state, selected option + data, addShippingMethod with data
- Parcel option shows 39 DKK; selected pakkeshop stored on cart via provider data

Made-with: Cursor

* docs(commerce): add E2E test steps for M10 shipping flow (t10.5 / GUA-80)

Made-with: Cursor

* fix(M10): cart clear, guest enrichment, pakkeshop-only, shipping address

- Cart: clear cartId after order, getOrCreateCart skips completed carts
- Guest: order-placed subscriber enriches guest with name/phone from shipping
- Pakkeshop-only: remove Standard/Ekspres from seed, default in checkout
- Shipping address: pakkeshop address when pickup; user address for billing
- Pickup points: Shipping Module Key + fallback to Basic Auth on 422
- Docs: SHIPMONDO.md dry-run, sandbox, env.template updates
- Backlog: t10.1-t10.5 done, t10.6 sandbox-test in progress

Made-with: Cursor

* fix: type-check, CodeRabbit review (pickup validation, timeout, sanitize logs, appliedShipping)

- cart: StoreCart type, getCart return type
- pickup-points route: query validation, carrier whitelist, limit 1-50, 8s timeout
- shipmondo: 10s timeout, truncate error body 300 chars, service_point postal/city first
- seed: throw on Shipmondo link fail
- CheckoutWithStripe: appliedShippingOptionId, validate selection vs opts
- pickup-points lib: try/catch fetch/json return []

Made-with: Cursor

* fix: CodeRabbit review (SHIPMONDO docs, clearCartId try/catch, pickup Basic Auth guard, Shipmondo types, address placeholders)

- SHIPMONDO.md: http code block, credentials note corrected
- OrderConfirmationComplete: clearCartId in try/catch, continue on success
- pickup-points: guard apiUser/apiKey before Basic Auth
- Shipmondo: getFulfillmentDocuments Promise<{name,url}[]>, retrieveDocuments returns
- CheckoutWithStripe: "—" → "" for address placeholders

Made-with: Cursor

* fix(shipmondo): revert to base class signatures (Promise<never[]>, Promise<void>)

Made-with: Cursor

* fix: CodeRabbit (carrier_code precedence, formData re-sync, address validation)

- pickup-points: add parentheses for ?? operator precedence
- CheckoutWithStripe: track lastAppliedFormData, re-run when address changes
- CheckoutWithStripe: validate address/postal/city before payment when no pakkeshop
- Clear paymentError on success

Made-with: Cursor

* fix(CheckoutWithStripe): validate billing address regardless of pakkeshop selection

Made-with: Cursor

* M9: Subscription recurring – admin, cart, renewal, design (#15)

* M9: Subscription recurring – admin, cart, renewal, design alignment

- Commerce: subscription module, admin UI (list/detail/widget), store/admin API,
  renewal + retry + expiration jobs, renew workflow, simulate script, react-router-dom + icon fixes
- Storefront: cart redesign (pill buttons, subscription toggle, oversigt), cart-utils split,
  CheckoutAuthGate/CartCheckoutGate, Button rounded-full + ProductPurchaseSection uses Button,
  i18n cart keys, setLineItemSubscription action

Made-with: Cursor

* fix: address CodeRabbit PR review comments

- Badge: use color orange for skip-next (valid @medusajs/ui value)
- Cart: displayTotal from cart.total, fallback subtotal+shipping; Math.max(0, total - subscriptionDiscountAmount)
- CartItems: lineTotalOriginal from item.total ?? unitPrice*quantity
- ProductPurchaseSection: SubscriptionSelector basePrice from selected variant
- setLineItemSubscription: try/catch with rollback addToCart on failure
- subscriptions.ts: 8s fetch timeout; comment on cookie forwarding
- AuthModal: focus trap (save/restore focus, Tab cycle), first focusable on open
- run-subscription-renewal: comment on Stripe amount (minor units/øre)
- subscription service: TODO for atomic delivery_count/next_renewal_at updates

Made-with: Cursor

* fix: address new CodeRabbit PR comments

- Discount consistency: default 20% for subscriptions (model + order-placed
  subscriber) to match storefront SUBSCRIPTION_DISCOUNT_PERCENT
- AuthModal: add closeLabel to labels (i18n da/en), use for close button aria-label
- AuthModal: restore focus after modal fully closed (separate effect when !isOpen && !exiting),
  add tabIndex=-1 on dialog, no restore in trap cleanup
- subscriptions.ts: encode id path segment (encodeURIComponent) for fetch URL

Made-with: Cursor

* fix: validate cycleWeeks and add per-item error isolation in subscription creation

- Validate subscription_cycle: must be number, integer, and > 0; skip and warn otherwise
- Wrap create + link in try/catch per item so one failure does not stop the loop; log errors

Made-with: Cursor

* Add .agents/ folder for use across branches

Made-with: Cursor

* chore(cursor,agents): project rules + Payload, Stripe, refactor skills

- Cursor rules: architecture, medusa-commerce, payload-cms, storefront-next,
  security, code-structure, nextjs-react-typescript, front-end
- Agent skills: payload, refactor, stripe-best-practices, stripe-integration

Made-with: Cursor

* feat(m11): split commerce backend core changes

Isolates Medusa backend runtime changes (API/modules/lib/jobs/subscribers) from the M11 branch into a reviewable PR under the CodeRabbit file limit.

Made-with: Cursor

* feat(m11): split commerce admin and ops tooling

Isolates Medusa admin routes, scripts, and operational docs from M11 into a smaller review unit for CodeRabbit and domain-focused reviewers.

Made-with: Cursor

* feat(m11): split cms content APIs and schema changes

Extracts Payload schema, fields, and storefront content API changes into a dedicated review PR for CMS-content ownership.

Made-with: Cursor

* feat(m11): split storefront routes and core libs

Moves storefront route handlers, app routes, state contexts, i18n, and core lib updates into a focused PR under CodeRabbit limits.

Made-with: Cursor

* feat(m11): split storefront account checkout cart components

Separates storefront commerce-oriented UI components (account, checkout, cart, subscriptions) into an isolated reviewable PR.

Made-with: Cursor

* feat(m11): split storefront content UI and design assets

Groups content-first storefront components, presentation UI, and design/public assets into a standalone PR for visual/content review.

Made-with: Cursor

* chore(m11): split ai skills, rules, and project metadata part 2

Splits remaining AI skills, Cursor rules, root metadata, lockfiles, and backlog/spec updates for incremental review and merge.

Made-with: Cursor

* fix(m11): address PR17 CI and renewal safety concerns

Updates lockfile for newly introduced commerce dependencies, hardens Stripe client retry behavior, and improves subscription idempotency plus renewal failure compensation semantics.

Made-with: Cursor

* test(m11): include commerce Jest config in backend split PR

Adds the missing Jest transformer config required for TypeScript unit tests in the commerce backend split, fixing CI parse failures.

Made-with: Cursor

* chore(m11): keep backend split PR within review file limit

Moves a non-critical TS env declaration out of PR17 so CodeRabbit can process the split PR within its file threshold.

Made-with: Cursor

* fix(subscriptions): enforce atomic idempotency per order line

Adds a DB-backed idempotency key with a unique partial index and updates order placement subscription creation to be race-safe on order_id + line_item_id conflicts.

Made-with: Cursor

* fix(m11): restore subscription hardening and stabilize admin ops flows

Recover critical subscription reliability safeguards (idempotency, migrations, stripe client hardening, renewal claim/refund protection) and resolve PR18 admin and script robustness issues so CI/ops behavior is explicit and safer.

Made-with: Cursor

* fix(m11): address latest PR18 wizard and refresh races

Apply follow-up CodeRabbit fixes by hardening Shipmondo wizard session invalidation, aligning sender normalization across preview/apply, preventing false-negative UI errors after successful saves, and validating free-shipping DTO payload shape before state updates.

Made-with: Cursor

* fix(m11): resolve latest accessibility and messaging review notes

Add explicit accessible labeling for free-shipping enable switch and improve Shipmondo product fetch fallback error text to be user-facing and complete.

Made-with: Cursor

* fix(m11): harden cms storefront APIs and payload validation

Resolve PR #19 review findings with runtime locale guards, draft-aware article fetching, homepage cache headers, conditional field validation, and shared navigation option cleanup while preserving CMS preview behavior.

Made-with: Cursor

* fix(m11): harden storefront redirects and upstream timeouts

Address critical PR #20 findings by adding request timeouts for free-shipping upstream calls, enforcing safe sign-out redirects, sanitizing rich-text link protocols, restoring visible keyboard focus states, and syncing pnpm lockfile with storefront dependencies.

Made-with: Cursor

* fix(m11): restore storefront components for CI and address PR20 review

- Sync apps/storefront/src/components from M11 source branch so @/components
  imports resolve and typecheck passes.
- globals.css: clip-path for skip-link, search-modal :focus-visible ring, body
  spacing for stylelint.
- free-shipping API: degraded payload constant, inner try/catch for no-cart
  upstream, outer catch returns degraded shape when cartId is null.
- Add .stylelintrc.json ignoring Tailwind v4 at-rules for CodeRabbit/stylelint.

Made-with: Cursor

* fix(storefront): address CodeRabbit major review items

- Stylelint: use at-rule-no-unknown for Tailwind v4
- Next: Medusa host in images.remotePatterns for checkout thumbnails
- A11y: HeaderBackButton, ProfileForm, AccountLayout mobile sheet dialog
- Checkout: required ContactForm callback; noop default in CheckoutSteps
- useShippingOptions: reset selection when cart cleared or fetch fails
- usePaymentSession: fuller form fingerprint; explicit shipping when multiple options
- Pickup: no rethrow in PickupPointManager; prefill catch; sheet search seq + errors
- Content: full-width ContentBlock; blog slug guards; PromotionSlider href + labels
- PostHog: opt_in_capturing on re-consent after init
- Cart/ProductCard: subscription_cycle parsing; toast on quick-add failure
- i18n: checkout.shippingMethodRequired (da/en)

Made-with: Cursor

* fix(storefront): address CodeRabbit PR20 follow-up review

- AccountLayout: normalizePath for nav/title; close sheet on md+; focus trap + restore
- CheckoutOrderSummary: dict keys for VAT/shipping helper copy; narrow cart type
- Pickup: sync carrier when point cleared; sheet close invalidates fetch seq; keep point if still in results; safer zip fallback
- ContentBlockSection: CTA href scheme/locale; single column without image
- PromotionSlider: accessibleLabel; play/pause; focus/touch pause; lazy offscreen imgs
- ProductCard: wishlist outside image link; split add vs sync errors; viewProduct a11y; unit price ??
- i18n: checkout + promoSlider pause/play strings

Made-with: Cursor

* fix(storefront): address PR #21 CodeRabbit review (stripe, pickup, cart, payment cache)

- Reset Stripe onProcessing in finally; delivery edit goes to step 2
- Fail fast UI when NEXT_PUBLIC_STRIPE_KEY is missing
- Include email/phone/marketingOptIn in payment-session form signature
- CartDropdown: error handling for remove and clear cart
- Profile/pickup: treat refetch failure separately after successful update
- Pickup sheet hooks: stale-request guards; sync from form without clobbering search
- Expand pickup-points lib (multi-carrier fetch, distance, carrier label)

Made-with: Cursor

* fix(storefront): dict keys for cart/checkout, Header dict, geocode timeout

- Extend Dictionary cart/checkout (i18n) for CartDropdown, CheckoutWithStripe, usePaymentSession
- Pass getDictionary from locale layout to Header and CartDropdown
- Checkout page: pass full dict to CheckoutWithStripe; drop confirmationHref
- CartDropdown: dictionary for VAT/shipping lines; updateLineItem 2-arg only
- CheckoutOrderSummary: use checkout.calculatedWhenDeliverySelected and vatIncludedBreakdown
- Add lib/cart-errors; client StoreCart type in lib/cart-data
- pickup-points: 5s AbortController timeout on geocode fetch

Made-with: Cursor

* fix(storefront): restore commerce modules, pass CI typecheck

- Exclude .next from tsc; add missing lib/contexts/hooks/ui (cart, shipping, account)
- cart: clearCart, updateLineItem metadata; cart-data StoreCartItem types
- medusa: withMedusaBackendUrl; buttonVariants + cva; @base-ui/react for dialog/sheet
- Auth: signOut redirectTo, AuthModal/Login/Register returnUrl; remove unused AuthAwareShell
- i18n: cart/subscriptionDetail keys; account layout profile/addresses; subscription dict wiring
- CheckoutWithStripe, PickupPointSheet, AddToCartModal fixes

Made-with: Cursor

* fix(storefront): use relative import for cart-data in cart server module

Made-with: Cursor

* chore: exclude storefront design-assets from CodeRabbit path review

Made-with: Cursor

* fix(storefront): exclude .next from tsc (CI typecheck)

Made-with: Cursor

* chore: sync pnpm-lock.yaml with apps/commerce package.json (CI frozen-lockfile)

Made-with: Cursor

* fix(m11): address open CodeRabbit threads on docs and patch

Made-with: Cursor

* fix(commerce): resolve open PR review blockers

Made-with: Cursor

* fix(m11): restore valid review patch application

Made-with: Cursor

* chore: sync pnpm-lock.yaml with storefront package specifiers

Made-with: Cursor

* chore: sync pnpm-lock.yaml with storefront package specifiers

Made-with: Cursor

* chore: sync pnpm-lock.yaml with storefront package specifiers

Made-with: Cursor

* chore: sync pnpm-lock.yaml with storefront package specifiers

Made-with: Cursor

* fix(storefront): remove duplicate ProductPurchaseSection props

Made-with: Cursor

* fix(storefront): wrap checkout auth gate in Suspense for useSearchParams

Made-with: Cursor

* fix(storefront): suspense-wrap account gate in layout

Made-with: Cursor

* fix(admin): patch product reviews widget null image crash on install

Made-with: Cursor

* fix(commerce): copy scripts before npm install in Docker build

Made-with: Cursor

* fix(admin): guard product details widget against runtime data shape errors

Made-with: Cursor

* fix(admin): patch product review table null product/order relations

Made-with: Cursor

* fix(cms): scope global reset to site layout only

Prevent the site-level CSS reset from leaking into Payload Admin, which collapsed spacing and made the CMS UI render incorrectly.

Made-with: Cursor

* feat(commerce): add destructive purge script for subscriptions

Add medusa exec script to delete all subscription rows and product↔subscription
order links, gated by CONFIRM_PURGE_SUBSCRIPTIONS=DELETE_ALL_SUBSCRIPTIONS.
Respects DATABASE_SCHEMA for table qualification.

Made-with: Cursor

* feat(admin): Products brand overview with linked brand column

Add nested UI route under Products that lists products with GET /admin/products
and fields including +brand.* for at-a-glance brand in the admin table.

Document Payload admin CSS scoping and this list workaround in openmemory.md.

Made-with: Cursor

* fix(commerce): reconcile inventory reserved counts after order purge

Direct deletes of reservation_item left inventory_level.reserved_quantity stale.
Add SQL reconciliation (sum of active reservation_item per level) to purge-orders
and a standalone medusa exec script for one-off repair.

Made-with: Cursor

* feat(commerce): order confirmation invoice PDF + admin document downloads

- Attach invoice PDF to order confirmation email via Plunk; drop PDF links from HTML for guests
- Extend Plunk client and sendTransactionalEmail with optional attachments
- Add GET /admin/orders/:id/documents/:type and order-documents widget (order.details.side.after)
- Refactor store document route to use getOrderDocumentBase64 helper
- Update PLUNK-EMAIL.md and openmemory for worker vars and behavior
- Tighten admin tsconfig include/exclude

Made-with: Cursor

* feat(commerce): professional order PDF layout with seller config and VAT lines

- Replace minimal PDFKit text dump with branded header, two-column seller/customer, meta band, item table, totals, footer
- Add pdf-seller-config (GUAPO_INVOICE_* env), pdf-payment-label for card hints from order metadata
- Pass tax_total, discount_total, variant title from graph; da/en PDF locale from shipping country
- Document env vars in env.template; extend openmemory

Made-with: Cursor

* docs: Danish market defaults for invoice PDF env; Railway server+worker parity

- Clarify DK-focused PDFs and da/en locale in commerce env.template
- Inventory GUAPO_INVOICE_* and Plunk/STOREFRONT in root env.example
- Move Plunk + STOREFRONT + GUAPO_INVOICE to shared Railway table; note worker runs order emails/PDFs
- Update openmemory deploy note

Made-with: Cursor

* feat(commerce): PDF seller from region metadata + admin regenerate endpoint

- resolve-pdf-seller: merge Region metadata guapo_invoice over env defaults
- order-document-generation: shared buffers for subscriber + regenerate route
- POST /admin/orders/:id/documents/regenerate (auth); middleware for POST
- Admin order-documents widget: Regenerate PDFs + clearer empty state
- openmemory: document PDF storage vs regenerate flow

Made-with: Cursor

* fix(commerce): PDF region metadata flat keys + correct line amounts from graph

- resolve-pdf-seller: read guapo_invoice or same keys flat on region metadata; parse vat_rate_percent string; normalize website URL
- order-document-generation: graph raw_* + item/detail join, flattenOrderItemFromGraph + toAmountMajor; shipping_address subfields
- subscriber: subscription line detection uses flattened item metadata
- env.template + openmemory: document region vs env seller config

Made-with: Cursor

* feat(commerce): PDF seller from region only, SVG logos, refreshed layout

- pdf-seller-config: defaults without GUAPO_INVOICE env; deprecated resolvePdfSellerProfile alias
- build-order-pdf: sharp rasterizes SVG logo_url; header/cards/table polish; remove env footer hint
- resolve-pdf-seller: base from getDefaultPdfSellerProfile
- sharp dependency + lockfile; env.template, env.example, DEPLOY-RAILWAY, openmemory updated

Made-with: Cursor

* fix(commerce): PDF layout — header, seller/customer, lines, margins

- Header: vertical accent (no line through title), drop internal ref, symmetric logo
- Seller/customer: no card boxes; single CVR line; DK → Danmark; no forced https on website
- Region merge: numeric postal_code stringified for seller address line
- Table: INNER_PAD + TABLE_WIDTH for symmetric A4 columns; taller rows with optional variant subtitle
- Lines: brand + product from graph, subtitle variant; extra graph fields for variant/product/brand

Made-with: Cursor

* fix(commerce,storefront): subscription discount sync + metadata parsing

- Extract applySubscriptionLineDiscountsForCart; reuse in cart.updated subscriber
- Add POST /store/carts/:id/subscription-discount/sync for synchronous line adjustments (staging/RD)
- Normalize subscription_cycle number|string across commerce and storefront
- Call sync after addToCart (subscription) and setLineItemSubscription; improve poll
- Add inspect:order-subscription script for ops debugging

Made-with: Cursor

* fix(commerce): create subscriptions in transactional order.placed path

- Extract createSubscriptionsForPlacedOrder + listSubscriptionsForOrder
- Run creation before PDFs/emails so worker process always persists Subscription rows
- Send subscription_created email only when Subscription rows exist (avoid false positive)

Made-with: Cursor

* feat(commerce): transactional email redesign + order line thumbnails

- Replace newsletter hero with compact header; admin tone skips closing banner
- Order confirmation: line items, totals, VAT; mobile-friendly tables + optional product thumbnails
- Resolve relative thumbnail URLs via MEDUSA_BACKEND_URL or S3_FILE_URL
- Subscription created email: product, cycle, next renewal, discount from subscriptions + catalog
- Document Plunk/HTML layout in PLUNK-EMAIL and openmemory

Made-with: Cursor

* feat(commerce): dark mode styles for transactional HTML email

- prefers-color-scheme: dark overrides for shell, hero, main prose, and links
- Order summary block: line titles, totals strip, thumbnail borders
- Meta color-scheme light dark; CTA link keeps white on navy button

Made-with: Cursor

* storefront: extend product search with brand matching and scoring

- Merge Medusa q= results with products from matching brand handles
- Score and sort brands (exact name/handle, whole-word tokens, substring)
- Share brand list cache with getBrandLookup; document in openmemory.md

Made-with: Cursor

* feat(storefront,cms): harden category PLP and align Payload category SEO

- Storefront: 404 unknown category handles; Medusa-only products; Payload body + SEO metadata; cache shared loaders
- CMS: editorial Categories tabs, SEO plugin generate title/description, live preview for category PLP
- Docs: README workflow for category sync and content

Made-with: Cursor

* chore: add Cursor workspace settings

Made-with: Cursor

* storefront: hide category PLP filters behind env flag

- Add NEXT_PUBLIC_ENABLE_CATEGORY_PLP_FILTERS (default off)
- Keep getFilterCategories and FilterSystem wired when enabled
- Sort-only bar when disabled; fix empty state copy without filters

Made-with: Cursor

* fix(storefront): show Payload category SEO on PLP and normalize localized meta

- Parse flat or locale-nested meta from Payload REST
- Use absolute document title when meta.title is set (avoid double | Guapo from layout template)
- Render SEO title + description block below product grid

Made-with: Cursor

* feat(cms,storefront): video option for Image + Text (Breakout) section

Allow MP4/WebM/MOV in Media; visualType image|video in Payload block.
Storefront renders looped muted autoplay video with same layout as image.

Made-with: Cursor

* chore: add remotion-subscription-demo workspace package

Root scripts for dev/render; packages/* in pnpm workspace.

Made-with: Cursor

* fix(cms): migrate image-text-breakout visual_type and video_id columns

Staging failed listing Pages: missing DB columns after block field changes.
Adds enums + columns for pages, _pages_v, and homepage block tables.

Made-with: Cursor

* storefront: swipeable promo slider, add-to-cart loaders, cart refresh after order

Made-with: Cursor

* fix(storefront,cms): category PLP title fallback, body below grid, Payload fallback-locale

- Add fallback-locale on category fetch (align with other Payload loaders)
- H1/metadata: Payload name, else stripped SEO title, else Medusa
- Move Lexical body below product grid; SEO snippet avoids duplicate H2 vs H1
- CMS: clarify body field placement in admin copy

Made-with: Cursor

* fix(storefront,cms): reliable Payload category fetch via storefront API

- Add GET /api/storefront/category/[handle] using Local API (same as product)
- Resolve PAYLOAD base URL from NEXT_PUBLIC_PAYLOAD_API_URL or PAYLOAD_API_URL
- env.template: document both vars for Railway

Made-with: Cursor

* fix(storefront): disable Next fetch cache for Payload category enrichment

Made-with: Cursor

* fix(storefront,cms): join category PLP to Payload by medusa_id

Sync can store a suffixed handle in Payload vs Medusa URL handle; lookup by medusa_id fixes empty docs

Made-with: Cursor

* fix(storefront,cms): keep category meta title/description in head only

Remove visible SEO duplicate block; clarify Payload admin copy

Made-with: Cursor

* feat(storefront): align brand PLP with category layout and Payload SEO

Brand pages reuse category PLP structure (sort, optional filters, Lexical body
below grid) and load CMS copy via storefront brand API with medusa_id join.
Commerce sync now sends medusa_id when creating Payload brand rows. Add CMS
migration for brands.medusa_id and brands_locales.meta_image_id (SEO OG image).

Made-with: Cursor

* fix(commerce): return priced variants on store products by-brand route

Brand PLP called /store/products/by-brand without pricing context, so
calculated_price was missing and storefront showed 0 kr. Align with
GET /store/products: QueryContext from region_id, sales channel filter,
inventory and tax helpers.

Made-with: Cursor

* fix(commerce): by-brand PLP — resolve product ids via brand graph then priced query

Product-level brand handle filter returned empty results in production; load
linked ids from brand first, then graph products by id with pricing + channel.

Made-with: Cursor

* fix(commerce): align by-brand store route with core GET /store/products

Use FeatureFlag index-engine branch (query.index vs query.graph), correct
sales_channel filter shape per path, set pricingContext on request, and
wrapVariantsWithInventoryQuantityForSalesChannel like the official product list.
Removes two-step id workaround that could return empty or diverge from core.

Made-with: Cursor

* fix(commerce): brand PLP products via two-step query.graph

Made-with: Cursor

* fix(commerce): brand PLP uses product_sales_channel like core store products

Made-with: Cursor

* feat(cms): product SEO fields and PDP metadata

Add Payload plugin-seo meta group to Products (localized title, description, OG image).
Migration adds columns on products_locales. Storefront product page uses SEO for HTML
meta and Open Graph; live preview includes products.

Made-with: Cursor

* fix(commerce): brand products via product_brand link; surface storefront fetch errors

Made-with: Cursor

* fix(storefront): absolute OG image URLs and populate Payload SEO image

- CMS storefront product API: resolve meta.image when stored as id only; depth 4
- PDP metadata: metadataBase, absolute canonical/og:url, normalize image URLs
- Fallback og:image to Medusa thumbnail/gallery when CMS OG image unset

Made-with: Cursor

* fix(commerce): by-brand link query fallback; brand PLP shows API errors

Made-with: Cursor

* fix(commerce): by-brand inventory wrap with single sales_channel_id

Made-with: Cursor

* fix(commerce): brand PLP avoids inventory wrapper; return by_brand_error details

Made-with: Cursor

* fix(commerce): by-brand query.graph uses variants.* field paths

Made-with: Cursor

* fix(store): brand PLP uses product-ids then core /store/products

Made-with: Cursor

* feat: CMS GTM tracking, email preview script, launch spec updates

- Payload Tracking global + migration + storefront API; load GTM after consent
- Transactional email preview script and commerce tmp/gitignore tweaks
- Mark open questions resolved and MVP quality gates checked in spec

Made-with: Cursor

* feat(storefront): PostHog e-commerce events and pageviews

- Manual $pageview on App Router; disable default capture_pageview
- product_viewed, add_to_cart, checkout_started, order_completed (existing)
- user_registered, user_logged_in (email + Google callback)
- search_results_viewed, search_submitted (modal, page form, results)
- wishlist_updated, wishlist_page_viewed; cart_viewed (page, drawer, modal)
- identify/reset on login state; medusaProductId on PLP cards

Made-with: Cursor

* docs(storefront): add PostHog setup guide and register locale super property

Made-with: Cursor

* storefront: simplify search modal suggestions and shortcuts

Made-with: Cursor

* chore: ignore and untrack work/backlog for promotion to main

Made-with: Cursor

* fix(ci): SDD sanity checks skip gitignored work/backlog

Made-with: Cursor

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant