Critical money-path QA (signup → product config → cart → checkout → payment → order → fulfillment)

By Pritesh Yadav 21 min read

Launch-readiness assessment — Print-Flow-360 — 2026-06-15 Source probes: /tmp/qa_money.json (59 findings, 7 stages) + /tmp/qa_state.json (14 cross-cutting loading/empty/error + mobile findings). Deduped where two probes hit the same issue. Severity legend: 🔴 blocker · 🟠 high · 🟡 medium · 🔵 low


Verdict

The money path is NOT launch-ready. Four blockers sit directly on the spine that takes money and turns it into a fulfilled order, and any one of them is a launch-stopper on its own:

  1. 🔴 Inventory is never decremented (StorefrontCheckoutController.php checkout pipeline; UpdatePaymentAndOrder.php:155-166). ProductInventoryService::deductStock() exists but is never called from checkout or payment-capture. Two customers can buy the same last unit; the platform will cheerfully oversell every limited-stock product on day one.
  2. 🔴 Customer password reset is a complete fake (CustomerAuthController.php:497). The endpoint returns a green “we sent you a link” success but only logs a TODO. No token, no email. Any customer who forgets their password is permanently locked out — a guaranteed support flood at launch.
  3. 🔴 A hardcoded master-OTP backdoor (AuthController.php:368) — env('MASTER_OTP', 702702) lets anyone who knows the constant 702702 clear the 2FA check on any admin account. This is a credential-bypass backdoor shipping to production.
  4. 🔴 Required file-upload product options are silently dropped (productInfo.ts:139-146, 709-714). When allow_custom_file_upload=false, a required file option is filtered out before the required check, so Add-to-Cart succeeds with the required artwork never collected — the order reaches production unfulfillable.

Beyond the blockers, the order-to-production spine is structurally incomplete: no PRODUCTION/SHIPPED statuses, EasyPost built but unwired (flat-rate only), tracking numbers hand-typed, no partial fulfillment, and print-ready PDFs that can be NULL at checkout. Layered on top is a pervasive silent-lie / fake-success class: offline (cheque) orders show “Payment Successful” while payment_status='pending', the confirmation email fires at draft time (“being processed”) before any payment and shows the wrong total, and several checkout address/payment fields skip server validation. The two strongest stages are the price-calculator math (largely correct, with two real bugs) and the cart↔checkout tax formula (verified to match) — but those are the only stages that read as close to shippable.


Prioritized findings table

All findings from both probes, deduped (the offline “Payment Successful” lie and the shipping-$0 drop were each found by two probes — noted ⚑ and confidence raised).

