Critical money-path QA (signup → product config → cart → checkout → payment → order → fulfillment)
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:
- 🔴 Inventory is never decremented (
StorefrontCheckoutController.phpcheckout 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. - 🔴 Customer password reset is a complete fake (
CustomerAuthController.php:497). The endpoint returns a green “we sent you a link” success but only logs aTODO. No token, no email. Any customer who forgets their password is permanently locked out — a guaranteed support flood at launch. - 🔴 A hardcoded master-OTP backdoor (
AuthController.php:368) —env('MASTER_OTP', 702702)lets anyone who knows the constant702702clear the 2FA check on any admin account. This is a credential-bypass backdoor shipping to production. - 🔴 Required file-upload product options are silently dropped (
productInfo.ts:139-146, 709-714). Whenallow_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).
| Sev | Finding | Stage | Where (file:line) | Type | Conf | Recommendation |
|---|---|---|---|---|---|---|
| 🔴 | Stock inventory never decremented at order/payment | 7 Fulfillment | StorefrontCheckoutController.php:54-612; UpdatePaymentAndOrder.php:155-166 | money-mismatch | confirmed | Call deductStock() in payment-captured flow, wrapped in a transaction; block oversell |
| 🔴 | Customer password reset is a TODO that lies “link sent” | 1 Signup/Auth | CustomerAuthController.php:497 | silent-lie | confirmed | Implement token+email+reset endpoint, or hide the feature until built |
| 🔴 | Hardcoded master-OTP 702702 bypasses admin 2FA | 1 Signup/Auth | AuthController.php:368 | silent-lie | confirmed | Remove backdoor; if needed, gate to system-admins with rate-limit + audit |
| 🔴 | Required file-upload option silently dropped when uploads disabled | 2 PDP/Pricing | productInfo.ts:139-146, 709-714 | silent-lie | confirmed | Keep file options in validation; show “file required but unavailable” error |
| 🟠 | Per-size price markup never applied (size_id never sent) | 2 PDP/Pricing | productInfo.ts:940-946; ProductPricingCalculator.php:408 | money-mismatch | confirmed | Send selected_size_id in calc + configure context |
| 🟠 | Print-ready PDF can be NULL at checkout, blocks production download | 7 Fulfillment | StorefrontCheckoutController.php:659-668 | silent-lie | confirmed | Auto-generate PDF at checkout OR reject order if missing |
| 🟠 | Preflight validation disabled, never auto-runs (72 DPI/RGB to production) | 7 Fulfillment | PreflightController.php:38-40 | bad-error-state | confirmed | Enable flag; gate checkout on ≥300 DPI with plain-language error |
| 🟠 | EasyPost carrier rates built but never wired into checkout | 7 Fulfillment | EasyPostShipping.php:40-104; StorefrontCheckoutController.php:486-496 | missing-loading-empty | confirmed | Add shipping-rates endpoint; resolve cost from carrier, not flat-rate table |
| 🟠 | No PRODUCTION/FULFILLED/SHIPPED order statuses | 7 Fulfillment | OrderEnum.php:5-14 | onboarding-gap | confirmed | Seed core lifecycle statuses; sync with PrintJob transitions |
| 🟠 | Tracking number hand-typed; no label generation / carrier API | 7 Fulfillment | OrderController.php updateShipping() | missing-loading-empty | confirmed | Create Shipment on READY_TO_SHIP via EasyPost; auto-fill tracking + poll |
| 🟠 | No partial fulfillment / split-shipment; orders atomic | 7 Fulfillment | Order.php; Item.php:1-76 | field-drop | confirmed | Add Shipment model + per-item fulfilled_quantity (post-launch feature) |
| 🟠 | Confirmation email shows item subtotal only (drops shipping/tax/coupon) | 6 Order | StorefrontCheckoutController.php:580 | money-mismatch | confirmed | Use $order->total_amount after save, not $items->sum('amount') |
| 🟠 | Confirmation email “received & being processed” sent at draft, before payment | 6 Order | StorefrontCheckoutController.php:571-610 (send @596) | silent-lie | confirmed | Send on payment_status='paid', or reword to “awaiting payment” |
| 🟠 | Offline (cheque) order shows “Payment Successful” but status=pending ⚑(2 probes) | 5 Payment | order-success.vue:779-798, 638-655 | silent-lie | confirmed | Relabel: “Order Submitted / Awaiting Cheque Payment” + mailing instructions |
| 🟠 | Shipping silently $0: nullable shipping_method + carrier keys never priced ⚑(2 probes) | 4 Checkout | CheckoutRequest.php:40-42; StorefrontCheckoutController.php:488-496; checkout.vue:1486-1488 | money-mismatch | confirmed | Require method when cart showed shipping; price non-flat-rate methods |
| 🟠 | Missing webhook_secret hangs order in ‘pending’ forever | 5 Payment | RazorpayGateway.php:348-350; PaymentWebhookController.php:23-46 | silent-lie | confirmed | Validate/warn on missing webhook_secret at gateway config save time |
| 🟠 | Cheque orders stuck ‘pending’ forever, no automation/reminder | 5 Payment | ChequeGateway.php:117-120; UpdatePaymentAndOrder.php | silent-lie | confirmed | Action Center card for pending cheques + daily admin digest |
| 🟠 | new_shipping_address/new_billing_address arrays have NO FormRequest rules | 4 Checkout | CheckoutRequest.php:25-59 | silent-lie | confirmed | Add required_with rules for address_line1/city/zip/country |
| 🟠 | Missing address fields default to empty string, save invalid address | 4 Checkout | StorefrontCheckoutController.php:427-432 | silent-lie | confirmed | Drop null-coalescing; validate before Address::create() |
| 🟠 | Quote endpoint shares the same missing address validation | 4 Checkout | StorefrontCheckoutController.php:687-691, 720-757 | silent-lie | confirmed | Apply shared address rule set to quote path |
| 🟠 | CheckoutCustomFieldsTest fails — draft order returns 400 | 4 Checkout | tests/Feature/Storefront/CheckoutCustomFieldsTest.php:113 | bad-error-state | confirmed | Diagnose 400 (cart seeding / store-header mismatch); make test green |
| 🟠 | Cart vs checkout tax double-count risk when prices_include_tax | 3 Cart / 4 Checkout | StorefrontCartService.php:54-61; checkout.vue:789 | money-mismatch | likely | Verify resolvedTotal doesn’t add cartTax when subtotal already includes it |
| 🟠 | Cart shows first shipping method, checkout lets user pick a different one | 3 Cart | StorefrontCartService.php:102-104; checkout.vue:775-790 | money-mismatch | confirmed | Pre-select cart’s method; show delta when changed |
| 🟠 | Guest cart localStorage key not tenant-scoped (cross-store collision) | 3 Cart | cartStore.ts:4, 48-57 | silent-lie | confirmed | Scope key by hostname: storefront-cart-session-${hostname} |
| 🟠 | Admin OTP resend has no rate limiting (inbox flood / DoS) | 1 Signup/Auth | routes/api.php:116 | other | confirmed | Add throttle:3,1 + per-email/device throttle |
| 🟠 | No Stripe signed-return fallback (Razorpay only) → stuck pending in dev | 5 Payment | StripeGateway.php (no verifyReturnParams) | missing-loading-empty | likely | Implement verifyReturnParams() mirroring Razorpay |
| 🟠 | Storefront profile/orders list has no error state (blanks on fetch fail) | 6 Order | profile/orders/index.vue:357-371 | missing-loading-empty | confirmed | Add error card + Retry, matching admin order/List.vue:108-114 |
| 🟠 | Storefront order detail shows “Order not found” on network error | 6 Order | profile/orders/[id].vue:298-303 | missing-loading-empty | confirmed | Track fetch error; show retriable error card vs “not found” |
| 🟠 | Checkout config load fails silently → broken empty form | 4 Checkout | checkout.vue:1163-1194 | bad-error-state | confirmed | Add checkoutConfigError; show error card before form sections |
| 🟡 | Quantity min/step auto-clamps with no feedback (1 → 250) | 2 PDP/Pricing | ProductQuantitySelector.vue:97-104 | other | confirmed | Toast “Quantity adjusted to 250 (sold in multiples of 250)“ |
| 🟡 | Custom-size bounds validated client-only, not backend | 2 PDP/Pricing | ProductSizePicker.vue:163-192; ProductPricingCalculator.php:371-394 | field-drop | confirmed | Enforce min/max bounds in prepareContext() |
| 🟡 | No validation that required file options are collectible (admin blind) | 2 PDP/Pricing | productInfo.ts:53-66, 139-142 | support-gap | confirmed | Customer + admin warning when required file option can’t be collected |
| 🟡 | $0-price guard shows generic “request a quote”, no diagnosis | 2 PDP/Pricing | productInfo.ts:372-387; ProductInfoPage.vue:199-203 | feedback-gap | confirmed | Improve message + admin diagnostic for misconfigured formula vars |
| 🟡 | Item notes save on blur with no loading state; lost on timeout | 3 Cart | cart.vue:146-148; useCart.ts:593-611 | bad-error-state | confirmed | Add per-item saving state; validate field in response |
| 🟡 | Availability checked at add-time, not re-validated at checkout (oversell) | 3 Cart | StorefrontCheckoutController.php:351-369; StorefrontCartService.php:145-172 | money-mismatch | likely | Re-validate availability in createOrderDraft(); reject if unavailable |
| 🟡 | CartItem availability check is display-only; no reservation/block | 3 Cart | StorefrontCartService.php:145-158 | silent-lie | confirmed | Re-validate at order creation or add short-lived reservation |
| 🟡 | Carrier shipping methods record shipping_cost=0 on order | 4 Checkout | StorefrontCheckoutController.php:486-496 | field-drop | confirmed | Compute carrier cost or mark nullable + document |
| 🟡 | Razorpay save accepts empty key_secret/webhook_secret as valid | 5 Payment | RazorpayGateway.php:281-284 | silent-lie | confirmed | validateConfiguration() should require webhook_secret for webhook gateways |
| 🟡 | markAsPaid() fires triggers inside payment DB transaction | 5 Payment | UpdatePaymentAndOrder.php:155-166; Order.php:190-202 | other | confirmed | Move TriggerDispatcher::dispatch() outside the transaction |
| 🟡 | Order status='paid' decoupled from payment_status (mark paid w/o payment) | 5/6 | OrderController.php:1467 | silent-lie | confirmed | Warn/confirm on status=‘paid’ when payment_status≠paid; document orthogonality |
| 🟡 | Missing order_uuid in redirect URL breaks polling (stuck pending page) | 5 Payment | order-success.vue:1-3; CheckoutPaymentService.php:89-96 | missing-loading-empty | likely | Always append order_uuid; implement by-number resolver TODO |
| 🟡 | Order-success spinner 5-90s with no progress/ETA feedback | 6 Order | order-success.vue:373-425, 643 | missing-loading-empty | confirmed | Show elapsed time / progress bar / “taking longer” at 30s |
| 🟡 | Order-success timeout hides error behind computed text, spinner spins on | 6 Order | order-success.vue:535-545, 657-692 | bad-error-state | confirmed | Distinct timeout state + “Contact Support” button |
| 🟡 | Offline order: cart cleared + UI ‘paid’ while backend stays ‘pending’ | 5 Payment | order-success.vue:779-798; StorefrontCheckoutController.php:93-127 | silent-lie | confirmed | Mark offline paid at creation OR don’t clear cart/fire GTM until paid |
| 🟡 | Unsafe isOrderOwner(): failed markOwned() → friend’s URL wipes cart | 6 Order | order-success.vue:390-416; checkout.vue:1620 | silent-lie | likely | Server-side order-owner check; log markOwned failures |
| 🟡 | Quote submission has no loading state (duplicate-quote risk) | 4 Checkout | checkout.vue:1564-1595 | missing-loading-empty | likely | Add submitting flag; disable button during call |
| 🟡 | Checkout custom-field errors inline only, no summary banner | 4 Checkout | checkout.vue:909-921 | feedback-gap | confirmed | Add plain-language summary banner above fields |
| 🟡 | Cart page has no error state for API failures | 3 Cart | cart.vue:31-59 | missing-loading-empty | likely | Add error card + Retry to cart composable |
| 🟡 | Storefront profile/index stats fail silently (blank tiles) | 6 Order | profile/index.vue:205-219 | missing-loading-empty | confirmed | Check all .ok; show error card before stats grid |
| 🟡 | Admin order list shows “Deleted customer” though snapshot exists | 6 Order | nuxt/.../order/List.vue:245-254 | silent-lie | likely | Expose customer_snapshot in OrderResource as fallback |
| 🟡 | Public order tracking has no rate-limit (enumeration risk) | 7 Fulfillment | StorefrontCheckoutController.php:978-1040 | support-gap | likely | Add throttle middleware + CAPTCHA on repeated failures |
| 🟡 | Print-ready PDF generation failure is silent (logged, never surfaced) | 7 Fulfillment | DesignerController.php:1431-1459 | bad-error-state | likely | Add print_ready_status flag; warn designer + checkout |
| 🟡 | B2B domain rejection only at sign-up, not login / admin-created accounts | 1 Signup/Auth | CustomerAuthController.php:130-132 vs 262-320 | other | confirmed | Enforce domain check in login + Google + admin creation |
| 🟡 | Admin registration uses hardcoded min:8, ignores SecurityPolicyService | 1 Signup/Auth | AuthController.php:61 | field-drop | confirmed | Use SecurityPolicyService::passwordRules() |
| 🟡 | Mobile: cart/checkout tap targets may be <44px at 375px | 3/4 | cart.vue quantity + shipping selectors | mobile | needs-runtime | Audit at 375px; enforce min 44×44px |
| 🔵 | “Estimated Total” label misleads (excludes shipping) | 2 PDP/Pricing | ProductPriceSummary.vue:8 | silent-lie | likely | Relabel “Estimated Subtotal”; note shipping at checkout |
| 🔵 | Missing loading indicator during 300ms price-calc debounce | 2 PDP/Pricing | productInfo.ts:836-864 | bad-error-state | likely | Set isCalculating=true immediately on change |
| 🔵 | Coupon apply/remove methods exist but no cart UI (dead code) | 3 Cart | cartStore.ts:23-24, 459-493; cart.vue | field-drop | confirmed | Add coupon input + apply/remove UI to cart summary |
| 🔵 | Product placeholder image uses absolute path (breaks in subdir) | 3 Cart | cart.vue:89; cartStore.ts:161 | other | likely | Import asset or use relative path |
| 🔵 | Quantity update success not validated against response field | 3 Cart | cart.vue:613-619; cartStore.ts:356-358 | silent-lie | needs-runtime | Validate returned quantity in applyServerCart() |
| 🔵 | Server allows empty payment_method (client-only guard) | 5 Payment | checkout.vue:1314-1321; CheckoutRequest | silent-lie | confirmed | payment_method => required|string|min:1 + active-gateway rule |
| 🔵 | Offline payment clears cart before success page loads | 5 Payment | checkout.vue:1697-1710; order-success.vue:310 | missing-loading-empty | likely | Include order_uuid; defer cart clear until paid; email on create |
| 🔵 | Payment method not validated at draft time (error at intent step) | 5 Payment | CheckoutRequest.php:39; StorefrontCheckoutController.php:128-140 | other | confirmed | Custom ValidPaymentMethod rule scoped to store |
| 🔵 | Redirect-flow cart clear without order_uuid verification (replay) | 5 Payment | order-success.vue:800-828 | other | likely | Implement by-number resolver with email check |
| 🔵 | Quote custom-field snapshot drops B2B company/department context | 4 Checkout | StorefrontCheckoutController.php:673-846 vs 504-505 | field-drop | confirmed | Persist company_account_id/department_id on Quote |
| 🔵 | Confirmation email fallback text doesn’t state payment-pending | 6 Order | StorefrontCheckoutController.php:591-592 | other | confirmed | Reword fallback or defer email to paid |
| 🔵 | OTP logged in plaintext | 1 Signup/Auth | AuthController.php:285, 346 | other | confirmed | Log hash/truncated only |
| 🔵 | Remembered-device OTP = sha256(device_id) only, forgeable if cookie stolen | 1 Signup/Auth | AuthController.php:374 | other | confirmed | Bind IP/user-agent into hash |
| 🔵 | Design-lock failure at checkout allows instant resubmission spam | 4 Checkout | checkout.vue:1480-1484 | bad-error-state | likely | Inline 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 code — applyCoupon/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’sshipping_costcan 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_idis 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.
- 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.
- 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 onpayment_status='paid'. This pins the largest silent-lie cluster. - Add a shipping round-trip test —
cart total === order.shipping_costfor 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. - 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.
- Introduce a shared
ErrorStatecomponent for the storefront and adopt it onprofile/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”). - Centralize address + payment-method validation — one shared rule set for
new_shipping_address/new_billing_addressused by both order and quote paths, plus aValidPaymentMethodrule scoped to the store; remove the null-coalescing defaults that persist blank addresses. - Run a mobile 375px sweep on cart and checkout — confirm all quantity, shipping, and payment controls meet the 44×44px tap-target minimum.
- Surface gateway-config gaps at save time — validate/warn on missing
webhook_secretfor webhook gateways and add an Action Center card for pending cheque orders, so non-technical owners aren’t left with silently stuck orders. - 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.