Skip to content

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.php20/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.phpACTIVE_STATUSES constant.
  • app/Helpers/Classes/Helper.php, app/Models/User.php (+ isMarketplaceCustomer()), app/Services/Navbar/NavbarService.php — reference the constant.
  • app/Http/Controllers/Dashboard/UserController.php — hero name/plan.
  • app/Http/Controllers/Payment/PlanAndPricingController.php — marketplace view data.
  • app/Http/Controllers/Finance/PaymentProcessController.phpdenyMarketplaceRail() 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 existing marketplace_active row resolves correctly once the fix deploys (verified by smoke against the same shape).
  • Buyer deep-link wired: config('app.aws_marketplace_buyer_url') (env AWS_MARKETPLACE_BUYER_URL) defaults to https://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 the Status field changes). Product code 2gpfhs29j2p1paoynnm0ifm2l; ARN arn: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 via AWS_MARKETPLACE_BUYER_URL if 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.