SevFindingStageWhere (file:line)TypeConfRecommendation
🔴Stock inventory never decremented at order/payment7 FulfillmentStorefrontCheckoutController.php:54-612; UpdatePaymentAndOrder.php:155-166money-mismatchconfirmedCall deductStock() in payment-captured flow, wrapped in a transaction; block oversell
🔴Customer password reset is a TODO that lies “link sent”1 Signup/AuthCustomerAuthController.php:497silent-lieconfirmedImplement token+email+reset endpoint, or hide the feature until built
🔴Hardcoded master-OTP 702702 bypasses admin 2FA1 Signup/AuthAuthController.php:368silent-lieconfirmedRemove backdoor; if needed, gate to system-admins with rate-limit + audit
🔴Required file-upload option silently dropped when uploads disabled2 PDP/PricingproductInfo.ts:139-146, 709-714silent-lieconfirmedKeep file options in validation; show “file required but unavailable” error
🟠Per-size price markup never applied (size_id never sent)2 PDP/PricingproductInfo.ts:940-946; ProductPricingCalculator.php:408money-mismatchconfirmedSend selected_size_id in calc + configure context
🟠Print-ready PDF can be NULL at checkout, blocks production download7 FulfillmentStorefrontCheckoutController.php:659-668silent-lieconfirmedAuto-generate PDF at checkout OR reject order if missing
🟠Preflight validation disabled, never auto-runs (72 DPI/RGB to production)7 FulfillmentPreflightController.php:38-40bad-error-stateconfirmedEnable flag; gate checkout on ≥300 DPI with plain-language error
🟠EasyPost carrier rates built but never wired into checkout7 FulfillmentEasyPostShipping.php:40-104; StorefrontCheckoutController.php:486-496missing-loading-emptyconfirmedAdd shipping-rates endpoint; resolve cost from carrier, not flat-rate table
🟠No PRODUCTION/FULFILLED/SHIPPED order statuses7 FulfillmentOrderEnum.php:5-14onboarding-gapconfirmedSeed core lifecycle statuses; sync with PrintJob transitions
🟠Tracking number hand-typed; no label generation / carrier API7 FulfillmentOrderController.php updateShipping()missing-loading-emptyconfirmedCreate Shipment on READY_TO_SHIP via EasyPost; auto-fill tracking + poll
🟠No partial fulfillment / split-shipment; orders atomic7 FulfillmentOrder.php; Item.php:1-76field-dropconfirmedAdd Shipment model + per-item fulfilled_quantity (post-launch feature)
🟠Confirmation email shows item subtotal only (drops shipping/tax/coupon)6 OrderStorefrontCheckoutController.php:580money-mismatchconfirmedUse $order->total_amount after save, not $items->sum('amount')
🟠Confirmation email “received & being processed” sent at draft, before payment6 OrderStorefrontCheckoutController.php:571-610 (send @596)silent-lieconfirmedSend on payment_status='paid', or reword to “awaiting payment”
🟠Offline (cheque) order shows “Payment Successful” but status=pending ⚑(2 probes)5 Paymentorder-success.vue:779-798, 638-655silent-lieconfirmedRelabel: “Order Submitted / Awaiting Cheque Payment” + mailing instructions
🟠Shipping silently $0: nullable shipping_method + carrier keys never priced ⚑(2 probes)4 CheckoutCheckoutRequest.php:40-42; StorefrontCheckoutController.php:488-496; checkout.vue:1486-1488money-mismatchconfirmedRequire method when cart showed shipping; price non-flat-rate methods
🟠Missing webhook_secret hangs order in ‘pending’ forever5 PaymentRazorpayGateway.php:348-350; PaymentWebhookController.php:23-46silent-lieconfirmedValidate/warn on missing webhook_secret at gateway config save time
🟠Cheque orders stuck ‘pending’ forever, no automation/reminder5 PaymentChequeGateway.php:117-120; UpdatePaymentAndOrder.phpsilent-lieconfirmedAction Center card for pending cheques + daily admin digest
🟠new_shipping_address/new_billing_address arrays have NO FormRequest rules4 CheckoutCheckoutRequest.php:25-59silent-lieconfirmedAdd required_with rules for address_line1/city/zip/country
🟠Missing address fields default to empty string, save invalid address4 CheckoutStorefrontCheckoutController.php:427-432silent-lieconfirmedDrop null-coalescing; validate before Address::create()
🟠Quote endpoint shares the same missing address validation4 CheckoutStorefrontCheckoutController.php:687-691, 720-757silent-lieconfirmedApply shared address rule set to quote path
🟠CheckoutCustomFieldsTest fails — draft order returns 4004 Checkouttests/Feature/Storefront/CheckoutCustomFieldsTest.php:113bad-error-stateconfirmedDiagnose 400 (cart seeding / store-header mismatch); make test green
🟠Cart vs checkout tax double-count risk when prices_include_tax3 Cart / 4 CheckoutStorefrontCartService.php:54-61; checkout.vue:789money-mismatchlikelyVerify resolvedTotal doesn’t add cartTax when subtotal already includes it
🟠Cart shows first shipping method, checkout lets user pick a different one3 CartStorefrontCartService.php:102-104; checkout.vue:775-790money-mismatchconfirmedPre-select cart’s method; show delta when changed
🟠Guest cart localStorage key not tenant-scoped (cross-store collision)3 CartcartStore.ts:4, 48-57silent-lieconfirmedScope key by hostname: storefront-cart-session-${hostname}
🟠Admin OTP resend has no rate limiting (inbox flood / DoS)1 Signup/Authroutes/api.php:116otherconfirmedAdd throttle:3,1 + per-email/device throttle
🟠No Stripe signed-return fallback (Razorpay only) → stuck pending in dev5 PaymentStripeGateway.php (no verifyReturnParams)missing-loading-emptylikelyImplement verifyReturnParams() mirroring Razorpay
🟠Storefront profile/orders list has no error state (blanks on fetch fail)6 Orderprofile/orders/index.vue:357-371missing-loading-emptyconfirmedAdd error card + Retry, matching admin order/List.vue:108-114
🟠Storefront order detail shows “Order not found” on network error6 Orderprofile/orders/[id].vue:298-303missing-loading-emptyconfirmedTrack fetch error; show retriable error card vs “not found”
🟠Checkout config load fails silently → broken empty form4 Checkoutcheckout.vue:1163-1194bad-error-stateconfirmedAdd checkoutConfigError; show error card before form sections
🟡Quantity min/step auto-clamps with no feedback (1 → 250)2 PDP/PricingProductQuantitySelector.vue:97-104otherconfirmedToast “Quantity adjusted to 250 (sold in multiples of 250)“
🟡Custom-size bounds validated client-only, not backend2 PDP/PricingProductSizePicker.vue:163-192; ProductPricingCalculator.php:371-394field-dropconfirmedEnforce min/max bounds in prepareContext()
🟡No validation that required file options are collectible (admin blind)2 PDP/PricingproductInfo.ts:53-66, 139-142support-gapconfirmedCustomer + admin warning when required file option can’t be collected
🟡$0-price guard shows generic “request a quote”, no diagnosis2 PDP/PricingproductInfo.ts:372-387; ProductInfoPage.vue:199-203feedback-gapconfirmedImprove message + admin diagnostic for misconfigured formula vars
🟡Item notes save on blur with no loading state; lost on timeout3 Cartcart.vue:146-148; useCart.ts:593-611bad-error-stateconfirmedAdd per-item saving state; validate field in response
🟡Availability checked at add-time, not re-validated at checkout (oversell)3 CartStorefrontCheckoutController.php:351-369; StorefrontCartService.php:145-172money-mismatchlikelyRe-validate availability in createOrderDraft(); reject if unavailable
🟡CartItem availability check is display-only; no reservation/block3 CartStorefrontCartService.php:145-158silent-lieconfirmedRe-validate at order creation or add short-lived reservation
🟡Carrier shipping methods record shipping_cost=0 on order4 CheckoutStorefrontCheckoutController.php:486-496field-dropconfirmedCompute carrier cost or mark nullable + document
🟡Razorpay save accepts empty key_secret/webhook_secret as valid5 PaymentRazorpayGateway.php:281-284silent-lieconfirmedvalidateConfiguration() should require webhook_secret for webhook gateways
🟡markAsPaid() fires triggers inside payment DB transaction5 PaymentUpdatePaymentAndOrder.php:155-166; Order.php:190-202otherconfirmedMove TriggerDispatcher::dispatch() outside the transaction
🟡Order status='paid' decoupled from payment_status (mark paid w/o payment)5/6OrderController.php:1467silent-lieconfirmedWarn/confirm on status=‘paid’ when payment_status≠paid; document orthogonality
🟡Missing order_uuid in redirect URL breaks polling (stuck pending page)5 Paymentorder-success.vue:1-3; CheckoutPaymentService.php:89-96missing-loading-emptylikelyAlways append order_uuid; implement by-number resolver TODO
🟡Order-success spinner 5-90s with no progress/ETA feedback6 Orderorder-success.vue:373-425, 643missing-loading-emptyconfirmedShow elapsed time / progress bar / “taking longer” at 30s
🟡Order-success timeout hides error behind computed text, spinner spins on6 Orderorder-success.vue:535-545, 657-692bad-error-stateconfirmedDistinct timeout state + “Contact Support” button
🟡Offline order: cart cleared + UI ‘paid’ while backend stays ‘pending’5 Paymentorder-success.vue:779-798; StorefrontCheckoutController.php:93-127silent-lieconfirmedMark offline paid at creation OR don’t clear cart/fire GTM until paid
🟡Unsafe isOrderOwner(): failed markOwned() → friend’s URL wipes cart6 Orderorder-success.vue:390-416; checkout.vue:1620silent-lielikelyServer-side order-owner check; log markOwned failures
🟡Quote submission has no loading state (duplicate-quote risk)4 Checkoutcheckout.vue:1564-1595missing-loading-emptylikelyAdd submitting flag; disable button during call
🟡Checkout custom-field errors inline only, no summary banner4 Checkoutcheckout.vue:909-921feedback-gapconfirmedAdd plain-language summary banner above fields
🟡Cart page has no error state for API failures3 Cartcart.vue:31-59missing-loading-emptylikelyAdd error card + Retry to cart composable
🟡Storefront profile/index stats fail silently (blank tiles)6 Orderprofile/index.vue:205-219missing-loading-emptyconfirmedCheck all .ok; show error card before stats grid
🟡Admin order list shows “Deleted customer” though snapshot exists6 Ordernuxt/.../order/List.vue:245-254silent-lielikelyExpose customer_snapshot in OrderResource as fallback
🟡Public order tracking has no rate-limit (enumeration risk)7 FulfillmentStorefrontCheckoutController.php:978-1040support-gaplikelyAdd throttle middleware + CAPTCHA on repeated failures
🟡Print-ready PDF generation failure is silent (logged, never surfaced)7 FulfillmentDesignerController.php:1431-1459bad-error-statelikelyAdd print_ready_status flag; warn designer + checkout
🟡B2B domain rejection only at sign-up, not login / admin-created accounts1 Signup/AuthCustomerAuthController.php:130-132 vs 262-320otherconfirmedEnforce domain check in login + Google + admin creation
🟡Admin registration uses hardcoded min:8, ignores SecurityPolicyService1 Signup/AuthAuthController.php:61field-dropconfirmedUse SecurityPolicyService::passwordRules()
🟡Mobile: cart/checkout tap targets may be <44px at 375px3/4cart.vue quantity + shipping selectorsmobileneeds-runtimeAudit at 375px; enforce min 44×44px
🔵“Estimated Total” label misleads (excludes shipping)2 PDP/PricingProductPriceSummary.vue:8silent-lielikelyRelabel “Estimated Subtotal”; note shipping at checkout
🔵Missing loading indicator during 300ms price-calc debounce2 PDP/PricingproductInfo.ts:836-864bad-error-statelikelySet isCalculating=true immediately on change
🔵Coupon apply/remove methods exist but no cart UI (dead code)3 CartcartStore.ts:23-24, 459-493; cart.vuefield-dropconfirmedAdd coupon input + apply/remove UI to cart summary
🔵Product placeholder image uses absolute path (breaks in subdir)3 Cartcart.vue:89; cartStore.ts:161otherlikelyImport asset or use relative path
🔵Quantity update success not validated against response field3 Cartcart.vue:613-619; cartStore.ts:356-358silent-lieneeds-runtimeValidate returned quantity in applyServerCart()
🔵Server allows empty payment_method (client-only guard)5 Paymentcheckout.vue:1314-1321; CheckoutRequestsilent-lieconfirmedpayment_method => required|string|min:1 + active-gateway rule
🔵Offline payment clears cart before success page loads5 Paymentcheckout.vue:1697-1710; order-success.vue:310missing-loading-emptylikelyInclude order_uuid; defer cart clear until paid; email on create
🔵Payment method not validated at draft time (error at intent step)5 PaymentCheckoutRequest.php:39; StorefrontCheckoutController.php:128-140otherconfirmedCustom ValidPaymentMethod rule scoped to store
🔵Redirect-flow cart clear without order_uuid verification (replay)5 Paymentorder-success.vue:800-828otherlikelyImplement by-number resolver with email check
🔵Quote custom-field snapshot drops B2B company/department context4 CheckoutStorefrontCheckoutController.php:673-846 vs 504-505field-dropconfirmedPersist company_account_id/department_id on Quote
🔵Confirmation email fallback text doesn’t state payment-pending6 OrderStorefrontCheckoutController.php:591-592otherconfirmedReword fallback or defer email to paid
🔵OTP logged in plaintext1 Signup/AuthAuthController.php:285, 346otherconfirmedLog hash/truncated only
🔵Remembered-device OTP = sha256(device_id) only, forgeable if cookie stolen1 Signup/AuthAuthController.php:374otherconfirmedBind IP/user-agent into hash
🔵Design-lock failure at checkout allows instant resubmission spam4 Checkoutcheckout.vue:1480-1484bad-error-statelikelyInline error + disable button with tooltip

