# Storefront personalisation — local repair, acceptance in progress

Updated 24 September 2026. **Current result:** unpaid customer-authenticated checkout, both placed-order artwork openings, independent return-line inspection and synthetic zero-refund completion pass locally. Payment-screen/provider acceptance, actual shipment and paid-order return/refund remain pending; see the [current pending list](../learner-review/appmint-sell-custom-products-online.md). The sections below preserve dated repair history, followed by the latest live results.

## Reproduced and repaired

- A direct `/store/personalised-welcome-sign` load intermittently crashed with `Cannot assign to read only property 'searchParams'` (and later `page`). Browser debugger traced React Flight resolving a shared asynchronous storefront element's frozen props. The page now awaits storefront rendering before handing its content to the page shell. No framework files or production service were changed. Three consecutive local product reloads passed with zero new page exceptions and both personalisation fields mounted.
- Missing name/file inputs were a consequence of failed hydration: the attribute endpoint returned the correct definition. They render after the page repair; no invented API workaround was required.
- Selecting the saved 45 cm option left the displayed price at45 despite its12 surcharge. Product and quick-view pricing now include selected option surcharges; an explicit variation price, including0, wins. Six source regressions cover45/57, percentage and negative changes, variation precedence, zero, malformed options and case-insensitive matching.
- The size control ignored the configured title and dropdown display. It now displays **Sign size** and its saved options in a labelled select.
- The default product page fabricated customer counts and free shipping/returns promises. Those generic claims are removed; Shipping and Returns show merchant information or neutral guidance.
- Anonymous artwork upload displayed a sign-in instruction with no link. The message now links to customer login with a return address for the product. Customer sign-in and both uploads passed locally.

## Evidence

- [Required-name rejection and57 price](assets/store-required-name-fixed.png) — visually inspected. Cart remained empty.
- [Before: failed page](assets/store-client-error-before.png).
- [Before: product without hydrated controls](assets/store-product-before.png).
- Product configuration, gallery, shipping default and site attachment saved and reloaded in the owned recovery organization.

Source: base-app `src/app/[[...slug]]/page.tsx`, storefront `utils/product-helpers.ts`, `hooks/useProductVariation.ts`, boutique product/detail/attribute components and `CustomAttributeInput.tsx`. Regression command: `node --test src/components/storefront/__tests__/product-selection-price.test.cjs` (6 passing). Full base-app typecheck passed after the repairs.

Customer Ada Okafor registered once, followed the locally captured magic link and returned to the product. Both artwork uploads completed. At that checkpoint, the first cart line retained The Okafors/30cm/quantity1/okafor-crest.svg and the second retained Welcome Home Maya/45cm/quantity2/maya-flowers.svg. Both are now preserved on order RPSDCGHC4.

## Historical checkpoint — cart corrections, 21 September

The server returned the correct159 merchandise subtotal,8 shipping and167 total. The drawer reused stale pricing after adding the second design, matched both lines by SKU and labelled the shipping-inclusive167 as Products. `Cart.tsx` now reprices on open-cart changes, ignores late responses, matches personalised options with `findPricedProductLine`, shows `productSubtotal`, and disables checkout when pricing fails. Five additional line-matching tests pass (11 tests total). Full reload, quantity2→1→2 (total167→110→167), and close/reopen passed. [Actual corrected cart](assets/store-two-cart-lines-fixed.png), [sanitized server quotes](assets/store-cart-proof.json).

Checkout accepted the test US address and continued to Payment Method. At the 21 September checkpoint, no gateway was configured and no order/payment had been submitted. The later unpaid order is documented below. The checkout summary dropped size options because the server returned `options` without legacy `attributes`; it now retains those selections. An empty payment-method panel now explains that online payment is unavailable. Both final UI checks passed in the reloaded browser: sizes30-cm/45-cm appear beside the correct names/files, and the empty payment state is visible. [Checkout evidence](assets/store-checkout-no-gateway.png).

## Historical checkpoint — remaining work on 21 September

- At this checkpoint, sandbox checkout, operator artwork retrieval, shipment and return/refund were unverified. Operator cart and placed-order retrieval subsequently passed, as documented below. The current pending list linked at the top supersedes this checkpoint.
- The seeded-layout preview problem is now [fixed and verified locally](account-layout-preview.md); the standalone page remains intact.
- Offline-payment source calls an unimplemented `/api/payments/offline` route; it has not been configured or falsely counted as a paid alternative.

The organization now has its own loopback SMTP config (`tutorial-local-mail-capture`,127.0.0.1:2527) so subsequent rehearsal messages reach the private catcher. Registration was not repeated. A full callback-URL console log containing session data was removed from `auth/complete-login/page.tsx`.

Do not count the course as finished based on these repairs.

## Streamed-page hydration repair

