RideKit
Changelog

Everything that shipped — bugs included

Rendered at build time from docs/CHANGELOG.md in the product repo. The "Fixed" sections stay on the record — a changelog with only features is marketing.

2026-09-08 — mobile: store builds, background-location disclosure, demo mode

  • Local signed store builds: mobile/scripts/build-store-aab.sh builds a signed AAB (+ universal APK) for any app from the committed native project. Release signing comes from RIDEKIT_UPLOAD_* environment variables through a new Expo config plugin (plugins/withReleaseSigning.js in each app) that survives expo prebuild --clean; ANDROID_VERSION_CODE / IOS_BUILD_NUMBER are read from the environment.
  • Driver app: prominent disclosure before background location. Google Play requires an in-app disclosure with explicit consent before the system prompt for background location; the driver app now shows it (14 languages) the first time a trip starts, and a decline keeps the trip on foreground pings.
  • Demo mode (EXPO_PUBLIC_DEMO_MODE=1, our store demo builds only): an amber DEMO strip on every screen, demo sign-in hints, mobile/scripts/demo-build.env profile. Compiles out of buyer builds.
  • Play Store listing kit for the three demo apps: docs/marketing/play-store/.
  • Sales site: privacy policy now covers the demo apps and no longer shows an owner TODO banner.

2026-09-08 — sales site: payments confirm without webhooks, real order emails

  • Payment confirmation no longer depends on the gateway webhook. The success page asks Razorpay/Stripe directly whether the checkout is paid and fulfils it on the spot; the account page re-checks that account's open checkouts on every load (new ledger data/checkouts.json, written when a checkout is minted); POST /api/checkout/reconcile (Bearer owner secret) sweeps everything pending. Fulfilment stays idempotent on provider + reference, so these paths can race the webhook. Lookups are throttled.
  • Order emails are sent through Resend when RESEND_API_KEY + EMAIL_FROM are set (text + HTML, reply-to, idempotency key per order). Every mail — sent or not — is recorded in data/outbox/ with its delivery result. "Email me the key" re-send on the account page (3/hour per order). Delivery failures never fail the order.
  • Success page shows the order and the license key when the arrival is trusted (Razorpay signed redirect, Stripe session id, or the signed-in owner), and polls itself while the gateway still reports the payment as pending.

[Unreleased] — Fleet partner console (2026-09-07)

