# VeloCars marketplace launch

## Publish real stock

- In Inventory, complete each vehicle’s identity, asking price, description and listing photos, then explicitly publish it. Drafts, private records, customer-specific imports and inactive dealers remain excluded.
- Review the public listing. Acquisition costs, selling margins, VINs, internal notes and private documents are excluded. Missing specifications remain “Not provided”. Drivetrain and seating filters are omitted because those fields are not currently stored.
- Dealer settings → Public marketplace: approve public business contacts, choose whether reserved listings remain visible, opt into named-dealer import enquiries and optionally set test-drive request hours in Kenya time.
- Reservation hiding is temporary: releasing the reservation restores the previously published listing. Explicitly unpublishing withdraws it, including its sold history. Sold listings retain their asking price; private final selling prices are not published.

## Production configuration

- Set the production HTTPS `APP_URL`, build assets with `npm run build`, apply the marketplace migration and use the application’s normal deployment checks. Keep development OTP delivery disabled in production.
- Confirm existing OTP email delivery, and configure the existing SMS provider if phone sign-in will be offered. Marketplace enquiries are recorded in CRM; they do not claim that an email/SMS was delivered.
- Keep original photos and derivatives in private storage with sufficient disk space and backups. Public media requests recheck publication before serving a derivative or returning 304. Do not configure a CDN or reverse proxy to bypass that check or cache listing HTML across users.
- Run a queue worker for `GenerateMarketplaceImages` (JPEG fallbacks); WebP derivatives reuse the existing upload processing. Prepare existing published photos with `php artisan marketplace:prepare-images`; for a controlled immediate preparation use `--sync`. No vehicles are published by this command.
- Check `/`, `/cars`, a real published vehicle, its dealer profile, `/saved-cars`, `/compare`, `/import-a-car`, `/login`, `/workspace`, `/platform` and `/portal` with their appropriate accounts.
- Submit a real test-drive enquiry and confirm the correct dealer’s CRM enquiry, source, request status, activity and follow-up. Requests await dealer confirmation. Import enquiries go to one explicitly selected dealer and can be converted through the existing enquiry-to-order workflow.

## Search and content

- Confirm HTTPS canonical URLs, titles, descriptions, social previews, structured data, image alternatives, `/robots.txt` and `/sitemap.xml` on the production hostname. The static robots file was removed so the application can provide the correct sitemap URL.
- Submit `/sitemap.xml` in Google Search Console and monitor indexing, crawl errors and structured-data reports. Arbitrary filters and comparisons are noindex; useful inventory-backed make/model/body/location pages have their own URLs. Pagination keeps its page URL. Public listing slugs are immutable, so title edits do not break links.
- Review optional vehicle search titles and descriptions in the vehicle editor. Add accurate, original vehicle descriptions and complete approved dealer business information. Review the two starter guides before launch and maintain meaningful authorship/review dates when publishing further guidance.
- Withdrawn, deleted and ineligible listings return 404 rather than redirecting buyers to an unrelated page. Review traffic and sold-page usefulness periodically. Technical SEO does not guarantee rankings or AI citations.

## Verification and performance

