Skip to content

User Profile Lifecycle

General Flow

Upon registration, the entity is considered undecided. The client makes the entity decision upon the first order of the FTMO Challenge.

The profile entity is locked based on the decision made (regardless of whether the order was paid). The client can no longer change or add company information to the order form.

API Endpoint

GET /user/profile (Controller_user::action_profile)

{
  "entityType": "undecided|person|company",
  "verifiedIdentity": true,
  "verifiedPhoneNumber": true,
  "isImpersonator": false,
  "company_name": "",
  "icnumber": "",
  "dicnumber": "",
  "secondaryTaxNumber": "",
  "taxCountry": "",
  "taxState": "",
  "isUnderVIES": false,
  "mode": "ftmo|vrgk|jv"
}

Footnote: - Entity — in future it shall be in IDP - verifiedIdentity and verifiedPhoneNumber — in future, should be obtained by calling Verified Identity


Entity (Client) State

The system contains three states:

State Name Description
1 Undecided No non-free-trial order exists yet
2 Company Has a non-free-trial order AND company data is present
3 Person Has a non-free-trial order AND no company data

The user interface presents the undecided state as a natural person with the option to switch to a company.

Decision Logic

Implemented in EntityTypeHelper::getEntityType():

1. Does the client have at least one non-free-trial order?
   NO  → Undecided (return early)
   YES → continue

2. Is any of company_name, icnumber, or dicnumber non-empty?
   YES → Company
   NO  → Person

Important: The order check comes first. A client with company data but no non-free-trial order is still Undecided.

Company Check

IdentityCompanyHelper::isCompany() — returns true if any of these are non-empty: - company_name - icnumber (Business Number / ABN) - dicnumber (VAT Number / ACN)

Non-Free-Trial Order Check

SELECT 1 AS `exists`
FROM orders
WHERE payment_method <> 'free_trial'
  AND clients_id = :clients_id
LIMIT 1;

The platform operates under three legal regimes, determined by the user's GDPR groups in their JWT token:

Regime Enum Description
Global (FTMO) LegalRegime::Ftmo Default regime, global users
VRGK LegalRegime::Vrgk Australian users
JV LegalRegime::Jv US joint-venture users

Resolved in LegalRegimeResolver::getRegime() based on GDPR groups: - VRGK groups: VRGK - JV groups: JV_PROP_EVAL_US, OANDA_PROP_TRADING_US - Global groups: FTMO_EVAL_GLOBAL, FTMO_EVAL_US, FTMO_TRADING_GLOBAL, FTMO_TRADING_US

A user cannot belong to multiple regimes simultaneously (throws MultipleLegalRegimesException). Users with no GDPR groups default to Global.

Transfer Countries

Countries that require a transfer flow: Australia (AU) and United States (US), defined in TransferCountry enum.


Profile Locking

Inputs on the order form and profile form are locked based on actions taken, ultimately leading to the entire profile being locked.

Key Code References

  • Profile page view: ViewComponentProfilePersonalInformation.php
  • Profile controller: Controller_profile.php (trader)
  • Profile API: Controller_user::action_profile (api)
  • Order form view: ViewTraderComponentOrderBillingInfo.php
  • Order form fill strategy: OrderFormFillStrategy.php

Locking Variables (Profile Page)

Three boolean flags control locking in ViewComponentProfilePersonalInformation:

$shouldPersonalInformationBeReadonly =
    hasVerifiedIdentity() || is_impersonated;

$shouldPhoneNumberBeReadonly =
    hasVerifiedPhoneNumber() || is_impersonated;

$shouldCountryAndStateBeReadonly =
    disableCountryAndStateChange || hasVerifiedIdentity() || is_impersonated;

Where disableCountryAndStateChange is computed in Controller_profile:

$disableCountryStateChange =
    hasTag(ALLOW_SANCTIONED_COUNTRY) || hasTag(NO_ORDERS)
    || ForbiddenCountryChange::tryFrom(country) !== null;  // currently only AU

A) Email Locking

Email is always locked and cannot be changed under any circumstances.

Both profile and order forms render the email input with disabled and readonly attributes hardcoded.


B) Entity Locking

The entity decision (person vs. company) is locked once the first non-free-trial order is placed.

Company Name

Locked on the order form when any of: - hasVerifiedIdentity() is true - hasAnyNonFreeTrialOrder is true (locked regardless of whether company name is filled) - is_impersonated is true

Note: The company name input is locked as soon as the user has any non-free-trial order, even if the field is empty. The user cannot retroactively add company information after their first paid order.

VAT Number / ACN (dicnumber)

Locked on the order form when any of: - hasVerifiedIdentity() is true - is_impersonated is true - hasAnyNonFreeTrialOrder AND the value is non-empty

For VRGK (AU) users, this field is labeled ACN (Australian Company Number). For all other users, it is the VAT Number. When the country is under VIES, the VAT input splits into a country code select + number input.

Business Number / ABN (icnumber)

Locked on the order form when any of: - hasVerifiedIdentity() is true - is_impersonated is true - hasAnyNonFreeTrialOrder AND the value is non-empty