Per-stage assessment

1. Signup & Auth — 🔴 two blockers, not shippable

Two of the four launch blockers live here. Password reset is entirely fake (CustomerAuthController.php:497): the endpoint returns a 201 “we’ll email you a reset link” while the body is a TODO — no token, no mail. Every customer who forgets their password is locked out with no recovery, and the storefront actively lies to them with a green success message (login.vue:342). Separately, a hardcoded master OTP (AuthController.php:368, default 702702) defeats admin 2FA for any account — a credential-bypass backdoor that must not ship. The 🟠 unthrottled resendOtp (routes/api.php:116) is an inbox-flood/DoS vector while its sibling forgotPassword is correctly throttled. The remaining items (admin registration ignoring SecurityPolicyService, B2B domain enforced only at sign-up, plaintext OTP logging, forgeable device hash) are real but lower-priority hardening. This stage cannot launch until the password-reset and master-OTP blockers are resolved.

2. Product configuration / PDP / price calculator — 🔴 one blocker, otherwise the strongest stage

The pricing engine is mostly correct, but one blocker and one high bug break the money math. Blocker: required file-upload options are filtered out before the required-field check (productInfo.ts:139-146), so when allow_custom_file_upload=false a product that needs artwork still passes Add-to-Cart with nothing collected — the order is created un-producible and nobody is told. High: per-size price markups never apply because the frontend never sends selected_size_id, so the backend’s per-size branch (ProductPricingCalculator.php:408) is dead — an 8×10 priced ”+$5” is charged at base price end-to-end (PDP, cart, invoice). The rest are UX-grade: silent quantity clamping (1→250 with no feedback), client-only size-bounds validation, an ambiguous “Estimated Total” label, and a debounce with no spinner. Once the file-option and per-size bugs are fixed, this stage is close to shippable — it has the fewest structural gaps of the seven.