- Automated coverage includes tenant boundaries, private media, publication changes, combined filters, comparison, guest/verified identity handling, OTP save merging, request idempotency/rate limits, request hours, import opt-in and reservation/sold history.
- Browser review: homepage, results, vehicle, comparison and saved pages at 1440×1000 and 390×844. Populated layouts were rendered from an isolated SQLite test database and labelled synthetic image fixtures; no demonstration accounts or inventory were inserted into the live database.
- Local Chrome timings are laboratory observations, not field Core Web Vitals or production capacity results. Recheck with real photographs, a cold cache, representative devices and throttled mobile networks after publishing stock. Collect real-user LCP, INP and CLS at the 75th percentile once traffic exists.
- Targets: LCP ≤2.5 s, INP ≤200 ms and CLS ≤0.1. No field INP score is claimed by this implementation. See [Web Vitals thresholds](https://web.dev/articles/defining-core-web-vitals-thresholds) and [field measurement guidance](https://web.dev/articles/vitals).
- Search references: [pagination and incremental loading](https://developers.google.com/search/docs/specialty/ecommerce/pagination-and-incremental-page-loading) and [product structured data](https://developers.google.com/search/docs/appearance/structured-data/product-snippet).

### Recorded local check · 26 October 2026

Final full suite: **298 passed, 5 environment-dependent tests skipped** (303 total). Subsequent targeted checks of the final enquiry validation and dealer settings: **30 passed**. Asset build, Blade compilation, formatter and whitespace checks passed.

Chrome desktop local measurements, followed by cold-cache 390×844 checks configured with 150 ms latency, 1.6 Mbps download, 600 kbps upload and 4× CPU slowdown:

| Page | Local LCP | Throttled mobile LCP | Measured CLS | Transferred bytes |
|---|---:|---:|---:|---:|
| Homepage | 212 ms | 1,092 ms | 0 | 203,992 |
| Results | 96 ms | 1,064 ms | 0 | 213,572 |
| Saved cars | 60 ms | 1,092 ms | 0 | 199,577 |

These live local pages contained no eligible published inventory; the saved page was a guest empty state. They are useful baseline page measurements, not representative photographic payloads or production guarantees. Separately, the five populated SQLite fixture layouts at both viewport sizes showed no page-wide overflow or JavaScript exceptions. Their synthetic-image LCP was 24–76 ms and CLS 0–0.040; those image fixtures must not be used to predict real photograph performance. Keyboard checks covered modal focus, Escape dismissal, the gallery, mobile filters and comparison differences. Saved and comparison controls were exercised in the browser; secure OTP merging and CRM routing were verified by application tests.

## Fair-exposure ranking · first version

Recommended is now the default discovery sort on the homepage and search. Price, newest and model-year sorts bypass exploration. First publication is immutable: the migration recovers the earliest retained publication event where available, falling back to the previously stored publication date. Ordinary edits cannot confirm availability.

Configuration lives in `config/ranking.php`:

- Signal weights: relevance 35%, quality 20%, freshness 15%, exposure opportunity 15%, availability confidence 10%, buyer interest 5%. Signals are normalised to 0–1. Broad browsing uses equal relevance; specific searches favour exact make/model identities.
- Target discovery allocation: primary 70%, new 20%, older 10%, with unused capacity filled from eligible alternatives. Pool selection uses a stable seeded weighted draw. Candidates are captured with SQL, not a catalogue-sized PHP collection or database `RAND()` sort. Assembly proceeds in bounded transactions; it does not impose a fixed top-candidate eligibility cutoff.
- Freshness declines over seven days. New exploration requires quality ≥0.60, confidence ≥0.65 and fewer than 20 recent impressions in the same filter context. Older exploration starts at 30 days, at most 10 recent contextual impressions, and a seven-day renewed-exposure cooldown. Hard buyer filters always hold.
- Quality caps useful photo coverage at four distinct view descriptions, counts six core specifications, checks for a clear price, and caps description credit at 40 distinct useful words. Repetition and excessive photo counts do not keep adding points. Optional drivetrain, seating and engine fields do not enter the completeness denominator.
- Opportunity compares seven-day impression rates within the normalised buyer-filter context, adjusted for listing age. Broad exposure is not used as the niche-search comparison cohort. Interest has a 30-impression prior at 2% and a 15% rate cap; each actor contributes at most one interest credit per listing in a seven-day window, while event types remain separate.
- Diversity targets two listings per dealer and three of the same make/model in a ten-result window, when alternatives exist. Specific searches preserve their strongest relevance tier. Constraints relax rather than hiding the only relevant stock.
- Result sets last six hours. Refresh, pagination and back navigation keep the result token; canonical URLs omit it. “Discover more cars” retains filters and starts a fresh selection. Newly published vehicles enter newly created sets. Ineligible vehicles disappear immediately on read, including from media and saved result sets. Withdrawn positions are left empty so later-page cars do not silently move backwards or get skipped. Price/year edits through controlled inventory forms invalidate affected explicit-sort sets; expired sets recover on the first page.
- Meaningful refresh boosts: at least a 5% and KES 50,000 reduction below the lowest observed asking price, a 0.15 improvement over best prior quality, or capped renewed confirmation on older stock. There is a 30-day cooldown, at most three awards per 90 days, seven-day duration and an eight-point maximum boost. Raising and lowering a price to an already seen level does not qualify. Decisions and reasons are retained.
- Availability reminders become due after 14 days. Confirmed confidence declines progressively to zero by day 45; unknown confirmation starts at 0.25 and does not qualify for exploration. Confirming stock is an explicit action in Listing performance & availability, and initial publishing explicitly confirms availability too.

### Jobs, reminders and rollout

The ranking migration is `2026_10_26_040702_create_marketplace_ranking_foundation.php`. It has been applied locally on MariaDB. No demonstration inventory or accounts were added to the application database.

1. Keep the existing queue worker running for incremental `RefreshListingSignals` jobs after public listing and photo changes.
2. Run the Laravel scheduler every minute (`php artisan schedule:run`). `marketplace:refresh-ranking` is scheduled every 15 minutes with overlap protection and a shared-cache single-server lock. It aggregates events, refreshes signals, reviews availability and prunes expired analytics/result sets. Use the same shared cache across scheduler nodes.
3. Run `php artisan marketplace:refresh-ranking` after deployment to initialise existing published history. This does not publish stock or infer confirmations from edits.
4. **Automatic pausing starts disabled:** `RANKING_AUTO_PAUSE=false`. Workspace reminders are available without an email provider. Enable `RANKING_EMAIL_REMINDERS=true` only with working production mail and verified active owner/manager addresses. Mail is sent immediately from the scheduled command, not queued. Inspect delivery errors in Platform → Marketplace ranking; retry corrected failures with `marketplace:refresh-ranking --retry-notices`.
5. Automatic pausing also requires a recorded successful reminder email, the configured overdue age, and at least the greater of seven notice days and a 30-day deployment grace period after the notice. Without delivery it will not pause stock. Enable enforcement only after reviewing notices and communication settings. Explicit reconfirmation clears the automatic pause but does not republish a deliberately private listing.

### Measurement and operational visibility

Card impressions require at least 50% visibility continuously for one second. Hidden tabs, modal dialogs and the mobile filter drawer interrupt the timer. Events are batched, deduplicated, bot-filtered and capped at five impressions per actor/listing/day across result sets. Opens, saves, successful enquiries and test-drive requests remain distinct. Failed submissions and guest-save account merges do not create conversion events. Analytics failure does not block browsing or successful CRM submissions.

Ranking analytics stores pseudonymous HMAC actor references, listing/dealer IDs, context hash, placement, position and result-set ID. It does not copy contact information or KYC. Raw analytics and daily aggregates are retained for 35 days. Result sets expire after six hours and are pruned after an additional day. Server-side eligibility remains authoritative; browser measurement is not a fraud-proof assertion of human attention.

Platform → Marketplace ranking shows visibility/exclusion reasons, quality, last context-specific signal/score snapshot, pool eligibility, activity, availability notices, active boosts, decisions and cooldowns. Vehicle → Listing performance & availability shows only the current dealer’s own aggregate performance and confirmation action. These figures are deduplicated discovery activity, not a replacement for CRM transaction counts or a promise of sales.

Reproduce the isolated demonstration with:

```sh
RANKING_REPORT_PATH=/tmp/velocars-ranking-demonstration.json php artisan test --compact --filter=MarketplaceRankingTest
node --test tests/JavaScript/*.test.mjs
```

The SQLite test dataset has 64 eligible listings across six dealers (stock counts 24/8/8/8/8/8), ages of 2/18/65 days, two quality levels, and recorded exposure on the 18-day stock. Across 30 deterministic first-ten result sets:

- Primary/new/older allocations were 210/60/30, matching 70/20/10 in this sufficiently populated test.
- Dealer selections were 55/41/60/60/35/49 out of 300 opportunities. No dealer exceeded two entries in any first ten. The same dataset’s newest-only first ten contained eight listings from one dealer and two from another.
- Twenty-eight distinct listings appeared. Two-day listings received 270 positions, 65-day overlooked listings received 30, and the already-exposed 18-day group received none in this experiment. All exploration selections passed the quality threshold; lower-quality records remained eligible for primary results but received no first-ten positions in this dataset.

These are controlled selection opportunities, not observed viewable impressions, field traffic, conversion improvements or proof of equal exposure for every listing. Monitor real cohort exposure, dealer concentration, small-sample interest and stale-stock notices before changing weights. Load-test snapshot storage and deep pagination with production-scale inventory; database snapshots trade storage for stable browsing and never bypass live eligibility checks.

Final ranking verification: **315 PHP tests passed, 5 environment-dependent tests skipped**, plus **11 JavaScript tests passed**. The ranking feature suite covers 17 cases. MariaDB migration, fresh filtered result creation, scheduler registration, asset build, Blade compilation, formatter and whitespace checks passed. The new platform list, score explanation and dealer performance views were inspected at 1440×1000 and 390×844 using isolated test-fixture HTML; no page-wide horizontal overflow or JavaScript exceptions were observed. A live local browser check confirmed Recommended as the default, result-token persistence in the browsing URL, the discovery action and a canonical URL without the token.