For VRGK (AU) users, this field is labeled ABN (Australian Business Number). For all other users, it is the Business Number.

Note: There is interconnected validation: when a company is required to fill in the VAT Number, Business Number, or both, the form enforces this via CompanyDataNotSetException.

Secondary Tax Number

Once filled, it is locked. It can only be filled out for a specific country or state (e.g., certain Canadian provinces). The value is carried over from existing billing data and is not editable on the order form UI.

taxCountry, taxState

Internal attributes for the edge case when the country of residence differs from the company tax registration country. Once filled (only with the first submission), they are locked. These values are preserved from existing billing data in OrderFormFillStrategy and not exposed as editable inputs.


C) Address Locking (City, Street, Postal Code)

Locked on the profile page when either: - hasVerifiedIdentity() is true (KYC/KYB verified) - is_impersonated is true

On the order form, the same conditions apply.


D) Phone Number

Locked when either: - hasVerifiedPhoneNumber() is true - is_impersonated is true

The phone number may be verified as part of KYC/KYB (majority of cases). There is a dedicated API attribute (verifiedPhoneNumber).

Backend enforcement: When both identity and phone are verified, no profile attributes are editable. When only identity is verified (but not phone), only PHONE remains editable via the API.

$attributes = match ([hasVerifiedIdentity(), hasVerifiedPhoneNumber()]) {
    [true, true]  => [],                    // nothing editable
    [true, false]  => [PHONE],              // only phone
    default        => [FIRST_NAME, LAST_NAME, TITLE, PHONE, COUNTRY, CITY, STREET, ZIP_CODE, STATE],
};

E) Country

There are regulated countries which cannot be changed from or into. Australia and the USA have special error messages and require a transfer flow.

Country Select is Disabled When

On the profile page (ViewComponentProfilePersonalInformation):

shouldCountryAndStateBeReadonly =
    disableCountryAndStateChange
    OR hasVerifiedIdentity()
    OR is_impersonated

where disableCountryAndStateChange =
    hasTag(ALLOW_SANCTIONED_COUNTRY) OR hasTag(NO_ORDERS)
    OR country == 'AU'

On the order form (ViewTraderComponentOrderBillingInfo):

disabled =
    disableCountryStateChange
    OR is_impersonated
    OR hasVerifiedIdentity()

Country Change Validation (Backend)

The following validations apply when a country change is submitted:

  1. Australia (VRGK): Changing from AU is forbidden (ForbiddenChangeCountryValidator::validateVrgkRequirements). Changing to AU is also forbidden if the user is not already AU (Controller_profile.php:595-598).
  2. JV regime: If the user is under the JV legal regime, any country or state change is forbidden (ForbiddenChangeCountryValidator::validateJvRequirements).
  3. Forbidden countries: Validated via CountriesAndStatesService::isForbiddenCountry() (sanctioned countries from DB).
  4. Available countries: The new country must be in the user's available countries list.

Australia Special Behavior

  • When the user's country is AU, a special message is shown: AUSTRALIA_DISABLED_COUNTRY_CHANGE_MESSAGE_1 with a link to customer support.
  • When a user selects AU in the country dropdown (frontend), an alert is shown (alert-au-order) and the submit button is hidden — handled in assets/src/trader/js/pages/profile.ts.

F) States

Forbidden States

Certain states (primarily in the USA) are considered forbidden/sanctioned. These are loaded from the database via CountriesAndStatesService::getForbiddenStatesCodes() (sourced from countriesLocationSettingsRepository::getSanctionedStates()).

State Change with Moved Confirmation

When a user changes from a forbidden state to a non-forbidden state, a "moved confirmation" is required (ClientsConfirmationCountriesAndStatesService::stateChangeRequiresMovedConfirmation):

  1. If the new state is forbidden → throws BlacklistedStateValidationException
  2. If the prior state was not forbidden → no confirmation needed
  3. If the prior state was forbidden and the user has not confirmed moving → blocks the change

The confirmation key is USA_STATES_RESTRICTION_2023_MOVED.

US ZIP-to-State Validation

For US users, the ZIP code must match the selected state (ZipToStateValidator::isValid).


Edition of Profile with Verified Identity

Once identity is verified, there are only two options to update profile data:

  1. NBO tool "Change IMS customer attributes" — allowed only for rare cases like typo fixes. Requires the special role user_profile_editor.
  2. Create a new KYC/KYB — triggers a new identity verification process.

Both methods are only available on individual request and for limited cases.


Termination of User Profile

No data is deleted or anonymised in SSO yet (there is a sleeping feature for data anonymisation).

Two features technically terminate user access:

1. Blacklisting

Adding the role blacklisted to the user. The login page shows a blocked message after sign-in.

2. Disabling

Setting enabled to false on the client's profile.

Both can be performed from the "Advance" card in NBO.

The API endpoint DELETE /profile (Controller_profile in api) handles termination requests using DiscardIdentityService::discardIdentity() with the action TERMINATION_DURING_TRANSFER. This cannot be used when impersonated.