Skip to content

The CRM mock

crm-mock is a small Laravel 12 / PHP ^8.2 application whose only job is to pretend to be the real CRMs during local development and testing. In production the platform talks to three Microsoft Dynamics 365 orgs (Egypt, Oman, Montenegro) and their Azure Logic App write endpoints through the crms-middleware service — see the CRM integration reference for how that real stack is wired. crm-mock slots in underneath the middleware, in place of those vendor systems, so you can exercise the whole chain without a VPN, real credentials, or a live Dynamics tenant.

A lightweight Laravel 12 app that returns canned responses shaped like the real CRMs, so crms-middleware’s mapper layer sees the same payload structure it would get from Dynamics / the Logic Apps. That shape is the contract — when a vendor changes their payload, the mock is meant to change alongside the middleware mappers (crm-mock/CLAUDE.md sections 1 and 3).

Where it sits in the request chain (from crm-mock/PROJECT-OVERVIEW.md):

backend ──HTTP──▶ crms-middleware ──HTTP──▶ crm-mock (forward: reads + writes)
▲ │
└──────HTTP──────────────┘ (reverse: simulated CRM webhook, optional)
crm-mock ──▶ crms-middleware /api/crms/*

Stack, verified against the repo:

Layer Technology
Language PHP ^8.2 (crm-mock/composer.json:9)
Framework Laravel ^12.0 (crm-mock/composer.json:10) — newer than backend (10) and crms-middleware (9)
Database SQLite by default (DB_CONNECTION=sqlite, crm-mock/.env.example) — no MySQL needed
HTTP client Laravel Illuminate\Support\Facades\Http (for the reverse webhook)
Tests PHPUnit ^11.5 (crm-mock/composer.json)
Assets Vite (crm-mock/vite.config.js)
Container crm-mock/docker/docker-compose-dev.yml

The complete mocked surface is crm-mock/routes/api.php. All routes are registered under the /api prefix (crm-mock/bootstrap/app.php), so the full paths are:

Method + path Handler What it returns
POST /api/oauth2/token inline closure (routes/api.php) access_token: "fake-token", token_type: "Bearer", expires_in: 3599 — for any body
POST /api/listing ListingController@handle A fixture file of pipelines or units, chosen by country + entityschemaname (bulk inventory read)
POST /api/unit/payment-terms UnitPaymentTermsController@handle A fixture of payment plans, chosen by Unit.country
POST /api/oman/brokers/register Oman\BrokerRegisterController@handle success: true plus a generated crm_broker_id; optionally fires the reverse webhook
GET /api/oman/brokers/{crmBrokerId} Crm\BrokerGetController@show The Oman broker fixture with the requested crmBrokerId injected

There is also a framework health check at GET /up (crm-mock/bootstrap/app.php).

POST /api/oauth2/token returns the same object regardless of input:

{ "access_token": "fake-token", "token_type": "Bearer", "expires_in": 3599, "ext_expires_in": 3599 }

Any client_id / client_secret you configure in the middleware works — the mock never validates them.

ListingController@handle reads the request’s Listing object and routes on two fields (crm-mock/app/Http/Controllers/ListingController.php):

  • country1 = Egypt, 2 = Oman, 3 = Montenegro
  • entityschemanameldv_interest / new_unit for Egypt and Oman; ldv_pipeline / ldv_unit for Montenegro

The pair selects a fixture from crm-mock/storage/fixtures/<Country>/ (pipelines.json or units.json). An unrecognised combination returns an empty array [].

UnitPaymentTermsController@handle reads Unit.country and returns crm-mock/storage/fixtures/<Country>/payment-plans.json for Egypt, Oman, or Montenegro. If the country is unknown or the fixture is missing it returns PaymentPlans: [] (crm-mock/app/Http/Controllers/UnitPaymentTermsController.php).

Two endpoints back the Oman broker flow (see also Oman broker pre-registration):

  • POST /api/oman/brokers/register — builds a broker id as CRM_ + a sanitised slug of the posted email or phone, logs it, and returns success, crm_broker_id, and a message. If the reverse webhook env vars are set (below) it then calls back into crms-middleware (crm-mock/app/Http/Controllers/Oman/BrokerRegisterController.php).
  • GET /api/oman/brokers/{crmBrokerId} — loads crm-mock/storage/fixtures/Oman/crm-broker-get.json, overwrites data.crm_broker_id with the id from the URL, and rewrites every data.documents[].document_url to the local public/sample.pdf via asset('sample.pdf') (crm-mock/app/Http/Controllers/Crm/BrokerGetController.php).

The middleware decides per operation whether it hits the real CRM or the mock, purely through environment variables — the mock never has to know it is being used. There are two switching styles.

1. Point the CRM URLs at the mock (URL swap)

Section titled “1. Point the CRM URLs at the mock (URL swap)”

crms-middleware/config/services.php builds each country’s CRM config from env keys. Point the *_AUTH_URL and *_BASE_URL (and specific per-operation URLs) at the mock instead of Dynamics. In crms-middleware/.env:

Terminal window
# Egypt org → mock
EGYPT_CRM_AUTH_URL=http://host.docker.internal:8089/api/oauth2/token
EGYPT_CRM_BASE_URL=http://host.docker.internal:8089/api
# Oman org → mock
OMAN_CRM_AUTH_URL=http://host.docker.internal:8089/api/oauth2/token
OMAN_CRM_BASE_URL=http://host.docker.internal:8089/api
# Unit payment terms operation → mock
UNIT_PAYMENT_TERMS_CRM_URL=http://host.docker.internal:8089/api/unit/payment-terms
# credentials can be any value — the mock returns "fake-token" regardless
EGYPT_CRM_CLIENT_ID=anything
EGYPT_CRM_CLIENT_SECRET=anything

The relevant middleware keys are EGYPT_CRM_*, OMAN_CRM_*, and MONTENEGRO_CRM_* (auth_url / base_url / client_id / client_secret / scope), plus operation URLs such as UNIT_PAYMENT_TERMS_CRM_URL (crms-middleware/config/services.php, crms-middleware/.env.example). Switching back to the real CRM is just restoring the real Dynamics / Logic-App URLs.

2. Use the middleware’s built-in mock toggle (Oman broker)

Section titled “2. Use the middleware’s built-in mock toggle (Oman broker)”

For the Oman broker flow the middleware ships a first-class mock switch — no need to overwrite the real Oman CRM config. In crms-middleware/.env:

Terminal window
OMAN_CRM_BROKER_CRM_MOCK_ENABLED=true
OMAN_CRM_BROKER_CRM_MOCK_BASE_URL=http://host.docker.internal:8089

The middleware reads crms.oman.broker_crm_mock.enabled / .base_url (crms-middleware/config/services.php) and, when enabled, routes the broker register/get calls to the mock. A sibling toggle CRM_LEAD_ELIGIBLE_CRM_MOCK_ENABLED follows the same pattern for the lead-eligibility check. This is the cleanest way to flip one flow to the mock while everything else still points at real (or other-mock) CRMs.

3. Reverse direction — mock pushes a webhook into the middleware (optional)

Section titled “3. Reverse direction — mock pushes a webhook into the middleware (optional)”

To test the inbound path (CRM → middleware → backend) without a real CRM trigger, set these in crm-mock/.env (crm-mock/.env.example):

Terminal window
CRMS_MIDDLEWARE_BASE_URL=http://host.docker.internal:8000
CRMS_MIDDLEWARE_CRM_WEBHOOK_CLIENT_ID=<middleware crm client id>
CRMS_MIDDLEWARE_CRM_WEBHOOK_CLIENT_SECRET=<middleware crm client secret>

When all three are present, BrokerRegisterController authenticates against ${CRMS_MIDDLEWARE_BASE_URL}/api/crms/login, then POSTs a broker payload to ${CRMS_MIDDLEWARE_BASE_URL}/api/crms/brokers/register (crm-mock/app/Http/Controllers/Oman/BrokerRegisterController.php). If any of the three is missing the webhook is silently skipped and only the normal registration response is returned.

Standard Laravel setup (crm-mock/CLAUDE.md section 3, crm-mock/PROJECT-OVERVIEW.md section 6):

Terminal window
composer install
cp .env.example .env
php artisan key:generate
touch database/database.sqlite # SQLite default DB
php artisan migrate
# all-in-one: HTTP server + queue worker + log tail (pail) + vite
composer run dev
# or just the HTTP server
php artisan serve # defaults to http://localhost:8000
php artisan serve --port=8001 # pick a non-default port if 8000 is taken

Docker option (crm-mock/docker/docker-compose-dev.yml): the nginx service publishes 8089:80, so the app is reachable at http://localhost:8089, and at http://host.docker.internal:8089 (or the container name crm-mock-host-dev) from other containers on the shared orascom-shared Docker network. Match whichever port you use to the *_BASE_URL values above.

Useful checks:

Terminal window
php artisan route:list --path=api # list the mocked endpoints
php artisan test # PHPUnit 11
  • Partial CRM coverage. The mock covers Egypt-style listing + payment terms and the Oman broker register/get flow — plus Montenegro fixtures for listing and payment plans. It does not mock the SalesMan or Link Dev surfaces, EOI, sale reservation, or most Logic-App writes. If you need those locally, you extend this app (crm-mock/CLAUDE.md section 4, KT-QUESTIONS.md).
  • Auth is fake. oauth2/token always returns fake-token with a fixed expires_in. Any flow that depends on specific JWT claims, scopes, or token expiry will silently not be exercised (crm-mock/KT-QUESTIONS.md Q6).
  • Static fixtures, no state machine. Responses are canned JSON files. The broker “get” endpoint just echoes whatever crmBrokerId you pass back into a single fixture and points every document at sample.pdf; it does not model a real register → upload → status progression (crm-mock/KT-QUESTIONS.md Q9).
  • No filtering / pagination. Listing routes only on country + entityschemaname; any paging or filter params the real CRMs honour are ignored (crm-mock/KT-QUESTIONS.md Q7).
  • PROJECT-OVERVIEW.md is slightly ahead of the routes. It lists a POST /api/oman/brokers/documents endpoint (BrokerDocumentUploadController) that is not in the current crm-mock/routes/api.php; the live route instead is GET /api/oman/brokers/{crmBrokerId}. Trust routes/api.php.
  • Drift is expected. Mappings and edge cases diverge from the real CRMs over time. Do not treat the mock as a spec for how Dynamics behaves — the middleware mappers (crms-middleware/app/Mapper/<Vendor>/...) are the source of truth (crm-mock/CLAUDE.md section 5).
  • Not for CI unless you wire it up. It is primarily a human-driven local dev aid; whether CI spins it up alongside the middleware is an open KT question (crm-mock/KT-QUESTIONS.md Q5, Q11).