# Vendor Lifecycle Audit — Complete System Analysis

Date: April 20, 2026
Status: CRITICAL — multiple disconnects blocking vendor activation

> **CURRENT STATE — re-verified 2026-04-21:**
>
> | # | Original blocker | 2026-04-21 status |
> |---|---|---|
> | 1 | Vendor profile has NO location fields | ✅ **RESOLVED** — `VendorProfileController::update()` accepts `address_1, address_2, city, state, postal_code, country` and creates/updates `VendorLocation` with `is_primary=true` (lines 56-100). |
> | 2 | Payment doesn't update vendor plan code | ✅ **RESOLVED** — `AccountSubscriptionService::activate()` line 141 writes `'current_plan_code' => $planCode`. |
> | 3 | Plan names wrong ("Verified" at $50) | ✅ **RESOLVED** — `sm_plans` now has `basic/featured/premium` for every tenant (ids 1-12). No "verified" rows remain. |
> | 4 | Legacy billing bridge incomplete | ✅ **RESOLVED** — `sm_listing_plans` now has `basic` rows (ids 7, 8). |
> | 5 | Onboarding marked complete before payment | ❌ **STILL BROKEN** — `OnboardingController::start()` line 162 calls `markOnboarded()` BEFORE the Stripe redirect at line 171. Unchanged. |
> | 6 | Checklist strictly sequential | ✅ **RESOLVED** — `VendorStateService::checklist()` line 53 comment: "Non-sequential — all steps accessible". |
> | 7 | Claim flow resets onboarding | ❌ **STILL BROKEN** — `ClaimOnboardingService::runPostClaimFlow()` lines 37-39 still calls `account.resetOnboarding()`. Partial guard in place (skips if already completed) but reset path still triggers for admin-created vendors. |
> | 8 | Duplicate plan rows | ℹ️ **CHANGED SHAPE** — `sm_plans` now has 4 copies (one per tenant, which is correct multi-tenant behavior, not duplicates). `sm_listing_plans` has 8 rows (2 tenants × 4 plans minus 1 `free`). Clean-up of the legacy `free` stubs (ids 1, 3 with `is_active=0`) still pending but not blocking. |
>
> **Net**: 5 of 8 blockers resolved since 2026-04-20. Remaining: #5 (onboarding timing) and #7 (claim flow reset).

---

## PLAN TIERS (corrected)

| Tier | Price | Status |
|------|-------|--------|
| Unclaimed | Free | System-generated listing, no owner, "Claim this business" CTA |
| Basic | $50/yr | Claimed vendor, contact visible, verified badge eligible |
| Featured | $150/yr | + RFQ, social links, featured placement |
| Premium | $350/yr | + video, SEO follow, no competitors |

**"Verified" is NOT a plan** — it's a trust status earned through document review.
**"Free" is NOT a plan** — unclaimed listings are the free tier.

The `sm_plans` table currently has "Verified" at $50 which should be renamed to "Basic".

---

## CRITICAL DISCONNECTS (prioritized)

### 1. VENDOR PROFILE HAS NO LOCATION FIELDS
**Severity:** BLOCKER — vendors cannot complete activation
**What:** VendorProfileController update() accepts: display_name, legal_name, website, email, phone, description_short, description_long. NO address fields.
**Impact:** VendorStateService::profileComplete() requires `vendor.locations.where(is_primary, true).exists()`. Vendor cannot satisfy this from the profile form.
**Result:** Checklist shows "Complete your business profile" → vendor goes to profile → fills everything → saves → checklist still says incomplete. Vendor is stuck.
**Fix:** Add address/city/state/zip fields to the vendor profile form. On save, create or update the vendor's primary VendorLocation. Auto-geocode.

### 2. PAYMENT DOESN'T UPDATE VENDOR PLAN CODE
**Severity:** HIGH — paid vendors show as free tier
**What:** AccountSubscriptionService::activate() sets listing.is_featured and listing feature flags, but NEVER writes vendor.current_plan_code.
**Impact:** Vendor.isFeatured() returns false. VendorStateService.hasActivePlan() works (reads AccountSubscription) but Listing.isPaid() through vendor chain fails.
**Result:** PoE Solutions paid $50 but their listing shows "Free tier" in the vendor portal.
**Fix:** In activate(), after applying plan to listing, also update vendor.current_plan_code with the plan code from the subscription item.

