Skip to content

Known defects & findings register

Finding Where Impact
Paymob webhook HMAC verification is entirely commented out — the callback accepts any body and trusts obj.success with no signature check app/Services/PaymobService.php:146-152 (guard call), :153 (trust) Critical — forged payment-success webhooks are accepted
Even the dead HMAC code uses == not hash_equals (non-constant-time) app/Services/PaymobService.php:210 Timing side-channel (moot while disabled, wrong if re-enabled)
No replay / idempotency guard on payment callbacks — a replayed success re-marks the transaction, re-submits to the CRM, and re-fires notifications app/Services/PaymentService.php:36-73 High — duplicate CRM writes / notifications
Payment callback routes are public / unauthenticated routes/api.php:31-38 High — combined with the above
check-whitelisted-emails is an unauthenticated GET that leaks whitelist membership and the brokerage source_id app/Http/Controllers/Api/Broker/UserController.php:116-121, routes/api/broker.php:72 Medium — enumeration / info leak
OTP throttles are keyed by the phone field value, not IP — rotating the phone value bypasses the bucket shopper OTP routes (routes/api/shopper.php) Medium — OTP brute-force / spam
EOI details GET forwards the raw {eoiSourceId} to the CRM with no local existence check (not a route-model binding) routes/api/shopper.php (EOI details) Low/Medium — CRM probing
Thawani secret can be sourced from a DB column payment_api_secret_key; Paymob integration id written to the debug log app/Services/ThawaniService.php:25, PaymobService.php:100 Low — secret handling
Finding Where Impact
getSalesOfferPdf route points at a controller method that does not exist — dead route, 500 on hit routes/api/broker.php:132 and routes/api/sales-man.php:79 (Base\SalesOfferPdfController has no such method) Broken endpoint (superseded by the async generate/status/download trio)
Sales-man logout resolves to a non-existent class (Api\SalesMan\Auth\MicrosoftController) — 500 “Target class does not exist”; the real class is App\Http\Controllers\SalesMan\Auth\MicrosoftController@logout routes/api/sales-man.php:22 Broken logout (SSO login works via routes/web.php)
Broker GET documents returns 4 hardcoded rows with document: "#" app/Http/Controllers/Api/Broker/UserController.php:24-52 Mock in production surface
Shopper GET user/referrals (“my referrals”) returns hardcoded mock data behind a // TODO: call CRM app/Http/Controllers/.../ReferralController.php:12-51 Mock in production surface
UnitClient::createResale() reads config('services.crms.unit.create_resale'), a key not declared in the config block → resolves to null crms-middleware/app/Clients/Crms/UnitClient.php:61 vs config/services.php:177-186 Resale-create silently broken
PreDeliveryUnitSaleResource and the UnitAddonPackageType enum are defined but referenced nowhere in app code app/Http/Resources/PreDeliveryUnitSaleResource.php; app/Enums/UnitAddonPackageType.php Dead code
Taskeen’s Broker code branch is dead (Taskeen is sales-man-only) app/Actions/Taskeen/GetTaskeenList.php:25-27 Dead code
Finding Where Impact
Shopper auth is Sanctum, not Passport (as backend/CLAUDE.md claims); config/auth.php sets the api guard driver to passport but routes gate on auth:sanctum app/Models/Shopper.php:11,16; config/auth.php Documentation + config mismatch
backend/CLAUDE.md §9 references app/Services/Payment/*, a directory that does not exist (payment code is flat under app/Services/) backend/CLAUDE.md:§9 Stale doc — misdirects searches
Frontend declares a @orascom/api tsconfig path alias pointing at libs/api/src/index.ts that does not exist frontend/tsconfig.base.json Dangling alias
PDF service fails closed on auth (unset API_TOKEN → 401 for all requests) even though its README/CLAUDE claim “no token ⇒ open” backend-pdf-service/src/routes/sales-offer.ts:9-12 Doc contradicts code
middleware-package config file is git-ignored / absent, so its provider’s config() reads resolve to null — it would fail to boot as checked out; its provider also auto-registers routes + webhook ingress against its own CLAUDE rules middleware-package/ (.gitignore last line; CrmIntegrationServiceProvider.php:48) Library unusable as-is (it is also unused: 0 imports platform-wide)
crm-mock PROJECT-OVERVIEW.md documents a POST /api/oman/brokers/documents route that is not in routes/api.php crm-mock/ Stale doc
PDF_SERVICE_URL default is http://localhost:3001 in config but docs cite the Docker host http://pdf-service:3001 backend/config/services.php:69 Minor drift
Finding Where Impact
There is no separate Payload CMS — all content is served by the Laravel Voyager admin over the same DB (the handover note “Payload CMS” is a mislabel) grep across repos (0 payload.config, 0 dep) Corrects a false architectural assumption
Firebase is auth-only, not FCM pushKreait Firebase Auth verifies shopper tokens; there is no CloudMessage/device-token/push-send code anywhere app/Services/FirebaseAuthService.php:16; shopper LoginController.php:25 “Push notifications” don’t exist server-side
OpenTelemetry is not wired (no open-telemetry/* / @opentelemetry/* deps) — Sentry tracing covers the need composer.json / package.json Corrects an assumed integration
Pusher is config-scaffold only, not wired — no pusher-php-server dep, no ShouldBroadcast, default driver null/log, no frontend pusher-js config/broadcasting.php Broadcasting is inert
Egypt is hardcoded in several write paths: broker register and EOI submit (Country::where('slug','egypt')), add-on payment (Egypt-only), and Taskeen (Egypt-only CRM URL) UserController.php:74; AddonPayment.php:19; Taskeen middleware Non-Egypt markets silently unsupported on these paths
Currency-Layer is wired with a deliberate CURRENY_LAYER typo “workaround”; the frontend NX_CURRENY_LAYER_* keys are vestigial (unused) config/currency.php; frontend env Confusing but functional
Finding Where Impact
Precedence bug: if (! $response->getStatusCode() == 200)! binds tighter than ==, so the guard is never true and the intended exception can’t fire app/Services/PaymobService.php:69, ThawaniService.php:52 Error path unreachable
Resell-assistance reads $requestData['service_slug'], a key the FormRequest never collects → PHP 8.2 warning + service_type = null on every unit_resale row app/Http/Controllers/.../CustomerRequestController.php:102 Data always null
A referral is classified referral locally off portal_page, but as is_referral in the CRM off the referral_* fields — the two conditions can disagree app/Http/Controllers/.../LeadController.php:48-51 vs ShopperSubmitNewLead.php:41 Inconsistent classification
The unit hold is placed at create-sale (DRAFT), not at the reserve click, and share-reservation-link creates no hold — there is no local reservation TTL (expiry is CRM-owned) app/Actions/Sale/CreateSaleCrm.php:35; ReserveSaleCrm.php:31 Holds behave unintuitively
create_unit_type and mark_pipeline_as_lost have no egypt entry in the per-country validator map — a valid egypt header still 422s on those actions app/Http/Requests/SalesMan/Country/CountryRequestValidator.php:44-61 Egypt agents blocked on 2 actions
finish-sale is a PATCH that force-sets status = DONE with no FormRequest / body validation on the state transition routes/api/shopper.php (finish-sale) Unvalidated state change
Infobip non-2xx errors are swallowed; SMS silently no-ops when ALLOW_SEND_SMS=false even though the OTP row is stored/verifiable app/Services/SmsService.php; app/Notifications/SmsNotification.php Silent delivery failures
OTP code space is only ~10k with two fixed leading zeros (e.g. 009999): str_pad((string) random_int(0, 9999), 6, '0', ...) app/Actions/Otp/SendOtp.php:16-18 Weak OTP entropy
The commission-analytics enum case CACHED is a misspelling of “cashed” (= paid) app/Enums/BrokerCommissionAnalyticsStatus.php Cosmetic but confusing
handlePaymobResponseCallback (serves all Paymob callbacks — sale / add-on / EOI) contains a hardcoded sleep(10) app/Services/PaymentService.php:102 Blocks the request 10s