3. Cart — 🟠 several money-mismatch / state gaps, no blocker

No blockers, but three highs cluster on money correctness and tenancy. The guest cart localStorage key is not tenant-scoped (cartStore.ts:4) — two stores on a shared host collide and overwrite each other’s guest carts. The cart shows the first shipping method while checkout lets the user pick another (StorefrontCartService.php:102-104), silently changing the total with no delta warning. Availability is checked only at add-time and is display-only — never re-validated at checkout (StorefrontCartService.php:145-158), so the cart can place an oversold order. Lower-severity: item-notes save on blur with no loading state (lost on timeout), no cart error state on API failure, an absolute placeholder image path, and dead coupon codeapplyCoupon/removeCoupon exist in the store but there is no cart UI to invoke them.

4. Checkout (shipping + tax + addresses + custom fields) — 🟠 silent-validation cluster, no blocker but high risk

This is the highest-density high-severity stage after Fulfillment, dominated by silently-skipped server validation. new_shipping_address/new_billing_address arrays have no FormRequest rules at all (CheckoutRequest.php:25-59); the controller then null-coalesces missing fields to empty strings (:427-432), persisting blank, invalid addresses on the order — the quote path repeats the identical bug (:687-691). The shipping-$0 drop (⚑ found by both probes): shipping_method is nullable unless required, and only flat_rate_* keys are priced (:488-496), so a customer who saw $100 shipping in the cart can place an order with shipping_cost=0. A failing test (CheckoutCustomFieldsTest.php:113, draft returns 400) signals a live regression in the custom-field checkout path. State gaps: checkout config fails silently to a broken empty form, custom-field errors lack a summary banner, and the tax double-count risk when prices_include_tax=true needs a runtime check. Add a shared address validation rule set and require a shipping method whenever the cart showed one before this stage ships.