A separate checkout/login hydration warning showed `data-source` changing the seeded page's text and `data-loading` attribute before React hydrated it. PageClient now marks its content pending until its mount effect; the runtime dispatcher skips pending subtrees and scans them when released. Actual checkout reload produced zero new hydration exceptions. Standalone browser checks prove plain HTML bindings still run immediately, initial marked HTML stays unchanged, and both initial and newly streamed subtrees bind after release. [Evidence](assets/store-runtime-hydration-proof.json). Source `src/runtime/dispatcher.ts` and `src/app/[[...slug]]/page-client.tsx`; compiled `public/runtime/appmint.js` and map rebuilt with esbuild.

## Operator cart and private artwork — verified locally

Studio initially displayed an empty server cart despite two browser-persisted customer lines. The customer drawer now persists its latest server quote through the existing cart-update API; writes are serialized. Customer email is read from the actual `user.data.email` field. Studio showed the original customer cart, two lines, and $167 at this pre-checkout checkpoint.

Cart repricing discarded the cart ID and created unrelated empty records. DiscountService now forwards it to lookup, and cart get/update passes the selected record's ID to calculation. Full backend compile and eight totals tests pass, including explicit cart-ID reuse. Two pre-fix extra records remain as diagnostic evidence.

Shared Studio artwork controls resolve authenticated, 15-minute file links and show filenames and Retry on access preparation failure. The first real opening returned NoSuchKey because legacy upload paths were relative to the customer folder. Legacy cart/order attachments now resolve using the customer email; new storefront uploads preserve the complete customer-folder path. After correction, clicking both actual links opened SVG documents (one SVG root each). No public sharing was enabled.

Evidence: [before: empty operator cart](assets/store-owner-empty-cart-before.png), [operator cart and artwork](assets/store-owner-artwork-fixed.png), [sanitized opening proof](assets/store-owner-artwork-proof.json).

**Historical pre-checkout checkpoint, superseded 24 September:** placed-order retrieval had not yet been tested here. It subsequently passed on RPSDCGHC4; see **Order artwork and unpaid balance** below. The seeded portal-layout issue is closed; see [layout repair](account-layout-preview.md).

## Saved-cart restoration and fresh uploads — passed

The restored browser snapshot had no local items, although the original two lines remained on the server. Added an explicit **Restore saved cart** action to the empty cart, with a sign-in link for signed-out customers. The new read-only `GET storefront/cart/mine` resolves the authenticated customer and account context; it accepts no arbitrary owner/id and creates no record. Six source tests verify customer/organization/account isolation and empty results. Restoring a late response cannot overwrite locally added items.

Actual browser acceptance used a fresh email magic link for the existing customer and the restore button, not injected cart data. Both designs returned, including size options that the drawer previously hid on server-restored lines. Totals167→110→167 and full reload all retained cart6ab1af3d9e37fe9e050e2ec6. Studio continued to show the same three records (the original and two pre-fix diagnostic records). [Restored cart](assets/store-cart-restored.png), [cart-ID/total sequence](assets/store-cart-continuity.json).

Fresh upload acceptance used a temporary third line **Artwork recovery check**,30cm×1, with `studio-access-check.svg`. Studio opened the actual SVG and its expected text. The temporary line was removed through the customer cart; original two lines and167total were confirmed again. The uploaded test file is retained as evidence; no order, charge, shipping action or refund occurred during this earlier upload-only check. The later unpaid order is documented below. [Fresh upload/operator opening](assets/store-fresh-artwork-operator.png), [sanitized proof](assets/store-fresh-artwork-proof.json).

Validation: six recovery-isolation tests, eight cart totals tests and eleven product/line-pricing tests pass; backend compilation and frontend typecheck pass. Latest source changes: AppEngine storefront controller/service/cart service and saved-cart.spec; base-app Cart, storefront-api, cart-store.

The final screenshot check also exposed a missing catalogue placeholder asset on the unrelated catering product. ProductCard now uses the existing `/images/placeholder.svg` and falls back to it when a single product image fails. Browser readback confirmed a loaded placeholder; the actual sign gallery image is unchanged. Final frontend typecheck passed.

## Selected-cart checkout regression — 24 September recovery

Checkout recalculated the selected cart without forwarding its ID. With the two diagnostic customer carts still present, this could price the submitted artwork but clear a different cart. `StorefrontOrderService.orderUpdate` now passes the already selected cart ID into `calculateCart`. A regression exercises a customer with competing cart records and checks the preserved artwork options, new/unpaid order state, selected-cart clear call and absence of a payment transaction. The regression passes (1 test); the full backend TypeScript compilation passes.

**Historical source-validation checkpoint:** this regression preceded the restored-browser checkout. The once-only submission and its live readback are now complete below. Do not submit the cleared original cart again.

## Unpaid order creation — verified locally, 24 September

Restored the local renderer and API after the crash. A live read confirmed zero orders and the original cart's two lines/$167 before submission. Signed the existing fictional shopper in through the real browser login and the newly captured local magic link. Submitted `checkout-cart` once from that authenticated customer browser, without a gateway, reference or tender. This verifies the server checkout contract; it does **not** claim the no-gateway payment screen submitted a paid sale.

