COUNTERBEE SERVER — complete ============================ Built FROM the php.zip you just sent. This is your tree with two stale files replaced and the missing pieces added. Nothing else was touched. Copy the whole thing over controllers/php/ — or just the four items listed under WHAT CHANGED, if you prefer a minimal touch. ================================================================ WHAT WAS MISSING ================================================================ Your Entitlement.class.php and Sync.class.php were already IDENTICAL to my final versions, and ocr_commit.php was correctly deleted. Good. Three real gaps: 1. Vision.class.php was STALE — job sheet only. No readQuotation, no shared call(), no gemini_model_text row. So the WhatsApp text parsing had nothing to run. 2. AiCredits.class.php was STALE — no checkText, no TEXT_PLANS. No way to meter a text parse. 3. ai_parse_order.php was ABSENT ENTIRELY. The paste sheet calls /mobile/ai_parse_order.php, so it was getting a 404. The Dart catches that and falls back to the local parser, which is why nothing crashed — but the AI parsing was completely inert. 4. TWELVE MIGRATIONS WERE MISSING: 025 to 036. You had 002-023 only. ================================================================ WHAT CHANGED ================================================================ REPLACED classes/Vision.class.php + readQuotation, shared call() REPLACED classes/AiCredits.class.php + checkText, recordText, TEXT_PLANS ADDED mobile/ai_parse_order.php text in, structure out ADDED mobile/migration_025 ... 036 twelve files UNTOUCHED: all 78 of your other PHP files, byte for byte. ================================================================ THE MIGRATIONS ARE THE URGENT PART ================================================================ Your Entitlement and Sync classes are deployed and correct, but migration 033 creates the business_entitlements table they read from. So Entitlement::require() is currently querying a table that does not exist, and it will throw on any scan or parse attempt. Against your 21 September dump, ALL of these are absent: business_entitlements platform_settings sale_events stock_batches ocr_usage That dump is over a week old, so CHECK YOUR LIVE DATABASE rather than trusting it: SHOW TABLES LIKE 'business_entitlements'; SHOW TABLES LIKE 'platform_settings'; SHOW TABLES LIKE 'sale_events'; SHOW COLUMNS FROM inventory LIKE 'stock_as_of'; RUN ORDER — and THERE IS NO 024: 025 026 027 028 029 030 031 032 033 034 035 036 Every one is idempotent, so a second run is harmless if you have already applied some. BACK UP FIRST: mysqldump -u user -p dbname > backup-$(date +%F).sql mysql -u user -p -e "CREATE DATABASE dbname_test" mysql -u user -p dbname_test < backup-$(date +%F).sql # then run each migration against dbname_test before the real one I previously told you migration_024_hourly_duration exists and is not idempotent. IT DOES NOT EXIST. I invented it and repeated it across several documents. Gaps in a migration sequence are fine. ================================================================ AFTER THE MIGRATIONS ================================================================ 1. The API key. Nothing AI works until this is set, deliberately — an empty key fails closed rather than erroring on every request. UPDATE platform_settings SET value='YOUR_KEY' WHERE key_name='gemini_api_key'; SELECT key_name, value FROM platform_settings WHERE key_name LIKE 'gemini%'; -- gemini_api_key -- gemini_model gemini-2.5-flash-lite -- gemini_model_text gemini-2.5-flash-lite 2. Enrol yourself as platform admin. Until you do, EVERY admin endpoint denies everyone including you — which is deliberate, but it does mean nothing in the app can do this for you. SELECT LOWER(HEX(id)), email FROM users WHERE email = 'you@counterbee.app'; INSERT INTO platform_admins (user_id, email, level, created_at) VALUES (UNHEX('your32charhexid'), 'you@counterbee.app', 'owner', UNIX_TIMESTAMP()); 3. Put a merchant on growth or business. register_scan is on the growth tier and above; free has no AI at all. 4. VERIFY THE TRIGGERS. Migration 026 creates two on sale_events, and your append-only audit log depends on them. Some shared hosts refuse to create triggers without SUPER privileges: SHOW TRIGGERS LIKE 'sale_events'; -- expect 2 If that returns nothing, stop and fix the host. This is the one check that disqualifies a server regardless of price. ================================================================ LEAVE THESE OFF UNTIL CONFIGURED ================================================================ batch_tracking_enabled until batch consumption is tested on a device notify_* until Firebase is configured campaigns until platform_settings holds real Brevo / Africa's Talking credentials All default to off. A wrong number that looks authoritative is worse than a missing feature. ================================================================ STILL NEVER RUN — BOTH GEMINI PATHS ================================================================ Everything AROUND the model calls is tested against your real samples: the sanitisers, both checksums, ordinal dates, year inheritance, arithmetic evaluation, the totals-row filter, and every field name across client and server. The HTTP calls themselves are reasoned, not proven, because Google's endpoint is unreachable from where I work. Test in this order — cheapest failure first: 1. Paste your Hurayrah quotation into the app. Expect a GREEN checksum card, the customer tile offering "Hurayrah Salihu", and caution 10,000 landing in the DEPOSIT field at checkout rather than in the sale total. 2. Paste sample 2 (the bare item list). Expect AMBER — no totals to check against. Confirm it does NOT read as green. 3. Paste a two-word greeting. Expect "No order found", and confirm nothing was billed: SELECT * FROM ai_text_usage; 4. Only then photograph the job sheet. Reading handwriting is the harder job; if the text path is wrong, fix that first. ================================================================ BEFORE A CLIENT ================================================================ · baseUrl is still http:// on a LAN IP. Tokens and customer phone numbers cross the network in clear text. · The Paystack live key from that database dump is still not rotated. · business_settings push is column-scoped rather than key-scoped, so a client could push paystack_secret_key. · branch_services does not sync at all. · Local SQLite is unencrypted.