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 application status machine
Section titled “The application status machine”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
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.
-
Start.
POST oman-pre-registration-applications/start(PreRegistrationBrokerController@start,routes/api/broker.php:39) sends apre_registration_brokerOTP by email. If a realBrokeralready exists for that email,startsilently 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. -
Verify the OTP → get the token.
POST otp/verify(verifyOtp,PreRegistrationBrokerController.php:70-90) checks and deletes the OTP, thenGeneratePreRegistrationBrokerToken(app/Actions/OmanPreRegistrationBroker/GeneratePreRegistrationBrokerToken.php:21-54) row-locks the applicant, stampsemail_verified_at, creates the application if missing (seedingstatus = PENDING_INFO), deletes any prior tokens so only one is ever active (:43), and mintscreateToken('pre-registration-broker', ['access-pre-registration-broker-api']). The response is{ public_id, token, token_type: 'Bearer' }. -
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 thedocumentsroutes. A full broker token gets 403 here — it authenticates fine but lacks the ability, so theability: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.
Two steps: PATCH company-info → PENDING_COMPANY_DETAILS (:74-79), then PATCH
company-details → PENDING_DOCUMENTS (:106-108). Company applications also collect the trade
license, authorized-person/signatory details, and (later) the two signed contracts.
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).
ODH admin review, in Voyager
Section titled “ODH admin review, in Voyager”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-
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(notifyBrokerContractReadyNotification). The broker uploads the two signed contracts (POSTdocuments/signed-contract, only inCONTRACT_SENT), then PATCHsubmit-contract, which requires both the Salalah-Beach and Jebel-Sifah signed types (else a 400 whose message key ispre_registration_signed_contracts_not_found) and setsCONTRACT_REVIEW(SubmitPreRegistrationSignedContracts.php:18-44). The admin then approve-contract, which setscontract_start_date = now,contract_end_date = now + 1yr, and dispatches the CRM job (ApproveApplicationContract.php:13-19).
- Individual (ambassador): no contract step — it dispatches the CRM job directly (
-
The CRM job registers the broker CRM-side.
RegisterOmanPreRegistrationBrokerInCrmJob(ShouldQueue,tries = 3,WithoutOverlapping("oman-prereg-crm-{applicationId}")) is idempotent — it returns early ifbroker_source_idis already set (:45-47). ItPOSTs{crms-middleware.api_url}/oman-pre-registration/brokerswith headercountry: oman(RegisterBrokerInCrm.php:17-29); on 200 it readscrm_broker_idand setsbroker_source_id+status = ACTIVE(:63-74). A non-200 or a missing id throwsAppCustomExceptionand retries. -
The CRM callback mints the local
Broker. After the CRM registers the broker it calls backPOST /api/crms-middleware/brokers/register(Passportclient_credentialsgroup) →CreateUser::handle(BrokerController.php:57-72), which creates the realBrokerrow (rolebroker) with its country source ids, destinations and commissions, and — for a PORTAL-originated Oman registration (handleOmanFlowkeys onregister_from= portal + Oman,CreateUser.php:53-80) — linksapplication.broker_id = broker.idand sends a reset-password email.
Why this is Oman-only
Section titled “Why this is Oman-only”There is no country-header middleware on these routes — Oman is enforced by construction, at several points at once:
broker_typemust be an Oman type.StartPreRegistrationRequestrequiresbroker_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). CreateUseronly performs the pre-reg linkage for a PORTAL-originatedCountryEnums::OMANregistration (CreateUser.php:53-80); any other country/origin takes the default portal flow and never touches anOmanPreRegistrationBroker.- 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.
Support
Section titled “Support”| 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 |
