Skip to content

Oman broker pre-registration

Every other broker self-registers against an email whitelist and logs straight in. Oman is different. An Oman broker goes through a bespoke, multi-step pre-registration that an ODH admin reviews in Voyager before the applicant ever becomes a real Broker. This page traces that whole journey. Everything here is verified against the current backend (develop @ 8434e475).

The whole flow is a state machine on one OmanPreRegistrationBrokerApplication row. Its status enum has 12 values (OmanBrokerApplicationStatusEnums.php:11-22); the onboarding path is the spine below, and the branch splits on broker_type (individual ambassador vs company).

stateDiagram-v2
[*] --> PENDING_INFO: token minted (CreateApplication)
PENDING_INFO --> PENDING_DOCUMENTS: ambassador-info (individual)
PENDING_INFO --> PENDING_COMPANY_DETAILS: company-info (company)
PENDING_COMPANY_DETAILS --> PENDING_DOCUMENTS: company-details
PENDING_DOCUMENTS --> PENDING_REVIEW: finalize (draft to submitted)
PENDING_REVIEW --> MISSING_INFORMATION: admin missing-info
MISSING_INFORMATION --> PENDING_REVIEW: broker resubmit
PENDING_REVIEW --> REJECTED: admin reject
REJECTED --> PENDING_INFO: start again (row deleted)
PENDING_REVIEW --> ACTIVE: admin approve (individual) then CRM job
PENDING_REVIEW --> CONTRACT_SENT: admin approve (company)
CONTRACT_SENT --> CONTRACT_REVIEW: submit-contract (both signed)
CONTRACT_REVIEW --> ACTIVE: admin approve-contract then CRM job
ACTIVE --> [*]: CRM callback mints the real Broker
Oman pre-registration — application status machine (verified)

Getting in: OTP → the pre-registration token

Section titled “Getting in: OTP → the pre-registration token”

The applicant starts with an email; there is no password.

  1. Start. POST oman-pre-registration-applications/start (PreRegistrationBrokerController@start, routes/api/broker.php:39) sends a pre_registration_broker OTP by email. If a real Broker already exists for that email, start silently returns and sends nothing (anti-enumeration; it logs “Pre-registration skipped already existing an (actual) broker”, StartPreRegistrationBroker.php:18-26). That is the usual cause of a “I never got the OTP” report from an email that’s already onboarded.

  2. Verify the OTP → get the token. POST otp/verify (verifyOtp, PreRegistrationBrokerController.php:70-90) checks and deletes the OTP, then GeneratePreRegistrationBrokerToken (app/Actions/OmanPreRegistrationBroker/GeneratePreRegistrationBrokerToken.php:21-54) row-locks the applicant, stamps email_verified_at, creates the application if missing (seeding status = PENDING_INFO), deletes any prior tokens so only one is ever active (:43), and mints createToken('pre-registration-broker', ['access-pre-registration-broker-api']). The response is { public_id, token, token_type: 'Bearer' }.

  3. Everything after is token-gated. The multi-step endpoints live under auth:sanctum + ability:access-pre-registration-broker-api (routes/api/broker.php:50-53) — me, ambassador-info, company-info, company-details, finalize, resubmit, submit-contract, and the documents routes. A full broker token gets 403 here — it authenticates fine but lacks the ability, so the ability: guard rejects it (MissingAbilityException); a missing/invalid token is the 401 case.

Filling the form: the draft steps (branch by broker_type)

Section titled “Filling the form: the draft steps (branch by broker_type)”

CreateApplication seeds PENDING_INFO at token mint. From there the path forks:

One step: PATCH ambassador-info (updateAmbassadorInfo, PreRegistrationApplicationController.php:44-48) fills the applicant’s details, syncs the chosen Oman sales consultants, and moves the application to PENDING_DOCUMENTS.

Then, while still in draft, the applicant uploads documents: POST documents (PreRegistrationDocumentController@store, :25-50), each stored document_status = PENDING. Document types are enumerated in OmanBrokerApplicationDocumentTypeEnums (national ID front/back, passport, brokerage license, bank-details certificate; for companies: POA, company-registration docs, and the Salalah-Beach + Jebel-Sifah signed contracts).

Once PENDING_REVIEW, the application is an ODH admin’s job. The admin actions live under routes/web/dashboard.php behind admin.user middleware; the controller extends VoyagerBaseController, so Voyager session + per-BREAD permissions apply. The dashboard list filters to the submitted statuses (getValidStatusForDashboard, app/Http/Controllers/Dashboard/OmanPreRegistrationBrokerApplicationController.php:31-58).

Admin action Effect Action class
approve Branches by broker type (see below) MarkApplicationAsApproved
missing-info MISSING_INFORMATION + admin_comment; notifies the broker MarkApplicationAsMissingInfo
reject REJECTED + rejection_reason + admin_comment; notifies the broker MarkApplicationAsRejected
approve-contract / reject-contract Company contract review (sets contract dates + dispatches the CRM job on approve) ApproveApplicationContract / RejectApplicationContract
approve/reject-renewal, approve/reject-documents-after-review The renewal + post-approval document cycles (respective action classes)
per-doc status / bulk-accept / download Accept/reject/expire individual documents OmanPreRegistrationBrokerDocumentController

If the admin asks for changes, the broker responds with PATCH resubmit — valid only from MISSING_INFORMATION (:160-171) — which returns the application to PENDING_REVIEW.

How a pre-registration becomes a real Broker

Section titled “How a pre-registration becomes a real Broker”

