Skip to content

Run the stack locally

By the end you’ll have a portal → backend → middleware → crm-mock round-trip running locally, with the SPA pointed at your local backend. You need 5 of the 11 repos: backend, crms-middleware, frontend, backend-pdf-service (only for sales-offer PDFs), and crm-mock (stands in for Dynamics + the Logic Apps).

Docker + Docker Compose; Node ≥ 20 + Yarn 1 (frontend) / npm (pdf-service). Each dev compose publishes on a deliberately non-clashing host port (DBs are off 3306):

Service Host port
backend nginx 8080
backend MySQL / Mailhog / Redis / MinIO 33006 / 8025 / 63790 / 9000–9001
crms-middleware nginx / MySQL 8081 / 33007
crm-mock nginx 8089
backend-pdf-service 3001
frontend (Nx: shopper / broker) 4200 / 4201

The SPAs talk only to backend (:8080); backendcrms-middleware (:8081) → crm-mock (:8089); backendbackend-pdf-service (:3001) for PDFs.

  1. crm-mock first (so the CRM endpoints exist) — Laravel 12 / SQLite:

    Terminal window
    cp .env.example .env
    composer install && php artisan key:generate
    touch database/database.sqlite && php artisan migrate
    composer run dev # or: docker-compose -f docker/docker-compose-dev.yml --env-file .env up -d

    Its oauth2/token just returns a literal fake-token — no real Azure needed. nginx on :8089.

  2. crms-middleware:

    Terminal window
    cp .env.example .env
    docker-compose -f docker/docker-compose-dev.yml --env-file .env up -d # note .yml (README says .yaml — typo)
    docker-compose exec app composer install
    docker-compose exec app php artisan migrate && docker-compose exec app php artisan db:seed
    docker-compose exec app php artisan passport:keys # passport:keys, NOT passport:install

    Then point its CRM config at the mock and create the OAuth client (next two sections).

  3. backend:

    Terminal window
    cp .env.example .env
    docker-compose -f docker/docker-compose-dev.yml --env-file .env up -d
    docker-compose exec app composer install
    docker-compose exec app php artisan key:generate
    docker-compose exec app php artisan migrate
    docker-compose exec app php artisan db:seed
    docker-compose exec app php artisan passport:keys

    nginx on :8080. Set CRMS_MIDDLEWARE_{BASE_URL,AUTH_URL,API_URL}http://host.docker.internal:8081/….

  4. backend-pdf-service (only for PDFs): npm install && npm run dev — on :3001.

  5. frontend: cp .env.example .envedit the endpoints (below)yarn installyarn start (shopper :4200) / yarn start:broker (:4201) / …

frontend/.env.example ships every endpoint as the placeholder https://api.example.com/api — forget to change it and the SPA compiles but every request 404s. Set the API bases (note the double underscore) NX__API_ENDPOINT, NX__BROKER_API_ENDPOINT, NX__SALESMAN_API_ENDPOINT, NX__SHOPPER_ANALYTICS_API_ENDPOINT, NX__PRE_DELIVERY_API_ENDPOINThttp://localhost:8080/api. The per-persona path prefix is separate: NX__BROKER_ENDPOINT_PREFIX=/broker, NX__SHOPPER_ENDPOINT_PREFIX=/shopper, NX__SALESMAN_ENDPOINT_PREFIX=/sales-man, etc.

All the CRM vars are empty in crms-middleware/.env.example, so this is a manual step:

  • {EGYPT,OMAN,MONTENEGRO}_CRM_BASE_URLhttp://host.docker.internal:8089
  • {EGYPT,OMAN,MONTENEGRO}_CRM_AUTH_URLhttp://host.docker.internal:8089/api/oauth2/token
  • Oman broker-CRM mock: OMAN_CRM_BROKER_CRM_MOCK_ENABLED=true + OMAN_CRM_BROKER_CRM_MOCK_BASE_URL=http://host.docker.internal:8089 (flips the Oman pre-reg clients to the mock via swapOmanBrokerCrmToMockIfEnabled).

Sanctum (end-user tokens) needs nothing beyond migrate. Passport (the client_credentials legs) needs keys + two clients:

  1. On middleware: passport:client --client → name it portals-backend. Copy its id/secret into backend CRMS_MIDDLEWARE_CLIENT_ID / CRMS_MIDDLEWARE_CLIENT_SECRET.
  2. On backend: passport:client --client → a crms-middleware client (used by the middleware to call the backend’s /api/crms-middleware/* webhooks).

The scopes (middleware-webhook / portals-api / crm-webhook) are applied at the token endpoint, not pinned on the client — so plain client_credentials clients are enough. See the auth model.

Queues — you don’t need a worker locally

Section titled “Queues — you don’t need a worker locally”
  1. Services up: curl http://localhost:8080 · :8081 · :8089/api/oauth2/token (→ {"access_token":"fake-token",…}) · :3001 (pdf has a /health probe).
  2. Backend works end-to-end: curl http://localhost:8080/api/currencies (a public route) should return JSON — proving nginx → PHP → MySQL.
  3. A CRM round-trip through the mock: exercise a mocked path — a units-listing read or the Oman broker pre-registration register — and confirm the middleware authenticates (fake-token) and returns a mapped body. Don’t use a lead submit (the mock has no lead-write endpoint).
  • Networking isn’t codified — use host.docker.internal. Each dev compose uses its own bridge network (orascom-dev, crms-middleware-dev, crm-mock-dev, orascom-pdf) and the declared external network names don’t line up across repos. Reach every service by http://host.docker.internal:<port>. On Linux add --add-host=host.docker.internal:host-gateway.
  • PDF_SERVICE_URL drift: .env.example says localhost:3001, CLAUDE.md says pdf-service:3001. From a dockerised backend use http://host.docker.internal:3001.
  • Port 3001 clashes with other local stacks — override PORT. (DB ports are safe at 33006/33007.)
  • CURRENY_LAYER is a deliberate typo — don’t “fix” it; renaming breaks every populated env.
  • Docs drift: both Laravel READMEs say Laravel 9 / PHP 8.1, but the backend is Laravel 10 / PHP 8.2 (composer.json wins). The middleware README compose path says .yaml; the file is .yml.