Build Status & Roadmap¶
Snapshot: 17 September 2026, except §7 which carries later changes · production in
sync with main
The figures on this page were checked against production on 17 September. Anything shipped since then is listed in §7 under its own date; the rest has not been re-checked.
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.
Open work: the owners' first revision round is tracked in Revision Round 1 — Todo. 47 of 48 items are closed; the one open is the regression gate, open for iOS only.
Everything below was checked against production or the repository on the date above, not recalled from memory. Where a claim rests on evidence, the evidence is named.
At a glance¶
Built, live and demo-ready — not yet launchable. Four things stand between this build and real customers, and three of them need the owners rather than code:
| Blocker | Needs | |
|---|---|---|
| B1 | No real payments — sandbox keys, online payment switched off | Owners: a live merchant account and keys |
| B3 | No imagery — 3 media rows platform-wide | Owners: vendor photography and brand artwork |
| B4 | Zero reviews, while "Top rated" is the default sort | Owners: a default sort until reviews exist |
| B5 | The regression flows have never run on an iPhone | A current TestFlight build, and one pass on a phone |
1. Live systems¶
| Surface | Where | State |
|---|---|---|
| API | api.yam3at.com |
Live. 323 routes; the 91 admin routes all sit behind admin authentication and a named permission. |
| Web dashboards | app.yam3at.com |
Live. 64 pages — 30 admin, 17 vendor. |
| Mobile | TestFlight only | Version 1.0.0; the build number is set per upload. 49 screens. Android not on Play, by decision. |
| Docs | docs.yam3at.com |
Live; this page and the revision round. |
| Infra | Hetzner <production-host> |
8 containers up: api, web, nginx, mysql, redis, meilisearch, scheduler, worker. Disk 20% (42G/226G). |
Which commit TestFlight carries is not recorded here. The mobile fixes from
17 September — the sign-in keyboard wait and the booking screen's opening day — reach
testers with the next run of the ios-release workflow.
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/, kept 14 days, plus the manual
backups taken before each production cleanup. 22 files present; the newest is
17 September 02:00. Restore procedure: operations/runbook.md.
Scheduler. 10 recurring jobs: quote expiry (hourly), calendar sync (hourly), unpaid-checkout expiry (every 5 minutes), ticket reservation expiry (every 15), booking auto-complete, subscription processing, vendor licence checks, FX refresh, expired-token pruning and idempotency-key pruning (daily).
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 on 17 September — PHP and MySQL report the same time, with +03:00.
The decision and its trade-off are recorded in
ADR-0001.
2. Integrations: wired, test, or fake¶
This table is the honest answer to "is it done?".
| Integration | Configured | Actually usable |
|---|---|---|
| Payments | MyFatoorah token + webhook secret; Tap key | No — sandbox only. PAYMENT_DRIVER=myfatoorah, still pointed at apitest.myfatoorah.com. The Tap key is sk_test_… and TAP_WEBHOOK_SECRET is empty. |
| Resend | Yes | |
| SMS / OTP | Twilio (+ Verify service) | Yes |
| Push | FCM (project, client email, private key) | Yes — last seen delivering on 16 Sep, including a tap on a cold-started app opening the order |
| 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 — the Apple button is confirmed on iOS in the R1-46 screenshots |
| — | No. No credentials exist; the sender is a no-op. |
Checked on 17 September: every key above is present (values not read). Email, SMS and AI were not re-exercised that day.
Online payment is switched off on the server, not only hidden in the app. The
online_payments_enabled setting is false, and while it is off the server refuses
every online charge — order, plan, deposit, milestone, ticket, boost and paid
subscription. The app and the web booking page read the same switch, so both show
KNET and card as "قريبًا". Cash is unaffected. Turning it on is audited (R1-10).
3. Production data¶
The test data is gone. On 16 September the load-test set, the audit accounts, the QA leftovers and the two old service-app demo vendors were removed — 418 rows across 30 tables, after a backup and a dry run — and replaced with eight curated events providers. Details in Revision Round 1, items R1-37 to R1-42.
| Table | Rows (17 Sep) | Was 27 Aug | Note |
|---|---|---|---|
| users | 17 | 55 | 4 real people, 1 admin, 2 deletion tombstones, 8 demo providers, 1 demo customer, 1 CI account for the iPhone screenshot run |
| vendors | 8 | 15 | all 8 are curated demo providers, all published |
| services / packages | 8 / 16 | 14 / 29 | one service each, two packages each |
| events | 5 | 31 | created by real users, plus one demo |
| RFQs / quotations | 0 / 0 | 28 / 77 | |
| bookings | 0 | 6 | |
| orders | 0 | 3 | |
| reviews | 0 | 0 | still nothing reviewed — see B4 |
| notifications | 2 | 30 | the "hello / world" test broadcast is gone |
| categories | 31 | 31 | 8 carry stock. Empty ones are left out of category lists (R1-24), but the home screen still shows the Home, Cars, Beauty and Business areas — kept visible by decision on 17 Sep |
| media | 3 | 3 | still effectively no imagery — see B3 |
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, checkout hold 15 minutes, provider accept window
24 hours, super_app_enabled true, maintenance_mode false, customer and
vendor registrations open, online_payments_enabled false.
4. Verified working, end to end¶
These were walked on a device against production, not asserted from code.
16–17 September — iPhone screen sizes, on simulators (R1-46). Codemagic ran the app on an iPhone SE (3rd gen), an iPhone 16 and an iPhone 16 Pro Max — 11 screens each, full-device captures with the status bar, keyboard and Dynamic Island drawn in. Safe areas, the floating navigation, the sticky booking and pay bars, the menu beside the Dynamic Island and the keyboard on sign-in and the address sheet pass on all three. The run found one defect, fixed: the booking screen opened on a fully booked day late in the evening. No Mac or iPhone was needed. This checks layout only; the flows are B5.
16 September — the owners' regression sheet, on Android. A release APK on a fresh install worked the 28-row sheet; 27 rows pass, and the full sheet with evidence is in Revision Round 1 §3.
- Direct booking, end to end on the live catalogue, including a triple-tap on Confirm producing exactly one order, and an offline Confirm that correctly refused to show success and then succeeded on retry — one order, not two.
- Provider declines / nobody accepts / abandoned checkout: all three resolve correctly, the customer is told in Arabic, and the held time slot goes back on sale. The 15-minute checkout sweep was caught being done by production's own scheduler, not by hand.
- Conversations are linked to a booking and its provider (
order_idandvendor_idon the row) — the feature the owners asked for. - The app lock (R1-34), with a simulated fingerprint: turning it on needs a successful scan, a cold launch locks, returning after a minute away re-locks over the private screen, a short switch away doesn't, and "sign out instead" really signs out. The Face ID prompt itself still needs an iPhone.
- Every sort returns what it claims (R1-25), checked against temporary seeded data on production, then removed: price, rating, most booked and newest each gave their own, correct order.
- Six defects were found by this run and fixed, none of which a static check could have caught: a notification claiming an order was confirmed when it was not, the keyboard covering the sign-in button, a header scrolling under the status bar on four screens, two screens drawing their own image fallback, provider chat unreachable at most statuses, and the account showing "Bookings: 0" beside a live order.
16 September — TestFlight launch. A TestFlight build opened to a permanent white screen: the app waited on a push-notification call before drawing anything, and on iOS that call never answered. Fixed so the app draws first; the owners confirmed the next build opens.
27 August.
- 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.
Tests, 25 October: 630 API tests pass (2 408 assertions), and 47 mobile tests.
flutter analyze and the web tsc check are both clean, and analyze is a blocking CI
step.
5. Launch blockers¶
Ranked. Nothing below is a nice-to-have.
B1 — No real payments¶
PAYMENT_DRIVER=myfatoorah, still on the test sandbox (apitest.myfatoorah.com). 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. Online payment is switched off on the
server until this is done, so nothing can be charged by accident.
To close: obtain the merchant account, swap to live keys and base URLs, populate the webhook secret, then turn on Online payments (KNET / card) in Admin → Settings. The app follows that switch; no new mobile build is needed for it.
~~B2 — Test data is live and public~~ — CLOSED 16 September 2026¶
418 rows across 30 tables removed after a database backup and a dry run: the
37 load-test users and their 12 [LT] vendors, both audit accounts, seven QA
leftovers the audit had never named, the two old service-app demo vendors, and
the "hello / world" test broadcast. Eight curated events providers replaced them.
UserPurge derives the whole graph from a set of account ids — about 45 tables,
children first, in one transaction — and both yam3at:load-test --wipe and
yam3at:purge-users run through it, with a per-table count and --dry-run.
Deliberately kept: the four accounts on personal addresses. Removing a real person's account is not a cleanup decision to take unasked.
B3 — No imagery¶
Still 3 media rows platform-wide. Vendor cards, service heroes and galleries all fall back to the brand mark. The app no longer looks broken; it still looks empty — visible on every screen of the iPhone screenshots.
To close: real vendor photography, plus the mascot/brand artwork. Note the launcher icon problem in G4.
B4 — Zero reviews, and rating is the default sort¶
reviews is empty, so every listing shows "جديد / New" and "Top rated" — the default
ordering on the browse screen — has nothing to rank by. The sorts themselves are
correct: all four were verified on a device against temporary seeded data (R1-25).
The demo content deliberately does not invent reviews; fabricated scores for
providers nobody has booked would be fake social proof shown to real customers.
To close: the owners choose a default sort until reviews accumulate. Worth deciding at the same time: "Top rated" ranks one 5★ review above fifty 4.9★ ones — literally what it says, but a weighted rating would be fairer once reviews arrive.
B5 — The regression flows have never run on an iPhone¶
The sheet passes 27 of 28 rows on Android, and iOS layout now passes on three simulator sizes. But the rows are flows — booking, paying, push, Face ID — and none has run on an iPhone. This is what keeps the R1-47 gate open.
To close: run ios-release in Codemagic so TestFlight carries the current code,
then one pass of the sheet on a phone.
6. Known gaps, not blocking¶
| # | Gap | Impact |
|---|---|---|
| G2 | WhatsApp channel is a no-op. No Business API credentials exist; Notifier skips the channel. |
Medium — WhatsApp is the dominant channel in Kuwait |
| G4 | The Android launcher foreground is the full square icon — checked 17 Sep: 432×432, not one transparent pixel — so Android 12+ paints a yellow circle with a square inside it. The adaptive-icon XML is correct; only the asset is wrong. | Low, but it is the first thing every Android user sees |
| G5 | Android release signs with debug keys (buildTypes.release uses the debug signing config). Harmless while Android stays off Play, blocking the moment it doesn't. |
Deferred by decision |
| G8 | Order numbers are sequential (YM-2026-000001, …002) and customer-visible, so they reveal order volume and invite enumeration — against the house rule. |
Medium; next round |
| G9 | About 30 display paths still format money through a float — reports, dashboards, affiliate and AI-quote totals. None feeds a charge; the booking, order, coupon and wallet paths were fixed (R1-08). | Low; next round |
| G10 | Delete account and export data have not been run. Both screens exist; the account under test was the owners' demo account. | Needs one pass on a throwaway account |
Waiting on the owners¶
- The partners' categories draft (مقترح فئات وخدمات منصة يمعات, 17 Sep): 17 event categories and 255 service types, 12 suggested for launch. Decided: nothing changes until the partners pick; service types will become filters inside each category; Home, Cars, Beauty and Business stay visible meanwhile. Compared category by category with the build — 9 of the 17 exist, 3 in part, 5 not at all; the platform has no service-type layer yet.
Raised in the revision round so they are decided, not so they grow it:
- Onboarding prices are in the wrong format — "900 KD" and "2,150 KD" on the third slide, where the rest of the app shows "450.000 د.ك".
- The app still opens by selling the super-app. Onboarding, the home domain row and the AI's quick suggestions offer home and car services, and after the cleanup only events categories have stock, so all of those lead to an empty state: a well-designed one, but empty.
- The AI plans thinly — asked for a party for 80, it suggested catering alone.
- The default sort before reviews exist, and weighted rating — see B4.
- Signing in from the onboarding carousel returns to the carousel instead of into the app; the session is started correctly.
Closed on 27 August¶
- G1 — mixed datetime types.
event_milestones.scheduled_atwas the onlyDATETIMEamong 210 datetime columns. Converted; production reports zerodatetimecolumns. - G3 — scheduling tools under Analytics. Vendor calendar and blocked dates moved into the vendor panel beside pricing and gallery.
- G6 — weak recommendations. Candidates were drawn from every visible vendor; now scoped to the events domain, ranked by the categories that suit the event type.
- G7 — unaudited settings. Settings changes are now audited with before/after values.
7. Recently fixed¶
25 October — a full review of money, access, personal data and operations¶
Four reviews were run against the whole build. Everything below is fixed, tested and deployed; each fix has a test that fails on the code it replaced.
A disclosure, closed. This documentation site was publishing three real customers' personal email addresses, a street address and the orders they belonged to, with no login. Redacted at source, rebuilt, republished, and the superseded copies removed from the server. It remains in git history — removing it there is a separate decision. The production host's IP is no longer printed beside the root SSH commands here either.
Money. None of these could bite today, because online payment is off at the server; all of them become real the day it is switched on.
- A cancelled order could never be refunded. Cancelling returns the wallet
portion and stamps
refunded_at, and the admin refund refused on that stamp alone — so a customer who paid online and whose order was then declined by the vendor had no path to their money. It now refunds what is still outstanding. - A cash order part-paid from wallet credit settled as though the provider had taken the whole amount at the door. One paid entirely from the wallet left them unpaid and billed commission on top. The settlement follows what they collect.
- A replayed success webhook flipped a refunded payment back to paid, counting the money as collected twice. A refund is final.
payout_hold_dayswas a setting nothing read: a settlement was payable the moment the order completed, before the customer could dispute it.- A usage-limited coupon was checked before the transaction and incremented without re-checking. The use is now claimed in one conditional write.
- Paying for a plan grouped its orders with nothing holding them still, so two taps built two groups and two charges. The orders are claimed under a lock.
- A ticket reservation that expired gave its seat back but was still issued on a late callback — two valid QR codes for one seat, with no refund route.
- The slot went back on sale while the customer was still on the payment gateway, so a success callback could confirm them into a time already taken.
Access. Signing in with Google or Apple matched an account by email alone, so someone who registered your address first kept your account when you later signed in with the provider. Suspension stopped only password login — the token already issued stayed valid and OTP or social login minted a fresh one. TOTP was enforced on the password path only. Vendor review notes (free text an admin writes on approval or rejection) and licence numbers were returned to anonymous callers.
Personal data. Deleting an account revoked the tokens and renamed the users row; everything else stayed, including the address snapshot on every order, which the vendor's inbox kept serving. Erasure now removes what existed only to serve that person and scrubs the personal data out of what the platform must keep. The export, which omitted orders and addresses, now answers the question it claims to.
Operations. Production could not write a single line to its error log for five
weeks — and Laravel throws when it cannot, so a request that logged anything
failed outright and the failure went unrecorded. Database dumps were world-readable
on a box shared with five other projects. The health check tested only the
database, so Redis dying left every signed-in request failing while the endpoint
said ok; it now checks both, the deploy polls it and fails loudly, and the docs
stop pointing monitoring at /up. The scheduler drifted past roughly eight
minutes of each day. Docker logs are capped. Both deploy scripts were committed
non-executable, which is why a pre-deploy backup silently failed on 17 September.
Still open, and why. These need a decision or something only the owners have:
| Item | Needs |
|---|---|
| Plan orders don't check slot availability | A product decision: enforce slots at booking, or stop plan orders occupying slots. They currently occupy a slot they never reserved. |
| Civil IDs, IBANs and Google refresh tokens are plaintext | Encrypting them needs a migration over existing rows |
| No consent record at signup; no unsubscribe on campaigns | A product and legal decision |
| Terms, Privacy and Refund pages are live, headed "Draft" | A lawyer |
| No off-site backup | Somewhere to put it |
| No uptime alerting | An external monitor on /api/v1/health |
| Android release builds sign with the debug key | Generating a keystore is a one-way identity decision |
| Order numbers are sequential and customer-visible | Next round (G8) |
| Event guests have no consent, erasure or retention | A product decision |
16–17 September¶
- Browsing a domain had no way to narrow it. Tapping Events listed every hall, caterer, photographer and florist at once, with only a search box and a sort order. The browse screen now carries a row of the domain's own categories, each with how many services sit behind it under the current search, and "All" to come back out. The counts come from the same query as the list, so a chip never opens an empty screen, and a category with nothing behind it is not offered at all. The API side is live; the app side reaches testers with the next mobile build.
16–17 September¶
- Deleting a category in admin silently stripped it from its services, vendors, quote
requests and orders — every link is
nullOnDelete. Admin now refuses while it's in use. - Vendors couldn't sign up into a category with no providers — the web form read the customer list, which hides empty categories. Sign-up now lists every active event category.
- TestFlight opened to a white screen — the app awaited a push call before drawing; now it draws first and every native call at startup has a deadline.
- The app lock never re-locked, and a verified email showed as unverified (R1-34).
- Sorts returned ties in an unstable order, and the filters button didn't reset (R1-25).
- A slow keyboard still covered the Login button — the form stopped waiting after 1.3 seconds; an emulator keyboard took 2.1 to start opening. Found by the iPhone screen-size walk.
- The booking screen opened on a fully booked day in the evening — a greyed-out day, selected, over "no slots available". Found in the iPhone Pro Max screenshot.
- iPhone screen-size checks without a Mac — a Codemagic workflow that walks three simulators and returns device screenshots, hardened over four runs.
27 August¶
Would have blocked real users
- Saved addresses never loaded.
Address.fromJsoncastlatitudewithas num?; MySQL decimals serialize as strings, so no direct booking could be completed by anyone — and every retry silently created a duplicate address. - 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 with
super_app_enabledon, as it is in production. - Availability slots were three hours off for Kuwait.
- Service ratings and booking counts were never computed, while catalog discovery sorted on exactly those columns.
Security — the session token moved from a plaintext cache file to the Android
Keystore / iOS Keychain with a one-time migration; allowBackup disabled; the catalog
no longer lists direct-booking services with no package.
Localisation — all 25+ notification types resolve per reader; vendor pricing advice is bilingual with Arabic plural forms; the network-error fallback behind ~20 screens is localised.
UX — sign-up rebuilt with a real password confirmation and an invite-code field; pending and rejected vendors see their status; unrated listings say "جديد / New".
8. Roadmap¶
Now — close the launch blockers¶
- Live payment merchant account; live keys, base URLs and webhook secret; then turn on online payments in Admin → Settings (B1).
- Vendor photography and brand artwork, including a correct adaptive-icon foreground (B3, G4).
- Decide the default sort before reviews exist (B4).
- Run
ios-release, then the regression sheet on an iPhone (B5). - The owners' decisions listed under "Waiting on the owners".
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.
- Non-sequential order numbers and the remaining float money formatting (G8, G9); delete-account and export run on a throwaway account (G10).
- Re-check recommendation quality once the catalogue is real.
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@<production-host> 'cd /opt/yam3at && git rev-parse --short HEAD && \
docker compose -f docker-compose.prod.yml ps'
# data snapshot
ssh root@<production-host> 'cd /opt/yam3at && docker compose -f docker-compose.prod.yml \
exec -T api php artisan tinker --execute="echo DB::table(\"users\")->count();"'
# test suites
cd apps/api && php artisan test
cd apps/mobile && flutter test && flutter analyze
# iPhone screen sizes: start "Yam3at iOS screen sizes (simulators)" in Codemagic
Deployment procedure and rollback: DEPLOYMENT.md in the repository root, 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.