### 3. PLAN NAMES WRONG IN DATABASE
**Severity:** HIGH — confusing for vendors
**What:** sm_plans has "Verified" at $50 instead of "Basic". Also has 4 duplicate rows per plan (one per tenant).
**Impact:** Vendor sees "Verified - $50/yr" in checkout instead of "Basic - $50/yr".
**Fix:** Rename plan code from 'verified' to 'basic', name from 'Verified' to 'Basic'. Clean up duplicates.

### 4. LEGACY BILLING BRIDGE INCOMPLETE
**Severity:** HIGH — vendor portal shows wrong plan state
**What:** AccountSubscriptionService::applyPlanToListing() dual-writes a ListingPlanSubscription, but only if a matching ListingPlan exists. No legacy plan with code='verified'/'basic' exists.
**Impact:** Listing.activePlan() returns free default. Vendor portal plan badge shows "Free".
**Fix:** Either add a 'basic' ListingPlan to the legacy table, or refactor Listing.activePlan() to check AccountSubscription directly.

### 5. ONBOARDING MARKED COMPLETE BEFORE PAYMENT
**Severity:** MEDIUM — vendors who abandon checkout aren't re-engaged
**What:** OnboardingController::start() calls account.markOnboarded() before payment is initiated.
**Impact:** If vendor selects a plan but abandons Stripe checkout, they're marked as onboarded. No re-prompt on next login.
**Fix:** Only mark onboarded when payment succeeds (in activate()), or split into onboarding_viewed / onboarding_paid states.

### 6. CHECKLIST IS STRICTLY SEQUENTIAL
**Severity:** MEDIUM — blocks valid vendor actions
**What:** VendorStateService enforces: profile → verification → plan → category. Each step blocks the next.
**Impact:** A vendor who paid first (step 3 done) can't verify (step 2) because profile (step 1) isn't complete. Steps should be completable in any order.
**Fix:** Change checklist to non-blocking — show all steps, highlight what's done, but don't gate access.

### 7. CLAIM FLOW RESETS ONBOARDING
**Severity:** MEDIUM — admin-created vendors get re-onboarded
**What:** ClaimOnboardingService::runPostClaimFlow() calls account.resetOnboarding() which sets onboarding_completed=false.
**Impact:** If admin creates a vendor + user (already onboarded), and the claim flow runs again, onboarding resets.
**Fix:** Don't reset if already completed, or skip ClaimOnboarding for admin-created vendors.

### 8. DUPLICATE PLAN ROWS
**Severity:** LOW — data quality
**What:** sm_plans has 4 copies of each plan (one per tenant), sm_listing_plans has 2 copies.
**Impact:** Queries may return wrong plan. Plan selection dropdowns may show duplicates.
**Fix:** Clean up duplicates, ensure tenant scoping is correct.

---

## RECOMMENDED FIX ORDER

### Phase 1: Unblock Vendors (do first)
1. Add location fields to vendor profile form
2. Set vendor.current_plan_code on payment activation
3. Rename "Verified" plan to "Basic" in sm_plans
4. Add 'basic' to sm_listing_plans for legacy bridge

### Phase 2: Fix Billing Flow
5. Make checklist non-sequential (all steps accessible)
6. Defer onboarding_completed to payment confirmation
7. Fix applyPlanToListing() to handle 'basic' plan code
8. Make Listing.activePlan() check vendor.current_plan_code as fallback

### Phase 3: Clean Up
9. Clean up duplicate plan rows
10. Remove "Free Listing" from sm_listing_plans (is_active already false)
11. Don't reset onboarding for admin-created vendors
12. Add RFQ eligibility indicator to vendor dashboard

---

## FILES THAT NEED CHANGES

| File | What to change |
|------|---------------|
| `resources/views/vendor/profile/edit.blade.php` | Add location/address section |
| `app/Http/Controllers/Web/VendorPortal/VendorProfileController.php` | Accept & save location fields |
| `app/Services/Billing/AccountSubscriptionService.php` | Set vendor.current_plan_code on activate |
| `app/Services/Vendor/VendorStateService.php` | Make checklist non-sequential |
| `app/Http/Controllers/Web/VendorPortal/OnboardingController.php` | Defer markOnboarded |
| `app/Models/Listing.php` | activePlan() fallback to vendor plan |
| `sm_plans` table | Rename verified→basic |
| `sm_listing_plans` table | Add basic plan row |
| `app/Services/Claims/ClaimOnboardingService.php` | Don't reset if already onboarded |