Added

  • Sales site: a buyer community — /community on ridekit.dev: issues, improvements, feature requests and team announcements, votes, statuses (planned / in progress / shipped) that build a public roadmap. Reading is public; posting signs in with the license key, the same proof the buyer portal takes. Self-hosted in the site's JSON data store, no third-party service. npm run issue-license records a manual order and prints a key for marketplace or offline buyers so they get the same portal, community and downloads.
  • Sales site: the plan on the pricing page — 6 months of support and updates included, a yearly renewal after that, community access for life, and an optional done-for-you setup service; prices are environment values.
  • Sales site redesign — light, product-led, every screenshot a real capture from the end-to-end run, two self-hosted recordings.
  • Fleet partner console — the fleet owner's app and web panel now run a business rather than describe one: live driver status (on a trip / online / offline, last seen, the car they are on), per-driver detail with trips, documents (status and expiry only) and per-driver terms, fleet-owned vehicles (register, edit, assign, free, retire), a trips list and detail with the money split, earnings by day with trip shares and lease charges apart, a wallet with the fleet's ledger and withdrawals from the fleet's own account, and an account screen. Backend: GET /fleet (live stats), /fleet/drivers (live rows, lookup?phone=, :driverId detail, terms PATCH), /fleet/vehicles (create / update / remove / types), /fleet/trips (+ :rideId), /fleet/earnings (series + split), /fleet/wallet, /fleet/payouts (+ limits), PATCH /fleet (profile).
  • Self-serve fleet application — POST /fleet/apply: an account that owns nothing applies from the app, the fleet is created unverified, and the fleet_owner role is granted. Nothing had ever granted that role: an operator could create a fleet and name its owner, and the partner surfaces built for them answered 403. Now granted on application, on operator creation, and on an ownership change; existing fleet owners are backfilled by migration 1700000072000-FleetConsole.
  • Operator side — Add fleet partner (owner found or created by phone), Open on a fleet for everything the owner sees plus verify/active switches, applications flagged with the date they applied. /admin/partners/* behind partner.manage.
  • Fleet payouts — payout_requests.organization_id; a fleet withdrawal reserves from and refunds to the fleet_owner ledger account, and the operator queue names the fleet.
  • 15-language admin catalogs and en/hi app catalogs for all of the above, with parity tests; admin e2e specs for the partners page, the partner panel and every operator screen; Maestro flows for a full ride on two emulators and for the fleet app; mobile/scripts/e2e-side-by-side.sh.

Fixed

  • The operator's Partners list never returned isVerified, the settlement terms, or the driver/vehicle counts — every fleet rendered as unverified with "0 drivers · 0 vehicles" whatever the row held.
  • A fleet owner's token opened the operator console by URL (an RBAC-empty shell where every call 403s); the console now routes them to their panel.
  • The partner app told every fleet owner they were not one when the fleet check failed for any reason; a 403 now leads to the application form and keeps the session, and any other failure says what it is.
  • site-template's colour fallback was a different teal from every other shipped default.
  • On the drivers, riders, trips, parcels and vehicles lists, the filter-to-URL sync (router.replace) could cancel a row's Open navigation that had just started; the URL is now updated with history.replaceState, which does not touch the router.
  • A fleet could never register a plate that had belonged to a retired car: the clash check read every vehicle ever while the unique index is partial on deleted_at IS NULL. Live vehicles only, on create and update.
  • The fleet console's Add-vehicle modal could open before the vehicle-type catalogue arrived and then stay disabled with a type visibly selected; it now takes the first type when the catalogue lands.

[Unreleased] — Platform completion phases 1–4 (2026-08)

Added

  • Bidding/negotiation end-to-end — inDrive-style offer/counter-offer UI on both apps over the existing PricingMode.BIDDING backend, with a one-shot 60-second bid grace on dispatch.
  • Refunds — operator refund console (payment.refund permission): wallet debit first, then gateway refund with compensation on failure; refuses insufficient balance stating the max refundable. Corporate rides get credit notes instead (no gateway leg to reverse).
  • Women-only rides — market-gated (womenOnlyEnabled), fails closed at booking before pricing; driver side filtered in dispatch SQL.
  • Live trip sharing — tokenized public tracking page (identity, phone and money structurally excluded from the payload); unknown/revoked/expired tokens all 404 identically.
  • Safety centre — trusted contacts (cap 5), SOS fan-out SMS carrying a live-trip link, automatic ride check-in (long-trip and stationary signals, escalation to a HIGH incident), emergency numbers for all 37 markets in market config. Audio recording deliberately deferred — see docs/safety-audio-recording.md for the reasoning and the full design.
  • Driver retention — destination mode (2/day, dropoff must close ≥25% of remaining distance), trip preferences (pickup distance / min fare / long-trip / tier opt-outs), auto-accept through the normal accept path, payout limits endpoint with honest settlement estimates.
  • Corporate rides — book on a company account with credit-limit enforcement at booking, org settlement as a first-class ledger account (negative balance = the month-end invoice), org-owner portal in the rider app, admin billing console with credit notes and company payments.
  • Parcel delivery — full vertical: OTP-at-both-ends + photo proof of delivery (codes redacted from the courier's app), priced failure paths (free retry → return-to-sender at 60%, courier paid both legs), chain of custody in admin, per-market size/weight tier pricing, market gating.
  • Rentals — hourly packages (1h/10km … 12h/120km) seeded from each market's own economy fare, live overage meter mirrored in both apps, mid-trip destination edits, package-expiry sweeper, rider-ends-the-trip semantics.
  • Intercity/outstation — one-way (1.3×) and round-trip (0.85× with a 250 km/day floor), driver daily/night allowances and toll/permit line items, driver opt-in as a dispatch predicate, 30-minute dispatch lead on scheduled departures.
  • Local payment rails — M-Pesa STK push, Paystack, Flutterwave, OXXO vouchers (top-up only) and GCash behind a new AsyncTopUpProvider interface (instruction + later-outcome semantics); Pix retrofitted onto the same path. Each rail declares an honest integration status the admin shows as "Keys set · unverified" until proven against the live provider.
  • Languages — Thai, Vietnamese and Indonesian across rider, driver and admin (catalogs now 14/14/15), parity-test enforced.
  • Public banners endpoint — GET /content/banners?audience= serving only live, windowed banners with an app-safe payload.

Fixed

  • Ledger-corrupting minor-unit bugs (formatMoney/toMinorSafe hardcoded /100; JPY and KWD amounts rendered and stored wrong).
  • Tax invoices claimed issued without any authority response; local fallback is now local_only.
  • Re-inviting a revoked org member silently did nothing (stale soft-delete timestamp written back over the restore).
  • Unaccepted parcels were uncloseable (no_drivers terminal-state gap).
  • Payout reconciliation: confirmed failures now return driver funds; uncertainty never moves money.
  • Rail errors no longer surface to riders as HTTP 500.
  • backend/storage/ untracked from git — dev uploads (including driver KYC test documents) no longer ship in the product archive.

[Unreleased] — Gap Closure (P0 → P3)

Closing every item in Claude_Code_Gap_Closure_Spec.md, sequence admin → rider → driver. Each item verified end-to-end (UI → API → DB → response), all four codebases (backend / admin / rider / driver) tsc-clean.

Added

  • Multi-stop rides (up to 3) — multi-leg fare pricing (distanceKmMultiPoint), rider "+ Add stop" flow + multi-leg polyline, driver per-stop arrival progression, POST /driver/rides/:id/stops/:index/arrive (idempotent, ownership-checked). Verified: fare rises monotonically with total leg distance; 4th stop rejected (400).
  • Driver incentives / quests — operator-defined target campaigns (rides or earnings goal → wallet reward), admin CRUD (/admin/incentives), driver quests screen with live progress, idempotent ledger-backed award (posts as adjustment → shows in earnings). Verified: reaching target pays once, never double-pays.
  • Country Preset Pack — canonical 37-country data (currency/locale/timezone/dial/distance-unit/gateways/tax-engine/top-up presets), seed migration, installer applies + activates a market on onboarding, both apps' COUNTRY_DIALS regenerated from the same source.
  • Distance units (km/mi) — per-market distanceUnit on /branding, formatDistance() helper (km→mi at render), i18n unit labels.
  • Market-driven dates & timezone — locale-aware client date helpers; server-side formatInMarketTz() (Intl, zero deps); daily report buckets + tax-invoice issue date rendered in the market IANA timezone (storage stays UTC).
  • Call masking — provider abstraction (Twilio Proxy-style) with direct-dial default; GET /rides/:id/call-number returns a masked or real number, ownership-checked; both apps dial via the endpoint.
  • Government e-invoicing live wiring — all 4 tax engines (India GST / ZATCA / CFDI / NFS-e) credential-gated: real authority/GSP filing when configured, deterministic local invoice as dev/B2C fallback; never blocks a ride. See docs/E-INVOICING.md.
  • Referral programme, broadcast (push/email/SMS), CMS FAQ + Blog, provider credential store (AES-256-GCM), SMS providers (Twilio/MSG91), S3/R2 storage, RazorpayX payouts, social login (Apple + Google) — full slices, all verified.
  • Docs — E-INVOICING.md (per-country credential requirements), BRANDING-ASSETS.md (white-label icon/splash spec).

Changed

  • White-label theming — welcome-screen gradients, Razorpay checkout name/theme, push LED colour, and wallet top-up presets all now derive from the market/branding config instead of hardcoded values; app icon/splash paths are env-overridable.
  • Status colours tokenized (rider + driver) — hardcoded success/danger/warn hex replaced with theme tokens that adapt to dark mode.
  • Money formatting — one shared formatMoney + useInstallation across the admin app (removed 10 local money() helpers).

Fixed

  • Promo money bug — a shown discount is now actually charged (promo persisted at booking, settled on the discounted fare).
  • Saudi gateway routing — SA now resolves to Moyasar (was falling through to Stripe); removed gateway keys with no provider code.
  • Socket event mismatch — offer-expiry now dismisses correctly (ride:offer_expired shared constant).
  • Silent error swallows — stale-tracking / fake-zero-balance / invisible-reject-failure surfaced to users; backend config-load failures now logged before defaulting (never silently price on defaults).
  • GeoJSON coords — pickup/drop-off markers now render from the raw ride entity's PostGIS GeoJSON in both apps.

[Unreleased] — Phase 3: Multi-Entity Panels

Added

  • Admin panel (Next.js, /admin) — OTP login, live dashboard (stats + trips, auto-refresh, sparklines, status pills, skeletons), drivers, riders, reports, dispatch. Premium UI pass (dark sidebar + light content); a deeper redesign is still planned.
  • Dispatcher — manual booking on a rider's behalf, force-assign + change-driver (ride.assign gated). Live-verified.
  • Geofencing / zones — PostGIS polygon zones (service / restricted / airport / surge), point-in-zone (ST_Covers, GIST-indexed), serviceability check. Live-verified with real Bangalore polygons.
  • Fleet / Corporate / Franchise — unified organizations model + memberships; create, add/remove members, scoped reports (fleet earnings / corporate billing vs credit limit / franchise city), owner-only isolation (403 otherwise). Live-verified all three types.
  • Docs — Admin Guide (panels, dispatch, zones).

Fixed

  • Added admin panel origin (:3200) to backend CORS (was blocking the panel login).

[Unreleased] — Phase 2: Money & Operations

Added

  • Wallet on the double-entry ledger — top-ups, balances, idempotent.
  • Ride settlement + commission — completing a ride splits the fare (rider → driver + platform commission) in one balanced entry; commission per vehicle type, floored.
  • Pricing engine extras — configurable surge/night multipliers (pricing_rules, compounding, midnight-wrap windows), waiting charge, flat toll.
  • Cancellation policy engine — admin-configurable free window, late-cancel fee, no-show fee, driver/rider fault; rider may go into debt for mandatory fees.
  • Promo codes (flat/percent + cap, limits, validity) and referrals (refer-a-rider + refer-a-driver, separate rewards) — all credited via the ledger.
  • Payout engine — driver wallet → bank, reserve→process→paid/failed-refund, pluggable rails (manual + RazorpayX stub), payout.approve gated.
  • Payment integration — gateway charge → wallet top-up bridge; real Stripe (PaymentIntents, India 3-D Secure handled via webhook capture), signed webhook verification; manual/cash provider; refunds.
  • Reports — revenue, drivers, customers, trips, tax (taxable base) — RBAC report.view, date-ranged, reconciled from the ledger.
  • Docs — Admin Guide (money & operations), User Manual (wallet & payments).

Fixed (found via live e2e testing)

  • Cancellation fee on a rider with no balance was blocked — rider wallet now goes into debt for mandatory fees.
  • Assigned driver was not released back to online on cancel/no-show.
  • Discovered (via real Stripe keys) the account is India-registered → server-side auto-capture isn't possible; reworked to webhook-driven capture.

[Unreleased] — Phase 1: Core Ride Loop

Added

  • Ride schema — vehicle types, vehicles, driver profiles (PostGIS location), rides, immutable ride-event audit, ride bids. GIST spatial indexes.
  • Driver presence — online/offline, location updates (PostGIS geography).
  • Dispatch engine — PostGIS nearest-driver query (ST_DWithin + ST_Distance), distance + rating weighted, GIST-indexed. Live-verified ranking.
  • Fare estimate — base + per-km + per-minute, min-fare floor, per vehicle type; integer minor-unit money math.
  • Ride loop — book → accept → arrive → start → complete, enforced by a state machine; every transition audited and pushed to rider + driver.
  • Bidding — inDriver-style negotiation toggle (rider bids, driver counters, rider accepts at agreed fare).
  • Realtime — Socket.IO ride:status / driver:location events to per-user rooms (JWT-authed, Redis-adapter fan-out). Live-verified.
  • Basic admin — RBAC-gated live trips, dashboard stats, driver/rider lists.
  • Docs — User Manual (booking), Driver Guide (going online + trip flow).

Fixed (found via live e2e testing)

  • rides.pricing_mode column mapping missing on the entity (every ride fetch 500'd).
  • Bidding accept DTO missing a validation decorator (rejected by global whitelist).

[Unreleased] — Phase 0: Foundation

Added

  • Project scaffold — NestJS + TypeScript, ESLint/Prettier, Node 20 pinned (.nvmrc, engines).
  • Config + env validation — typed config; boot fails fast on missing/weak vars (JWT secret ≥32 chars; prod rejects placeholder secrets and empty CORS).
  • Database — PostgreSQL + PostGIS via TypeORM; UUID PKs, soft deletes, dormant tenant_id, migration tooling. Extensions auto-enabled by first migration.
  • Wallet ledger — double-entry, immutable. DB-enforced: per-currency balance, ≥2 postings, positive amounts, immutability triggers, one system account per (owner_type, currency).
  • RBAC — roles + permissions + @RequirePermissions guard; 8 system roles seeded.
  • Auth — phone OTP (Redis, hashed), JWT access + rotating refresh tokens (theft detection), password login, social-login structure, SMS provider plugin (dev log provider).
  • Payments — gateway plugin layer (manager pattern); manual/cash provider + Stripe stub with HMAC webhook-signature verification.
  • Storage — local + S3/R2 driver abstraction, config-selected; path-traversal guarded.
  • Realtime + queues — Socket.IO with Redis adapter (multi-instance), BullMQ background jobs.
  • Observability — pino structured logging, Sentry capture filter, /api/health readiness (DB+Redis, returns 503 on failure with bounded Redis ping).
  • Ops — Docker multi-stage image (non-root), docker-compose (Postgres+PostGIS, Redis), GitHub Actions CI (lint→build→migrate→test→docker build).
  • Docs — Installation + Deployment drafted; section tree scaffolded.

Fixed (found via live testing during Phase 0 audit)

  • Ledger: cross-currency false-balance, duplicate system accounts, zero-posting entries, mixed-case currency split wallets, soft-delete vs immutability crash, idempotency race returning 409/500 instead of the existing entry.
  • Runtime: health check hanging on dead Redis; 5xx errors silent without Sentry DSN.
  • Auth: refresh replaced_by typed uuid vs token hash (every refresh 500'd).
  • Build: broken package-lock.json failing npm ci in Docker/CI.