Backend:
- New tenants table with PSGC-derived code, links to
cities_municipalities, optional boundary_polygon override, theme
color, contact info, timezone.
- Adds nullable tenant_id to users / households / drop_off_points /
dumpsites / partner_stores. Foreign-keyed, indexed.
- Tenant model with deriveCode() helper + effectiveBoundary()
fallback chain.
- App\Tenancy\Tenancy — process-level current-tenant register with
withTenant() / withoutScope() helpers for jobs + super-admin.
- App\Tenancy\TenantScope — global Eloquent scope, no-op when no
tenant is set (so seeders + super-admin reads still work).
- App\Tenancy\HasTenant trait — applied to Household, DropOffPoint,
Dumpsite, PartnerStore. Auto-fills tenant_id on create from
Tenancy::current().
- ResolveTenant middleware — reads X-Tenant-Code (or X-Tenant-Id),
validates tenant exists + active, sets Tenancy::current(). Falls
back to authenticated user's tenant_id when header missing.
Registered globally on the api group.
- Login + register now require an active tenant (super-admin
bypasses). Cross-tenant credential reuse is rejected with a
403 + clear message.
- super_admin role added to RoleSeeder + users.role enum.
- Public GET /api/v1/tenants/lookup?code= — no auth, returns tenant
details for the pre-login screen.
Seeders:
- SuperAdminSeeder seeds super@verde.local (tenant_id = null).
- SanPascualTenantSeeder seeds Region IV-A → Batangas → San Pascual
municipality → sample Poblacion barangay → Tenant row with code
SAN-PASCUAL-BAT, then backfills every existing tenant-aware row
(13 users / 2 households / 5 DOPs / 1 dumpsite / 3 stores) so
the dev environment keeps working end-to-end.
- Wired into DatabaseSeeder so migrate:fresh --seed bootstraps
cleanly.
Customer-web:
- New /tenant page — text input, calls public lookup, confirms with
resolved tenant card, stores code + name in cookies (1 year).
"Pilot users: SAN-PASCUAL-BAT" hint as a clickable shortcut.
- /login + /register now redirect to /tenant?next= when no cookie,
show a verde "signing in to <LGU>" pill with a Switch link,
delegate the actual form to client components.
- /api/tenant route — POST sets cookie, DELETE clears.
- apiServer auto-attaches X-Tenant-Code on every API call when the
cookie is present.
- Tenant cookies are non-httpOnly so the client can echo them; the
session token stays httpOnly.
Build: 23 routes (added /tenant), 196 backend tests still pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1. API docs via dedoc/scramble at /docs/api (scoped to api/v1).
Linked from the admin sidebar Settings group.
2. Scheduled commands registered in routes/console.php:
- reports:aggregate (02:00) — daily/weekly/monthly aggregations
- qr:expire (02:30) — flips past-due allocated/active codes to expired
- trucks:prune-locations (03:00) — drops history older than retention
window (default 7 days, config('verde.location_retention_days'))
All idempotent + withoutOverlapping. --dry flags on qr:expire and
trucks:prune-locations for safe inspection.
3. Trip double-booking validation: AdminTripController::store rejects
new trips when the team or truck already has a non-cancelled trip on
the same date. override_conflicts: true bypasses for emergencies.
Cancelled trips don't block rebooking.
4a. Email verification: User implements MustVerifyEmail.
VerifyEmailNotification overrides verificationUrl() for our
namespaced route. Register sends the link automatically (best
effort, won't block signup). POST /auth/email/resend (auth) +
GET /auth/email/verify/{id}/{hash} (signed URL).
4b. Password change while logged in: POST /me/password validates
current_password, requires the new password to differ, revokes
every other active token on success — current session stays.
5a. PickupImminent notification: when TripStop -> arrived,
TripExecutor::notifyAssignedHouseholds() finds households whose
assigned_drop_off_point_id matches and sends DB + SMS.
5b. Auto-geofence on truck location: TruckTracker::record() now
auto-fires TripExecutor::arriveAtDumpsite() when an in-progress
trip's truck pings inside its dumpsite boundary. The executor's
status guard prevents duplicate timeline events if the driver also
presses arrive-dumpsite manually.
190 feature tests passing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Notifications: notification_preferences + Laravel notifications inbox.
SmsChannel adapter for our SmsService. RoutesByPreferences trait reads
per-user toggles. HouseholdApproved/Rejected, QrBalanceLow, and
CodesPurchased notifications wired in via auto-discovered listeners
or direct dispatch from controllers/StoreOperations.
Payments: payments table + PaymentDriver interface. ManualPaymentDriver
works out of the box; PayMongoDriver activates when
PAYMONGO_SECRET_KEY is set, falls back to manual otherwise. Resident
initiates code-purchase, admin can mark paid manually, webhook applies
real provider events. Fulfillment runs StoreOperations::sellToHousehold.
Live tracking (HTTP polling): truck_location_history (with SPATIAL
INDEX + 7-day retention plan). Driver POST /driver/trucks/{uuid}/location
writes history, updates trucks.last_known_coordinates, caches in Redis,
flags geofence-trigger when entering active trip dumpsite. Admin
GET /admin/live/trucks returns active truck positions. Reverb broadcast
deferred.
Flow corrections:
- QrAllocator now idempotent — re-approving a household no longer
re-dispenses free codes.
- arrive-dumpsite enforces dumpsite geofence via ST_Contains; can be
bypassed with override_geofence: true.
171 feature tests passing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>