This is the part that surprises people: approval doesn’t create the broker — a CRM round-trip does, and it takes two writes to two different id columns.

sequenceDiagram
participant AD as ODH admin (Voyager)
participant APP as Application row
participant JOB as RegisterOmanPreRegistrationBrokerInCrmJob
participant MW as crms-middleware → CRM
participant CB as Portal callback (CreateUser)
AD->>APP: approve (individual) OR approve-contract (company)
Note over APP: dispatch the CRM job
JOB->>MW: POST /oman-pre-registration/brokers (country: oman)
MW-->>JOB: 200 { crm_broker_id }
JOB->>APP: broker_source_id = crm_broker_id, status = ACTIVE
Note over MW,CB: CRM registers the broker, then calls back
MW->>CB: POST /api/crms-middleware/brokers/register (Passport client_credentials)
CB->>APP: create real Broker, link application.broker_id, send reset-password email
Note over APP: ACTIVE + broker_id set = fully onboarded
Approval → CRM job → CRM callback mints the Broker (verified)
  1. Approve. From PENDING_REVIEW, MarkApplicationAsApproved (MarkApplicationAsApproved.php:13-48) branches:

    • Individual (ambassador): no contract step — it dispatches the CRM job directly (:29-31).
    • Company:CONTRACT_SENT (notify BrokerContractReadyNotification). The broker uploads the two signed contracts (POST documents/signed-contract, only in CONTRACT_SENT), then PATCH submit-contract, which requires both the Salalah-Beach and Jebel-Sifah signed types (else a 400 whose message key is pre_registration_signed_contracts_not_found) and sets CONTRACT_REVIEW (SubmitPreRegistrationSignedContracts.php:18-44). The admin then approve-contract, which sets contract_start_date = now, contract_end_date = now + 1yr, and dispatches the CRM job (ApproveApplicationContract.php:13-19).
  2. The CRM job registers the broker CRM-side. RegisterOmanPreRegistrationBrokerInCrmJob (ShouldQueue, tries = 3, WithoutOverlapping("oman-prereg-crm-{applicationId}")) is idempotent — it returns early if broker_source_id is already set (:45-47). It POSTs {crms-middleware.api_url}/oman-pre-registration/brokers with header country: oman (RegisterBrokerInCrm.php:17-29); on 200 it reads crm_broker_id and sets broker_source_id + status = ACTIVE (:63-74). A non-200 or a missing id throws AppCustomException and retries.

  3. The CRM callback mints the local Broker. After the CRM registers the broker it calls back POST /api/crms-middleware/brokers/register (Passport client_credentials group) → CreateUser::handle (BrokerController.php:57-72), which creates the real Broker row (role broker) with its country source ids, destinations and commissions, and — for a PORTAL-originated Oman registration (handleOmanFlow keys on register_from = portal + Oman, CreateUser.php:53-80) — links application.broker_id = broker.id and sends a reset-password email.

There is no country-header middleware on these routes — Oman is enforced by construction, at several points at once:

  • broker_type must be an Oman type. StartPreRegistrationRequest requires broker_type ∈ {ambassador, local_company, international_company} (StartPreRegistrationRequest.php:23-27, OmanBrokerRegistrationTypeEnums.php:11-13).
  • CRM calls hardcode country: oman (RegisterBrokerInCrm.php:27, UpdateBrokerInCrm.php:28).
  • CreateUser only performs the pre-reg linkage for a PORTAL-originated CountryEnums::OMAN registration (CreateUser.php:53-80); any other country/origin takes the default portal flow and never touches an OmanPreRegistrationBroker.
  • The entire surface — models, enums, notifications, contract/email templates, the Voyager BREAD — is Oman-specific. No other country has a pre-registration endpoint at all.
Symptom Cause Where to look
“I never got the OTP” (email already a broker) start silently no-ops when a real Broker exists for that email (anti-enumeration) StartPreRegistrationBroker.php:18-26 — logs “skipped already existing an (actual) broker”
422 application_already_in_progress An application is already mid-review (PENDING_REVIEW / MISSING_INFORMATION / CONTRACT_SENT / CONTRACT_REVIEW) StartPreRegistrationBroker.php:35-44
“Resend OTP said success but nothing arrived” resendOtp only actually sends when the status is in the requested set; otherwise it returns success without sending PreRegistrationBrokerController.php:99-109
403 on a multi-step endpoint Ability mismatch — a full broker token (or any non-pre-reg token) hits a pre-reg route; the ability: guard rejects it. (A missing/invalid token is a 401 instead.) routes/api/broker.php:52
403/400 wrong-status transition finalize needs PENDING_DOCUMENTS; resubmit needs MISSING_INFORMATION; submit-contract needs CONTRACT_SENT grep laravel.log for “Oman pre-registration … called with wrong status” (logs broker_id/application_id/status/expected_status)
400 (message key pre_registration_signed_contracts_not_found) One of the two required signed contract types is missing SubmitPreRegistrationSignedContracts.php:18-28
Approved but never becomes ACTIVE The CRM job didn’t run (no queue worker) or the middleware returned non-200 RegisterOmanPreRegistrationBrokerInCrmJob logs “registering broker” / “broker registered”; overlap key oman-prereg-crm-{applicationId}
ACTIVE but can’t log in / no Broker The CRM→portal brokers/register callback never arrived → broker_id is null, no reset-password email Check middleware→portal connectivity; CreateUser Oman branch (CreateUser.php:70-80); confirm the register payload’s country resolves to Oman