The resulting order **RPSDCGHC4** (`6ab539fed097781cbba687cb`) is **New**, total **$167**, with no payment records or provider reference. Readback found exactly one order. The selected cart `6ab1af3d9e37fe9e050e2ec6` returns404 after checkout clears it. Customer **Orders → RPSDCGHC4** shows both personalisations, quantities1/2, filenames, $159 merchandise and $8 shipping. [Actual customer order](assets/store-unpaid-customer-order.png); [sanitized order evidence](assets/store-unpaid-order-proof.json).

Operator attachment opening subsequently passed, as documented in the next section. Payment, actual shipment and provider refund have not been executed. Do not resubmit the original cart or mark this order paid merely to advance the tutorial.

## Order artwork and unpaid balance — passed

Studio **Storefront → Order → Order Management → #RPSDCGHC4 → Items (2)** retained both personalisations after the selected server cart was cleared. Clicked each actual **Open artwork** link. Both opened one SVG root, with the expected text: **The Okafors** and **Welcome Home Maya / A PLACE TO BLOOM**. The links use authenticated, temporary file access; signed URLs are omitted from evidence. [Operator Items screen](assets/store-unpaid-operator-artwork.png); [file-opening proof](assets/store-unpaid-artwork-proof.json).

The **Payments** tab explicitly displays **Paid$0.00**, **Balance$167.00**, **Unpaid**, and no payments. No gateway, payment reference, charge, shipping action or refund was submitted. The pending-course file now contains only the unverified payment/shipment/return work.

This pass also found **Invalid Date** in the order header/Created timeline: the component read legacy `createdAt` while persisted records use `createdate`. Both reads now prefer `createdate` with the old field as fallback. After restarting the isolated Studio, the same order header displays **9/24/2026**. The repaired date and both loaded artwork links are visible in the operator screenshot. [Payments readback](assets/store-unpaid-payments.png) confirms Paid$0/Balance$167.

## Independent inspection for repeated product SKUs

Further local review found an application defect beyond the provider gate: return inspection keyed both the UI decision and backend lookup by SKU. Two personalised lines sharing `SIGN-WELCOME-01` could not keep separate accept/reject decisions; the backend repeatedly changed the first matching line.

The UI now labels each return line with its position, name, quantity and price and sends its index. The backend resolves that exact RMA line, validates its matching SKU, requires one boolean decision and valid condition per line, and rejects missing/duplicate/ambiguous decisions before any state mutation. Older callers with unique SKUs remain supported. Refund estimation includes only explicitly accepted lines.

Eight regressions pass, including reject$45/accept$57 for repeated SKUs, six malformed/ambiguous request cases and unique-SKU compatibility. Full backend compilation and TSX syntax check pass. Browser acceptance began with the separate, explicitly synthetic **RMA-TUTORIAL-LINES-20260924** in Received state solely to test inspection; its later zero-refund completion is recorded below. It has no linked paid order, actual shipment, payment or refund; it is not a fabricated completed sale.

**Browser acceptance passed:** on the synthetic RMA, selecting Reject for line1 left line2 undecided. Selected Accept for line2 and submitted **Inspect**. The immediate inspection readback was Inspecting, decisions `[false,true]`, and accepted value$57; this preceded the zero-refund completion below. [Actual independent decisions](assets/store-return-independent-decisions.png); [sanitized readback](assets/store-return-inspection-proof.json).

The readback exposed a second UI defect: the list still displayed$102 by summing rejected and accepted lines. It now uses the saved return amount, preserving an explicit zero, and falls back to accepted lines for older inspected records. The completion form also required a payment reference for a zero refund even though the backend correctly allows zero without moving money. It now requires a reference only above zero. Eleven related UI regressions pass, including independent indexes, accepted-value/zero display and zero-refund validation. Final browser readback passed: the inspected fixture displays **$57**. Completed that same synthetic fixture once with **Original payment**, amount **0**, and a blank reference. It now displays **Completed / $0**; the saved timeline explicitly says no money moved, and refundId is absent. No gift card, provider payment, stock change or change to RPSDCGHC4 occurred. [Accepted-value screen](assets/store-return-accepted-value.png), [zero completion](assets/store-return-zero-completion.png), [saved zero-refund proof](assets/store-return-zero-completion-proof.json).

The completion-dialog guidance was also corrected in source: the old “never transfers money” claim hid that Store credit/Gift card can add stored value. It now distinguishes external-gateway recording, cash-record verification and stored-value issuance. After restarting Studio, opened **Complete return** on a separate synthetic, unsubmitted fixture, **RMA-TUTORIAL-WORDING-20260924**, and verified the rendered guidance. It explicitly says external references are not provider verification, no external gateway or restock is executed, and Store credit/Gift card can add stored value. Selected **Cancel** without submitting; that fixture remains **Inspecting / $57**. [Actual final dialog](assets/store-return-completion-guidance.png). All application changes described in this report are loaded and the corresponding repaired flows above were checked locally.