5. Payment & gateways — 🟠 silent-lie cluster around offline/webhook flows

No blocker, but the truth of payment status is unreliable. Offline (cheque) orders show “Payment Successful” in the UI (⚑ found by both probes — order-success.vue:779-798) while the backend payment_status stays 'pending' with no Payment row — a chargeback/dispute and reconciliation hazard. Cheque orders then sit pending forever with no automation or admin reminder (ChequeGateway.php:117-120). A missing webhook_secret hangs real card orders in 'pending' indefinitely because the webhook 401s (RazorpayGateway.php:348-350), and the gateway-save validation never catches the missing secret. Order status='paid' is decoupled from payment_status so an admin can mark an order paid with no payment received. Stripe lacks Razorpay’s signed-return fallback (stuck-pending in dev), and markAsPaid() fires automation triggers inside the payment DB transaction (a trigger failure rolls back the paid status). The offline-success lie and webhook-secret hang are the items to fix first here.

6. Order creation & confirmation — 🟠 the confirmation email lies twice

Two highs both sit on the order confirmation email. It computes the total as $items->sum('amount') (StorefrontCheckoutController.php:580), dropping shipping, tax, and coupons — the customer is emailed “$50” while they were charged $65. Worse, the email fires at draft creation, before any payment (:596), telling the customer the order is “being processed” even if payment then fails, with no follow-up — a direct support-call generator. The page-state gaps reinforce the “is my order real?” anxiety: the order-success spinner runs 5-90s with no progress indication and hides its timeout behind computed text rather than a “Contact Support” action; the storefront orders list and order-detail pages blank or show “Order not found” on a network error instead of a retriable error card; and the admin order list shows “Deleted customer” though the snapshot data exists. Move the email to payment-confirmed and fix the total before launch.

7. Fulfillment / production / shipping / tracking — 🔴 the most structurally incomplete stage

