Findings — Partner Dashboard: Billing-Rail Mismatch + Residual Hardcoded Data¶
Companion to
PARTNER_DASHBOARD_BILLING_HARDCODED_AUDIT_PROMPT.md. Work done on dev (code/social), held for human review before dev→main. Smoke:scripts/smoke-marketplace-billing-rail.php— 20/20 PASS.
Prod confirmation (read-only, SSM, ron@vell.ai = user 6)¶
name="Ron" surname="Davis",marketplace_customer_id="i5MzSPW7LgH".- 1 subscription row:
stripe_status="marketplace_active",plan_id=1(Starter, $299),paid_with="stripe",ends_at=null,name="AWS Marketplace". - Before fix:
activePlan()= null,getCurrentActiveSubscription()= null → "Active Plan: None" on a paying customer. "Alex" greeting is a pure persona leak (he's Ron).
Track B — hardcoded / persona data (all in discovery/_hero.blade.php)¶
Sweep result: the only real leaks were in the hero. Emails use real $recipientName/$user->name;
the funnel/prov-badge taxonomy is honest + clearly Sample/Live/Soon/Connect badged; ChatContextBuilder
grounds on live state. The voiceover-isolator "Alex" is a TTS voice label (legit, untouched).
Fixed:
- Greeting name (L31 established, L53 empty-state): "Alex" literal → :name interpolation from
$hero['name'] (signed-in user's real first name, first token of users.name, falls back to "there").
Set in UserController::discoveryDashboardData().
- Eyebrow (L26): hardcoded "… · Starter · Acct ••3440" → AWS Marketplace Partner + the real plan
name ($hero['plan'] from activePlan()) when present; the fake account suffix is gone.
- Fabricated trend (L95): the green "▲ up from 64 · last 30d" (only shown in Sample state, but looked
live) → honest "Illustrative preview — run a check to score yours".
- Funnel tiles (28.4k, 2.1%, $312k, etc.) were already Sample/Connect-badged via prov-badge —
left as-is (out of scope; honestly labeled). Noted as the remaining sample surface.
Track C — billing rail¶
BUG 1 (marketplace subs invisible to the plan system) — FIXED. The active-status whitelist omitted
marketplace_active and was copy-pasted in 4 places (the code even called it "the canonical whitelist").
Centralized into one source of truth: App\Models\Finance\Subscription::ACTIVE_STATUSES (adds
marketplace_active), referenced from:
- Helper::getCurrentActiveSubscription() — app-wide feature gating + "Active Plan".
- User::relationPlan() (hasOneThrough Plan).
- NavbarService::getUserPlan() — menu/feature visibility.
CheckSubscriptionEnd (gateway-renewal cron) and PaymentProcessController::deletePaymentPlan were
deliberately NOT changed — AWS drives the marketplace lifecycle via the entitlement webhook, and
marketplace rows aren't gateway-cancellable.
BUG 2 (wrong rail exposed) — FIXED. Detection keys off User::isMarketplaceCustomer()
(marketplace_customer_id != null — NOT paid_with, which is "stripe" on real rows).
- subscriptionPlans.blade.php branches on $isMarketplaceUser:
- Active Plan tile resolves via activePlan() (marketplace rows store name="AWS Marketplace", so
the name-based getSubscriptionName() returns '' — bypassed).
- Renewal tile → "Managed by AWS Marketplace" (getSubscriptionDaysLeft() keys off paid_with and
has no ends_at → wrong/empty — bypassed).
- The standalone Stripe "Select a Plan" grid + "Upgrade Your plan"/"Cancel My Plan" are hidden and
replaced by a branded card with "Manage in AWS Marketplace" (deep-link, config('app.aws_marketplace_buyer_url'),
default https://aws.amazon.com/marketplace/library) + "Request a higher-tier offer"
(route('request-private-offer') — already wired). External users see the standalone flow unchanged.
- Route-level guard: PaymentProcessController::denyMarketplaceRail() added and called at the top of
startSubscriptionProcess and startSubscriptionCheckoutProcess — a marketplace user hitting a
direct/stale subscription link is refused (hidden buttons ≠ closed route). startPrepaidPaymentProcess
(one-time token packs, not a subscription-rail conflict) intentionally left open.
Product decision (confirmed with human)¶
Upgrade UX = deep-link to AWS now + an in-app "Request a higher-tier offer" button
(→ existing request-private-offer flow).
Files changed (dev, not yet promoted)¶
app/Models/Finance/Subscription.php—ACTIVE_STATUSESconstant.app/Helpers/Classes/Helper.php,app/Models/User.php(+isMarketplaceCustomer()),app/Services/Navbar/NavbarService.php— reference the constant.app/Http/Controllers/Dashboard/UserController.php— heroname/plan.app/Http/Controllers/Payment/PlanAndPricingController.php— marketplace view data.app/Http/Controllers/Finance/PaymentProcessController.php—denyMarketplaceRail()guard.resources/views/default/panel/user/dashboard/discovery/_hero.blade.php— Track B fixes.resources/views/default/panel/user/finance/subscriptionPlans.blade.php— Track C branch.scripts/smoke-marketplace-billing-rail.php— new (20/20).
Notes / follow-ups¶
- No DB migration needed — the bug was read-side resolution, not data.
ron@vell.ai's existingmarketplace_activerow resolves correctly once the fix deploys (verified by smoke against the same shape). - Buyer deep-link wired:
config('app.aws_marketplace_buyer_url')(envAWS_MARKETPLACE_BUYER_URL) defaults tohttps://aws.amazon.com/marketplace/pp/prod-fcqhg7gqf2nxe(the Vell SaaS product page). The product id is permanent — it does NOT change when the listing moves Limited → Public (only theStatusfield changes). Product code2gpfhs29j2p1paoynnm0ifm2l; ARNarn:aws:aws-marketplace:us-east-1:137068223442:AWSMarketplace/SaaSProduct/prod-fcqhg7gqf2nxe. While Limited, only allowlisted accounts see the page — subscribers are allowlisted, so the link works for them. Override per-env viaAWS_MARKETPLACE_BUYER_URLif a different destination is wanted. - Feature-gating blast radius (BUG 1) was app-wide via
getCurrentActiveSubscription; the single-constant fix covers all callers. Re-verify nothing else duplicates the list before adding a 5th status someday.