Build Status & Roadmap¶
Snapshot: 27 August 2026 · production e56066f · in sync with main
This page is the state of the build: what is deployed, what has been verified against production, what is genuinely broken or missing, and what has to happen before launch. It is deliberately separate from Scope & Roadmap, which is the authoritative contract for what belongs in MVP / Scale / Vision. That page decides scope; this one reports reality.
Everything below was checked against production on the date above, not recalled from memory. Where a claim rests on evidence, the evidence is named.
1. Live systems¶
| Surface | Where | State |
|---|---|---|
| API | api.yam3at.com |
Live. 315 routes, 89 of them admin-only and all permission-gated. |
| Web dashboards | app.yam3at.com |
Live. 64 pages — 30 admin, 17 vendor. |
| Mobile | TestFlight only | 1.0.0+1. 48 screens. Android not on Play, by decision. |
| Infra | Hetzner 89.167.17.109 |
8 containers up: api, web, nginx, mysql, redis, meilisearch, scheduler, worker. Disk 18% (39G/226G). |
Shared box. Five other projects run on this host. Never restart anything
outside the yam3at compose project.
Backups. Daily at 02:00 to /var/backups/yam3at/, 17 retained. Verified
present. Restore procedure: operations/runbook.md.
Scheduler. 7 recurring jobs: quote expiry, booking auto-complete, subscription processing, vendor licence checks, calendar sync, FX refresh, ticket reservation expiry.
Security headers on the API, verified live: HSTS (2 years, includeSubDomains),
X-Frame-Options: DENY, X-Content-Type-Options: nosniff,
Referrer-Policy: strict-origin-when-cross-origin. APP_DEBUG=false,
APP_ENV=production.
Timezone. The platform runs on Asia/Kuwait (UTC+3, no DST), with the MySQL
connection pinned to +03:00 to match. Both halves are required: TIMESTAMP
columns convert on read using the session timezone, so an app sending Kuwait
wall-clock to a UTC session would store every new instant three hours wrong.
Verified live — PHP and MySQL both report the same local time, exactly three
hours from UTC, and API responses carry +03:00.
2. Integrations: wired, test, or fake¶
This table is the honest answer to "is it done?".
| Integration | Configured | Actually usable |
|---|---|---|
| Payments | MyFatoorah + Tap keys present | No — PAYMENT_DRIVER is unset, so it falls back to fake. MyFatoorah points at apitest.myfatoorah.com; the Tap key is sk_test_…. |
| Resend | Yes | |
| SMS / OTP | Twilio (+ Verify service) | Yes |
| Push | FCM (project, client email, private key all set) | Yes |
| AI | Anthropic, claude-sonnet-5, with daily + monthly budget caps |
Yes |
| Search | Meilisearch via Scout | Yes |
| Maps | Google Maps key (mobile-restricted) | Yes, gated behind MAPS_ENABLED at build time |
| Social login | Google OAuth; Apple on iOS | Yes |
| — | No. Sender is a no-op until Business API credentials exist. |
The mobile app's kOnlinePaymentEnabled build flag defaults to false, which
correctly matches the fake driver: both payment surfaces now say "قريبًا" rather
than offering a button that leads nowhere.
3. Production data¶
| Table | Rows | Note |
|---|---|---|
| users | 55 | 37 are load-test accounts |
| vendors | 15 | 12 are [LT] load-test, 1 is Audit Test Vendor |
| services / packages | 14 / 29 | |
| events | 31 | |
| RFQs / quotations | 28 / 77 | |
| bookings | 6 | event bookings (quote → accept) |
| orders | 3 | direct bookings |
| reviews | 0 | nothing has been reviewed yet |
| notifications | 30 | |
| categories | 31 | real catalogue |
| media | 3 | effectively no imagery |
| admins | 1 |
Platform settings in force: commission 5%, deposit 20%, payout hold
7 days, referral reward 2 KWD, loyalty 1 point/KWD redeemed at 100,
free vendor RFQ limit 5, super_app_enabled true, maintenance_mode
false, registrations open.
4. Verified working, end to end¶
These were walked on a real device (Android emulator) against production, not asserted from code:
- Direct booking: browse → service → package → date → slot → address →
payment → order confirmed (
YM-2026-000003), then cancelled cleanly. - Events / RFQ: create event with date and guest count → RFQ to a vendor → vendor quotes → customer accepts → booking created with the deposit computed correctly at 20% (290.000 of 1450.000).
- Planner: generated a checklist whose due dates are back-calculated from the event date — budget 120 days out, venue 90, guest count 60, invitations 30, final headcount 3.
- Auth: registration, login, session surviving app restart from the Keychain, logout, role-based navigation (customer vs vendor).
- Vendor panel: pending-state banner, RFQ inbox, quoting, analytics, pricing intelligence, gallery, calendar, blocked dates.
- Localisation: Arabic renders correctly throughout, including notification
titles and pricing advice, in both
aranden.
469 API tests pass (1 632 assertions).
5. Launch blockers¶
Ranked. Nothing below is a nice-to-have.
B1 — No real payments¶
PAYMENT_DRIVER is unset, so production uses FakePaymentProvider. MyFatoorah
is on the test sandbox and the Tap key is a test key. TAP_WEBHOOK_SECRET is
empty, so Tap webhook signatures cannot be verified even once a live key
lands.
To close: obtain the merchant account, set PAYMENT_DRIVER, swap to live keys
and base URLs, populate the webhook secret, then flip the mobile build's
ONLINE_PAYMENT dart-define. Until all of that is true, no money can move on
either the direct-booking or the events path.
B2 — Test data is live and public¶
37 load-test users, 12 [LT] vendors and Audit Test Vendor are approved and
published, so they appear in real vendor discovery.
To close: php artisan yam3at:load-test --wipe removes the [LT] set
precisely (it matches only rows whose owner's email ends in the load-test
domain, and is covered by tests). Audit Test Vendor must be removed
separately.
B3 — No imagery¶
3 media rows platform-wide. Vendor cards, service heroes and galleries fall back to placeholder tiles. A marketplace that looks empty converts like one.
To close: real vendor photography, plus the mascot/brand artwork. Note the launcher icon problem in G4.
B4 — Zero reviews, and rating is a ranking input¶
reviews is empty, so every listing shows "جديد / New" and the "Top rated" sort
— the default ordering on the browse screen — has nothing to rank by. The
machinery is correct and tested; it simply has no data.
To close: seed trust through early real bookings, or launch with a different default sort until reviews accumulate.
6. Known gaps, not blocking¶
| # | Gap | Impact |
|---|---|---|
| G2 | WhatsApp channel is a no-op. Notifier dispatches to it behind an enabled() check that is always false. |
Medium — WhatsApp is the dominant channel in Kuwait |
| G4 | ic_launcher_foreground.png is the full square icon, not a transparent foreground layer, so Android 12+ paints a yellow circle with a black square inside it on every cold start. The adaptive-icon XML is correct; only the asset is wrong. |
Low, but it is the first thing every user sees |
| G5 | Android release signs with debug keys (buildTypes.release uses signingConfigs.debug). Harmless while Android stays off Play, blocking the moment it doesn't. |
Deferred by decision |
Closed on 27 August¶
- G1 — mixed datetime types.
event_milestones.scheduled_atwas the onlyDATETIMEamong 210 datetime columns; the rest areTIMESTAMP, which converts on read using the session timezone whileDATETIMEnever converts. Storage and the API's ISO-8601 contract agreed only because the app timezone happens to be fixed. Converted. Production now reports zerodatetimecolumns. - G3 — scheduling tools under Analytics. Vendor calendar and blocked dates moved into the vendor panel beside pricing and gallery. Boost stayed in Analytics, beside the numbers that justify buying it.
- G6 — weak recommendations. Not a sparse-data artifact: candidates were drawn from every visible vendor, and 7 of the 15 in production sit in the home and cars domains, so a cleaning company was a legitimate suggestion for a wedding. Now scoped to the events domain, with the heuristic ranking the categories that suit the event type first. Verified live — a wedding returns venues → catering → photography → decor → flowers → entertainment, and no home-services vendor at all.
- G7 — unaudited settings. Settings decide commission, deposit, payout hold and whether registrations are open, and changed with no record of who moved them. Now audited with before/after values; an identical resubmit is correctly not recorded as a change.
7. Fixed this session¶
15 commits. The ones that mattered:
Would have blocked real users
- Saved addresses never loaded.
Address.fromJsoncastlatitudewithas num?; MySQL decimals serialize as strings, so it threw on every address. The list showed "no saved addresses" forever, checkout requires an address, so no direct booking could be completed by anyone. The throw also happened after a successful POST and wasn't anApiException, so it surfaced as "could not reach the server" — every retry silently created a duplicate. - Event date and guest count did not exist in the mobile app, though the API stored both and the planner derives every checklist deadline from the date.
- Event bookings were unreachable.
EventBookingsScreenwas referenced only whensuper_app_enabledis false; production has it true, so an accepted quote produced a booking with a deposit owing and no list view to reach it. - Availability slots were three hours off for Kuwait, hiding bookable evening slots and offering ones already gone.
- Service ratings and booking counts were never computed. Three columns shipped defaulted to 0 with nothing writing them, while catalog discovery sorted on exactly those columns.
Security
- Session token moved from a plaintext file in the cache directory to the Android Keystore / iOS Keychain, with a one-time migration. Verified on device: nothing token-shaped remains in app storage.
allowBackupdisabled with explicit data-extraction rules.- Catalog no longer lists direct-booking services that have no package.
Localisation
- All 25+ notification types were English-only regardless of locale. Titles now resolve per-reader, retroactively for rows already stored.
- Vendor pricing advice was English-only; the competitor line uses
trans_choicebecause Arabic inflects at 1, 2, 3–10 and 11+. - The network-error fallback behind ~20 screens was a hardcoded English string.
UX
- Sign-up rebuilt (it self-confirmed the password, so a typo created an unusable account, and had no invite-code field — referrals were dead on mobile).
- Pending vendors now see their application status, and rejected ones see the reviewer's notes.
- Unrated listings say "جديد / New" rather than "0.0 ★".
8. Roadmap¶
Now — close the launch blockers¶
- Live payment merchant account; set
PAYMENT_DRIVER, live keys, webhook secret; enableONLINE_PAYMENTin the mobile build (B1). - Wipe load-test and audit data from production (B2).
- Vendor photography and brand artwork, including a correct adaptive-icon foreground (B3, G4).
- Decide the pre-review default sort (B4).
Next — first real supply and demand¶
- Onboard and verify the first real vendors; the approval queue and status banner are ready.
- WhatsApp Business API (G2) — the highest-leverage channel in this market.
- Re-check recommendation quality once the catalogue is real. The scoping bug is fixed; what remains is whether the affinity ordering matches how Kuwaiti customers actually shop.
Then — scale¶
- Android to Play, which forces real release signing (G5).
- Vendor subscription billing, once payments are live.
- Escrow milestones — the code path exists and is tested, but is unexercised without real payments.
9. How to re-run this audit¶
# deployed version + container health
ssh root@89.167.17.109 'cd /opt/yam3at && git rev-parse --short HEAD && \
docker compose -f docker-compose.prod.yml ps'
# data snapshot
ssh root@89.167.17.109 'cd /opt/yam3at && docker compose -f docker-compose.prod.yml \
exec -T api php artisan tinker --execute="echo DB::table(\"users\")->count();"'
# test suite
cd apps/api && php artisan test
# mobile, against production
cd apps/mobile && flutter build apk --debug \
--dart-define=API_URL=https://api.yam3at.com/api/v1
Deployment procedure and rollback: DEPLOYMENT.md and operations/runbook.md.
Caution when scripting against production from Windows. Passing Arabic through a Git Bash
curl -dstring transcodes it to the ANSI codepage and stores????. Write the JSON to a file with explicit UTF-8 and usecurl --data-binary @file. Two records were corrupted this way during the audit and had to be repaired.