This is where the platform is least ready and where the remaining blocker lives. Inventory is never decremented (StorefrontCheckoutController.php + UpdatePaymentAndOrder.php:155-166) — deductStock() exists but is unreachable from the money path, so the platform oversells by default. Beyond that, the order-to-production spine is missing its backbone: there are no PRODUCTION/FULFILLED/SHIPPED statuses (OrderEnum.php:5-14), EasyPost carrier rating is fully built but never wired into checkout (EasyPostShipping.php:40-104) so only flat-rate ships, tracking numbers are hand-typed with no label generation, there’s no partial/split fulfillment, and the print-ready PDF can be NULL at checkout (:659-668) — staff click “Download Print-Ready” and hit a null path. Preflight is disabled (PreflightController.php:38-40), so 72-DPI RGB no-bleed artwork reaches production with no gate. Public order tracking has no rate limit (enumeration risk). The inventory blocker is mandatory for launch; the status/carrier/preflight gaps are the post-launch order-to-production buildout this product still needs.


Stale-doc corrections (vs readme/QA_FINDINGS_2026-06-01.md)

The 2026-06-01 pass reported two money mismatches with specific dollar figures. Reconciling against current code:

  • Cart→checkout shipping mismatch ($49 → $100): the symptom is confirmed and broader than first reported. It is not a single hardcoded $49→$100 jump but a structural divergence with two live causes: (a) the cart previews only the first active flat-rate method (StorefrontCartService.php:102-104) while checkout lets the customer pick a different (pricier) method with no delta warning; and (b) any non-flat_rate_ method silently prices to $0 (StorefrontCheckoutController.php:488-496) because carrier rating (EasyPost) was never wired in. So the cart figure and the order’s shipping_cost can legitimately differ — and for carrier methods the order records $0 regardless of what was shown. The old “$49→$100” line should be retired in favor of these two concrete code findings. The mismatch is not fixed.
  • PDP price mismatch ($77 vs $75): the current findings do not reproduce a fixed $77-vs-$75 discrepancy, but they identify the live mechanism that produces PDP↔order price drift — the per-size markup is never applied because selected_size_id is never sent (ProductPricingCalculator.php:408). Any product with per-size pricing will show and charge the base price across PDP, cart, and invoice, i.e. the size delta is dropped rather than mis-added. Treat the old “$77 vs $75” note as superseded by this root-cause finding.

Readiness recommendations

Clearly labelled recommendations — engineering actions to get the money path to launch-ready, ordered by leverage.

  1. Fix the four blockers first — inventory decrement on payment-capture (wrapped in a transaction, with oversell guard), real password-reset flow (token + email + reset endpoint), remove the master-OTP backdoor, and stop dropping required file-upload options. Nothing else ships until these are closed.
  2. Add a payment-status-truth test — assert that offline/cheque orders never display “Payment Successful” while payment_status='pending', and that the confirmation email is gated on payment_status='paid'. This pins the largest silent-lie cluster.
  3. Add a shipping round-trip testcart total === order.shipping_cost for the selected method across flat-rate and carrier methods; this would catch both the method-switch divergence and the carrier-$0 drop in one assertion.
  4. Add an inventory-decrement test — two concurrent orders for the last unit must not both succeed; assert stock reaches 0 and the second order is rejected.
  5. Introduce a shared ErrorState component for the storefront and adopt it on profile/orders/index.vue, profile/orders/[id].vue, profile/index.vue, cart.vue, and the checkout-config failure path — every money-path view must render loading / empty / error (currently several blank or mislabel network errors as “not found”).
  6. Centralize address + payment-method validation — one shared rule set for new_shipping_address/new_billing_address used by both order and quote paths, plus a ValidPaymentMethod rule scoped to the store; remove the null-coalescing defaults that persist blank addresses.
  7. Run a mobile 375px sweep on cart and checkout — confirm all quantity, shipping, and payment controls meet the 44×44px tap-target minimum.
  8. Surface gateway-config gaps at save time — validate/warn on missing webhook_secret for webhook gateways and add an Action Center card for pending cheque orders, so non-technical owners aren’t left with silently stuck orders.
  9. Scope the order-to-production buildout as the next milestone — core PRODUCTION/SHIPPED statuses, EasyPost wired into checkout, preflight enabled with a ≥300 DPI gate, print-ready PDF guaranteed (or order rejected) at checkout, and rate-limiting on public order tracking. This is the largest structural gap and should be treated as a tracked post-launch (or launch-gating, per the founder’s risk appetite) workstream.

Continue reading