openapi: 3.0.0
info:
  title: Halogen Wallet API
  description: |
    The Halogen Wallet API provides a unified interface for managing client investment portfolios, funds, and transaction requests.

    ### API Key Setup

    To authenticate and use the API, you must first create an API key:
    1. Login to **https://wallet.halogen.my**.
    2. Navigate to **Settings > API Keys**.
    3. Create a new API key by providing a Certificate Signing Request (CSR). Elliptic Curve P-256 (recommended) and RSA-4096 are supported.

    #### Generating a CSR using OpenSSL

    * **Elliptic Curve P-256 (Recommended):**
        ```bash
        $ mkdir -p .key
        $ openssl ecparam -name prime256v1 -genkey -noout -out .key/ec_private_key.pem
        $ openssl req -new -key .key/ec_private_key.pem -out .key/ec_csr.pem -sha256 -subj "/C=US/ST=State/L=City/O=Organization/OU=Unit/CN=example.com"
        ```
    * **RSA-4096:**
        ```bash
        $ mkdir -p .key
        $ openssl req -new -newkey rsa:4096 -nodes -keyout .key/rsa_private_key.pem -out .key/rsa_csr.pem -subj "/C=US/ST=State/L=City/O=Organization/OU=Unit/CN=example.com"
        ```
    > **Security Note:** Keep the generated **Private Key** (e.g., `ec_private_key.pem` or `rsa_private_key.pem`) in a secure storage and **never share it**. This key is used to digitally sign your API requests before they are sent to the Halogen Wallet server.

    ---

    ### API Usage Guide

    This API routes requests through two primary endpoints based on the operation type.

    * **`/query`**: Used for **read-only** operations (e.g., listing accounts, getting profile details).
    * **`/command`**: Used for **write/mutate** operations (e.g., creating investments, updating profiles).

    ---

    ### Request Structure

    Every request must be a **POST** containing a JSON body with two keys:
    1.  `name`: The specific action identifier (e.g., `list_client_accounts`).
    2.  `payload`: The data structure required for that specific action.

    > **Documentation Tip:** In the **Request Body** section below, use the **Schema/Example Dropdown** to select the specific action you want to perform. The documentation will update to show the exact `payload` required.
  version: 0.0.9
servers:
  - url: https://external-api.wallet.halogen.my
    description: Production Server

components:
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT

  # -------------------------------------------------------------------------
  # SHARED ERROR EXAMPLES (Values)
  # -------------------------------------------------------------------------
  examples:
    ErrMissingParameter:
      value:
        {
          statusCode: 400,
          code: "ErrMissingParameter",
          message: "{field} is required.",
        }
    ErrInvalidParameter:
      value:
        {
          statusCode: 400,
          code: "ErrInvalidParameter",
          message: "{field} is invalid.",
        }
    ErrInvalidDateRange:
      value:
        {
          statusCode: 400,
          code: "ErrInvalidDateRange",
          message: "fromDate must be earlier than toDate.",
        }
    ErrActionOutsideFundHours:
      value:
        {
          statusCode: 400,
          code: "ErrActionOutsideFundHours",
          message: "Unable to {action}. Fund's operation hours 08:00 to 22:00 MYT",
        }
    ErrInsufficientBalance:
      value:
        {
          statusCode: 400,
          code: "ErrInsufficientBalance",
          message: "insufficient balance.",
        }
    ErrAlreadyExists:
      value:
        {
          statusCode: 400,
          code: "ErrAlreadyExists",
          message: "{resource} already exists.",
        }
    ErrInvalidAccountExperience:
      value:
        {
          statusCode: 400,
          code: "ErrInvalidAccountExperience",
          message: "account is not a {experience}.",
        }
    ErrInvalidRequestPolicy:
      value:
        {
          statusCode: 400,
          code: "ErrInvalidRequestPolicy",
          message: "policy is invalid. Value must be {reason}.",
        }
    ErrRequestCannotBeCancelled:
      value:
        {
          statusCode: 400,
          code: "ErrRequestCannotBeCancelled",
          message: "Request cannot be cancelled. {reason}.",
        }

    ErrUnauthorizedIPAddress:
      value:
        {
          statusCode: 401,
          code: "ErrUnauthorizedIPAddress",
          message: "IP address is not authorized.",
        }

    ErrInsufficientAccess:
      value: { statusCode: 403, code: "ErrInsufficientAccess", message: "" }

    ErrMissingResource:
      value:
        {
          statusCode: 404,
          code: "ErrMissingResource",
          message: "The {resource} does not exist.",
        }

    ErrCancelledRequest:
      value:
        {
          statusCode: 410,
          code: "ErrCancelledRequest",
          message: "The request has been cancelled.",
        }

    ErrActionNotAllowedForAccountType:
      value:
        {
          statusCode: 412,
          code: "ErrActionNotAllowedForAccountType",
          message: "Action not allowed for this account type.",
        }
    ErrSuitabilityAssessmentRequired:
      value:
        {
          statusCode: 412,
          code: "ErrSuitabilityAssessmentRequired",
          message: "Suitability assessment required.",
        }

    ErrRateLimitExceeded:
      value:
        {
          statusCode: 429,
          code: "ErrRateLimitExceeded",
          message: "rate limit exceeded.",
        }

    ErrInternal:
      value: { statusCode: 500, code: "ErrInternal", message: "" }

    ErrServiceUnavailable:
      value: { statusCode: 503, code: "ErrServiceUnavailable", message: "" }

  # -------------------------------------------------------------------------
  # SHARED RESPONSE DEFINITIONS (The "Dropdowns" for Errors)
  # -------------------------------------------------------------------------
  responses:
    BadRequestError:
      description: Bad Request / Validation Error
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          examples:
            ErrMissingParameter:
              { $ref: "#/components/examples/ErrMissingParameter" }
            ErrInvalidParameter:
              { $ref: "#/components/examples/ErrInvalidParameter" }
            ErrInvalidDateRange:
              { $ref: "#/components/examples/ErrInvalidDateRange" }
            ErrInvalidRequestPolicy:
              { $ref: "#/components/examples/ErrInvalidRequestPolicy" }
            ErrActionOutsideFundHours:
              { $ref: "#/components/examples/ErrActionOutsideFundHours" }
            ErrInsufficientBalance:
              { $ref: "#/components/examples/ErrInsufficientBalance" }
            ErrAlreadyExists: { $ref: "#/components/examples/ErrAlreadyExists" }
            ErrInvalidAccountExperience:
              { $ref: "#/components/examples/ErrInvalidAccountExperience" }
            ErrRequestCannotBeCancelled:
              { $ref: "#/components/examples/ErrRequestCannotBeCancelled" }

    UnauthorizedError:
      description: Unauthorized
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          examples:
            ErrUnauthorizedIPAddress:
              { $ref: "#/components/examples/ErrUnauthorizedIPAddress" }

    ForbiddenError:
      description: Forbidden
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          examples:
            ErrInsufficientAccess:
              { $ref: "#/components/examples/ErrInsufficientAccess" }

    NotFoundError:
      description: Not Found
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          examples:
            ErrMissingResource:
              { $ref: "#/components/examples/ErrMissingResource" }

    GoneError:
      description: Gone
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          examples:
            ErrCancelledRequest:
              { $ref: "#/components/examples/ErrCancelledRequest" }

    PreconditionFailedError:
      description: Precondition Failed
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          examples:
            ErrActionNotAllowedForAccountType:
              {
                $ref: "#/components/examples/ErrActionNotAllowedForAccountType",
              }
            ErrSuitabilityAssessmentRequired:
              { $ref: "#/components/examples/ErrSuitabilityAssessmentRequired" }

    TooManyRequestsError:
      description: Too Many Requests
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          examples:
            ErrRateLimitExceeded:
              { $ref: "#/components/examples/ErrRateLimitExceeded" }

    InternalServerError:
      description: Internal Server Error
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          examples:
            ErrInternal: { $ref: "#/components/examples/ErrInternal" }

    ServiceUnavailableError:
      description: Service Unavailable
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          examples:
            ErrServiceUnavailable:
              { $ref: "#/components/examples/ErrServiceUnavailable" }

  schemas:
    # -------------------------------------------------------------------------
    # DATA MODELS
    # -------------------------------------------------------------------------
    Error:
      type: object
      properties:
        statusCode: { type: integer, deprecated: true, example: 400 }
        code: { type: string, example: "ErrMissingParameter" }
        message: { type: string, example: "accountId is required." }

    Address:
      type: object
      properties:
        type: { type: string }
        line1: { type: string }
        line2: { type: string }
        city: { type: string }
        postcode: { type: string }
        state: { type: string }
        country: { type: string }
    
    AllocationPerformance:
      type: object
      properties:
        date: { type: string }
        units: { type: number, format: double }
        asset: { type: string }
        netAssetValuePerUnit: { type: number, format: double }
        value: { type: number, format: double }
        postFeeAmount: { type: number, format: double }

    Bank:
      type: object
      properties:
        name: { type: string }
        bic: { type: string }
        imageUrl: { type: string }
        rank: { type: integer }

    BankAccount:
      type: object
      properties:
        accountNumber: { type: string }
        accountName: { type: string }
        accountCurrency: { type: string }
        accountType: { type: string }
        bankName: { type: string }
        bankBic: { type: string }
        referenceNumber: { type: string }
        imageUrl: { type: string }
        status: { type: string }
        source: { type: string }
        createdAt: { type: string }
        createdBy: { type: string }
    
    Balance:
      type: object
      properties:
        fundId: { type: string }
        fundClassSequence: { type: integer }
        fundName: { type: string }
        fundShortName: { type: string }
        fundClassLabel: { type: string }
        fundCode: { type: string }
        fundImageUrl: { type: string }
        units: { type: number }
        asset: { type: string }
        value: { type: number }
        valuedAt: { type: string }
        minimumRedemptionAmount: { type: number }
        minimumRedemptionUnits: { type: number }
        minimumSubscriptionAmount: { type: number }
        minimumSubscriptionUnits: { type: number }
        redemptionFeePercentage: { type: number }
        switchFeePercentage: { type: array, format: string }
        availableModes: {  type: string }
        isOutOfService: { type: boolean }
        outOfServiceMessage: { type: string }

    Consent:
      type: object
      properties:
        name: { type: string }
        label: { type: string }

    ClientAccount:
      type: object
      properties:
        id: { type: string }
        type: { type: string, enum: [single, joint] }
        name: { type: string }
        experience: { type: string, enum: [fundmanagement, mandate, dim] }
        experienceLabel: { type: string, format: string }
        asset: { type: string, format: string }
        portfolioValue: { type: number, format: double }
        exposurePercentage: { type: number, format: double }
        pnlAmount: { type: number, format: double }
        pnlPercentage: { type: number, format: double }
        netInflow: { type: number, format: double }
        totalInflow: { type: number, format: double }
        totalOutflow: { type: number, format: double }
        pendingSwitchInAmount: { type: number, format: double }
        canInvest: { type: boolean }
        canRedeem: { type: boolean }
        canSwitch: { type: boolean }
        canDeposit: { type: boolean }
        canWithdraw: { type: boolean }
        canUpdateAccountName: { type: boolean }
        riskLabel: { type: string, format: string }
        riskDescription: { type: string, format: string }
    
    ClientAccountRequest:
      type: object
      properties:
        id: { type: string }
        type: { type: string }
        fundId: { type: string }
        fundName: { type: string }
        fundShortName: { type: string }
        fundClassLabel: { type: string }
        asset: { type: string }
        amount: { type: number }
        postFeeAmount: { type: number }
        units: { type: number }
        unitPrice: { type: number }
        feePercentage: { type: number }
        strokedFeePercentage: { type: number }
        feeAmount: { type: number }
        rebateFromDate: { type: string }
        rebateToDate: { type: string }
        status: { type: string }
        voucherCode: { type: string }
        consentType: { type: string }
        consentStatus: { type: string }
        collectionBankAccount:
          type: array
          items: { $ref: "#/components/schemas/BankAccount" }
        createdAt: { type: string }

    ClientAccountPerformance:
      type: object
      properties:
        date: { type: string }
        accountId: { type: string }
        value: { type: string }

    DisplayCurrency:
      type: object
      properties:
        id: { type: string }
        label: { type: string }
        imageUrl: { type: string }

    Fund:
      type: object
      properties:
        id: { type: string }
        type: { type: string }
        name: { type: string }
        shortName: { type: string }
        baseCurrency: { type: string }
        category: { type: string }
        code: { type: string }
        investmentObjective: { type: string }
        investmentType: { type: string }
        riskRating: { type: string }
        riskScore: { type: integer }
        primaryFundManager: { type: string }
        secondaryFundManager: { type: string }
        shariahCompliant: { type: boolean }
        status: { type: string }
        tagLine: { type: string }
        trustee: { type: string }
        imageUrl: { type: string }
        createdAt: { type: string }
        isOutOfService: { type: boolean }
        outOfServiceMessage: { type: string }
        metadata: { type: object }
        classes:
          type: array
          items: { $ref: "#/components/schemas/FundClass" }

    FundClass:
      type: object
      properties:
        sequence: { type: integer }
        label: { type: string }
        baseCurrency: { type: string }
        managementFee: { type: number, format: double }
        trusteeFee: { type: number, format: double }
        custodianFee: { type: number, format: double }
        transferFee: { type: number, format: double }
        trusteeFeeAnnualMinimum: { type: number, format: double }
        switchingFee: { type: number, format: double }
        subscriptionFee: { type: number, format: double }
        redemptionFee: { type: number, format: double }
        performanceFee: { type: number, format: double }
        taxRate: { type: number, format: double }
        minimumInitialInvestment: { type: number, format: double }
        minimumUnitsHeld: { type: number, format: double }
        minimumRedemptionAmount: { type: number, format: double }
        canDistribute: { type: boolean }
        launchPrice: { type: number, format: double }
        hexColor: { type: string }
        commencementAt: { type: string }
        initialOfferingPeriodFrom: { type: string }
        initialOfferingPeriodTo: { type: string }
        createdAt: { type: string }
        distributionFrequency: { type: string }
        tagLine: { type: string }
        metadata: { type: object }

    PolicyGroup:
      type: object
      properties:
        label: { type: string }
        min: { type: integer }
        max: { type: integer }

    PolicyParticipant:
      type: object
      properties:
        email: { type: string }
        groupLabel: { type: string }
        name: { type: string }
        signed: { type: boolean }
        signedAt: { type: string }

    Promo:
      type: object
      properties:
        accountId: { type: string }
        accountName: { type: string }
        code: { type: string }
        label: { type: boolean }
        description: { type: string }
        discountPercentage: { type: number, format: double }
        discountFrom: { type: string }
        validFromDate: { type: string }
        validToDate: { type: string }
        createdAt: { type: string }

    SuitabilityAssessment:
      type: object
      properties:
        id: { type: string }
        clientId: { type: string }
        source: { type: string }
        investmentExperience: { type: string }
        investmentObjective: { type: string }
        investmentHorizon: { type: string }
        currentInvestment: { type: string }
        returnExpectations: { type: string }
        attachment: { type: string }
        totalScore: { type: integer }
        riskTolerance: { type: string }
        createdBy: { type: string }
        createdAt: { type: string }

    # -------------------------------------------------------------------------
    # PAYLOADS (Inputs & Outputs)
    # -------------------------------------------------------------------------
    ListClientAccountsInput:
      type: object
      properties:
        accountIds: { type: array, items: { type: string } }
    ListClientAccountsOutput:
      type: object
      properties:
        amount: { type: number }
        asset: { type: string }
        canCreateAccount: { type: boolean }
        accounts:
          { type: array, items: { $ref: "#/components/schemas/ClientAccount" } }

    GetClientProfileInput:
      type: object
      properties: {}
    GetClientProfileOutput:
      type: object
      properties:
        name: { type: string }
        nationality: { type: string }
        nricNo: { type: string }
        passportNo: { type: string }
        msisdn: { type: string }
        email: { type: string }
        permanentAddress: { $ref: "#/components/schemas/Address" }
        correspondenceAddress: { $ref: "#/components/schemas/Address" }
        type: { type: string }
        investorCategory: { type: string }
        countryOfIncorporation: { type: string }
        authorisedPersonName: { type: string }
        authorisedPersonEmail: { type: string }
        authorisedPersonMsisdn: { type: string }
        authorisedPersonOfficeNo: { type: string }
        companyRegistrationNo: { type: string }
        oldCompanyRegistrationNo: { type: string }
        ethnicity: { type: string }
        domesticRinggitBorrowing: { type: string }
        taxResidency: { type: string }
        countryTax: { type: string }
        taxIdentificationNo: { type: string }
        incompleteProfile: { type: boolean }
        isAccountOwner: { type: boolean }
        canInvestInUnitTrust: { type: boolean }
        canInvestInDim: { type: boolean }
        canUpdateProfile: { type: boolean }
        canSubscribePushNotification: { type: boolean }
        status: { type: string }

    GetFundInput:
      type: object
      properties:
        fundId: { type: string }
    GetFundOutput:
      type: object
      properties:
        fund: { $ref: "#/components/schemas/Fund" }

    GetClientAccountAllocationPerformanceInput:
      type: object
      properties:
        accountId: { type: string }
        allocationId: { type: string }
        type: { type: string }
        fundClassSequence: { type: integer }
        timeframe: { type: string }
        interval: { type: string }
    GetClientAccountAllocationPerformanceOutput:
      type: object
      properties:
        performance:
          { type: array, items: { $ref: "#/components/schemas/AllocationPerformance" } }

    GetClientAccountStatementInput:
      type: object
      properties:
        accountId: { type: string }
        fromDate: { type: string, format: "YYYY-MM" }
        toDate: { type: string, format: "YYYY-MM" }
        format: { type: string }
    GetClientAccountStatementOutput:
      type: object
      properties:
        fromDate: { type: string, format: "YYYY-MM" }
        toDate: { type: string, format: "YYYY-MM" }
        format: { type: string }
        filename: { type: string }
        bytes: { type: "[]byte" }

    GetClientAccountRequestConfirmationInput:
      type: object
      properties:
        accountId: { type: string }
        requestId: { type: string }
        format: { type: string }
    GetClientAccountRequestConfirmationOutput:
      type: object
      properties:
        format: { type: string }
        filename: { type: string }
        bytes: { type: "[]byte" }

    GetClientReferralInput:
      type: object
      properties: {}
    GetClientReferralOutput:
      type: object
      properties:
        referralCode: { type: string }
        referredClientsCount: { type: integer }

    GetClientAccountRequestPolicyInput:
      type: object
      properties:
        accountId: { type: string }
        requestId: { type: string }
    GetClientAccountRequestPolicyOutput:
      type: object
      properties:
        groups: { type: array, items: { $ref: "#/components/schemas/PolicyGroup" } }
        participants: { type: array, items: { $ref: "#/components/schemas/PolicyParticipant" } }

    ListFundsForSubscriptionInput:
      type: object
      properties:
        accountId: { type: string }
    ListFundsForSubscriptionOutput:
      type: object
      properties:
        funds: { type: array, items: { $ref: "#/components/schemas/Fund" } }

    ListClientAccountBalanceInput:
      type: object
      properties:
        accountId: { type: string }
    ListClientAccountBalanceOutput:
      type: object
      properties:
        balance: { type: array, items: { $ref: "#/components/schemas/Balance" } }

    ListClientAccountRequestsInput:
      type: object
      properties:
        accountId: { type: string }
        requestId: { type: string }
        fundIds: { type: string }
        fromDate: { type: string }
        toDate: { type: string }
        types: { type: array, format: string }
        statuses: { type: array, format: string }
        limit: { type: integer }
        offset: { type: integer }
        completedOnly: { type: boolean }
    ListClientAccountRequestsOutput:
      type: object
      properties:
        requests: { type: array, items: { $ref: "#/components/schemas/ClientAccountRequest" } }

    ListClientBankAccountsInput:
      type: object
      properties: {}
    ListClientBankAccountsOutput:
      type: object
      properties:
        bankAccounts:
          { type: array, items: { $ref: "#/components/schemas/BankAccount" } }

    ListDisplayCurrenciesInput:
      type: object
      properties: {}
    ListDisplayCurrenciesOutput:
      type: object
      properties:
        displayCurrency: { type: string }
        currencies: { type: array, items: { $ref: "#/components/schemas/DisplayCurrency" } }

    ListClientSuitabilityAssessmentsInput:
      type: object
      properties: {}
    ListClientSuitabilityAssessmentsOutput:
      type: object
      properties:
        shouldAskSuitabilityAssessment: { type: boolean }
        canIgnoreSuitabilityAssessment: { type: boolean }
        assessments: { type: array, items: { $ref: "#/components/schemas/SuitabilityAssessment" } }

    ListInvestConsentsInput:
      type: object
      properties:
        accountId: { type: string }
        fundId: { type: string }
        fundClassSequence: { type: integer }
    ListInvestConsentsOutput:
      type: object
      properties:
        consents: { type: array, items: { $ref: "#/components/schemas/Consent" } }
        consentFundIM: { type: boolean }
        consentHighRisk: { type: boolean }

    ListBanksInput:
      type: object
      properties: {}
    ListBanksOutput:
      type: object
      properties:
        banks: { type: array, items: { $ref: "#/components/schemas/Bank" } }

    ListClientPromosInput:
      type: object
      properties: {}
    ListClientPromosOutput:
      type: object
      properties:
        promos: { type: array, items: { $ref: "#/components/schemas/Promo" } }

    ListClientAccountPerformanceInput:
      type: object
      properties:
        accountIds: { type: array, items: { type: string } }
        timeFrame: { type: string }
        interval: { type: string }
    ListClientAccountPerformanceOutput:
      type: object
      properties:
        performance: { type: array, items: { $ref: "#/components/schemas/ClientAccountPerformance" } }

    ListPaymentMethodsInput:
      type: object
      properties: {}
    ListPaymentMethodsOutput:
      type: object
      properties:
        duitnow: { type: boolean }
        bankTransfer: { type: boolean }

    GetVoucherInput:
      type: object
      properties:
        accountId: { type: string }
        fundId: { type: string }
        fundClassSequence: { type: integer }
        amount: { type: number }
        voucherCode: { type: string }
    GetVoucherOutput:
      type: object
      properties:
        valid: { type: boolean }
        code: { type: string }
        strokedSubscriptionFeePercentage: { type: number }
        appliedSubscriptionFeePercentage: { type: number }
        voucherDiscountPercentage: { type: number }
        feeAmount: { type: number }
        postFeeAmount: { type: number }

    GetPreviewInvestInput:
      type: object
      properties:
        accountId: { type: string }
        fundId: { type: string }
        fundClassSequence: { type: integer }
        amount: { type: number }
    GetPreviewInvestOutput:
      type: object
      properties:
        strokedSubscriptionFeePercentage: { type: number }
        appliedSubscriptionFeePercentage: { type: number }
        postFeeAmount: { type: number }
        feeAmount: { type: number }
        defaultVoucher: { items: { $ref: "#/components/schemas/GetVoucherOutput" } }

    GetProjectedFundPriceInput:
      type: object
      properties:
        fundId: { type: string }
        fundClassSequence: { type: integer }
    GetProjectedFundPriceOutput:
      type: object
      properties:
        asset: { type: string }
        netAssetValuePerUnit: { type: number, format: double }

    CreateInvestmentRequestInput:
      type: object
      properties:
        accountId: { type: string }
        fundId: { type: string }
        fundClassSequence: { type: integer }
        amount: { type: number }
        consentFundIM: { type: boolean }
        consentHighRisk: { type: boolean }
        consents:
          type: object
          additionalProperties:
            type: boolean
        voucherCode: { type: string }
    CreateInvestmentRequestOutput:
      type: object
      properties:
        requestId: { type: string }

    CreateRedemptionRequestInput:
      type: object
      properties:
        accountId: { type: string }
        fundId: { type: string }
        fundClassSequence: { type: integer }
        requestedAmount: { type: number }
        units: { type: number }
        toBankAccountNumber: { type: string }
    CreateRedemptionRequestOutput:
      type: object
      properties:
        requestId: { type: string }

    CreateSwitchRequestInput:
      type: object
      properties:
        accountId: { type: string }
        switchFromFundId: { type: string }
        switchFromFundClassSequence: { type: integer }
        switchToFundId: { type: string }
        switchToFundClassSequence: { type: integer }
        requestedAmount: { type: number }
        units: { type: number }
    CreateSwitchRequestOutput:
      type: object
      properties:
        requestId: { type: string }

    CreateRequestCancellationInput:
      type: object
      properties:
        accountId: { type: string }
        requestId: { type: string }
    CreateRequestCancellationOutput:
      type: object
      properties: {}

    CreateSuitabilityAssessmentInput:
      type: object
      properties:
        suitabilityAssessment: { $ref: "#/components/schemas/SuitabilityAssessment" }
    CreateSuitabilityAssessmentOutput:
      type: object
      properties:
        suitabilityAssessmentId: { type: string }

    CreateClientBankAccountInput:
      type: object
      properties:
        bankAccount: { $ref: "#/components/schemas/BankAccount" }
    CreateClientBankAccountOutput:
      type: object
      properties: {}

    UpdateDisplayCurrencyInput:
      type: object
      properties:
        displayCurrency: { type: string }
    UpdateDisplayCurrencyOutput:
      type: object
      properties: {}

    UpdateAccountNameInput:
      type: object
      properties:
        accountId: { type: string }
        accountName: { type: string }
    UpdateAccountNameOutput:
      type: object
      properties: {}

    UpdateClientProfileInput:
      type: object
      properties:
        ethnicity: { type: string }
        otherEthnicity: { type: string }
        domesticRinggitBorrowing: { type: string }
        taxResidency: { type: string }
        countryTax: { type: string }
        taxIdentificationNo: { type: string }
    UpdateClientProfileOutput:
      type: object
      properties: {}

    # -------------------------------------------------------------------------
    # WRAPPERS (Discriminator Schemas)
    # -------------------------------------------------------------------------
    ListClientAccounts:
      type: object
      description: Lists all the accounts associated with the client.
      x-errors:
        - ErrMissingParameter
        - ErrInsufficientAccess
        - ErrInternal
      properties:
        name:
          {
            type: string,
            enum: [list_client_accounts],
            example: list_client_accounts,
          }
        payload: { $ref: "#/components/schemas/ListClientAccountsInput" }
    GetClientProfile:
      type: object
      description: Retrieves detailed profile information including personal and demographic data for the authenticated client.
      x-errors:
        - ErrMissingParameter
        - ErrInsufficientAccess
        - ErrInternal
      properties:
        name:
          {
            type: string,
            enum: [get_client_profile],
            example: get_client_profile,
          }
        payload: { $ref: "#/components/schemas/GetClientProfileInput" }
    GetFund:
      type: object
      description: Retrieves detailed information about a specific fund including its characteristics, fees, and investment requirements.
      x-errors:
        - ErrMissingParameter
        - ErrInsufficientAccess
        - ErrMissingResource
        - ErrInternal
      properties:
        name: { type: string, enum: [get_fund], example: get_fund }
        payload: { $ref: "#/components/schemas/GetFundInput" }
    GetClientAccountAllocationPerformance:
      type: object
      description: Retrieves historical performance metrics for a specific fund allocation within an account over a defined timeframe.
      x-errors:
        - ErrMissingParameter
        - ErrInsufficientAccess
        - ErrInvalidParameter
        - ErrInternal
      properties:
        name:
          {
            type: string,
            enum: [get_client_account_allocation_performance],
            example: get_client_account_allocation_performance,
          }
        payload:
          {
            $ref: "#/components/schemas/GetClientAccountAllocationPerformanceInput",
          }
    GetClientAccountStatement:
      type: object
      description: Retrieves retrieves the account statement as a document (PDF or HTML) for transactions within a specified date range.
      x-errors:
        - ErrMissingParameter
        - ErrInsufficientAccess
        - ErrInvalidParameter
        - ErrInvalidDateRange
        - ErrInternal
      properties:
        name:
          {
            type: string,
            enum: [get_client_account_statement],
            example: get_client_account_statement,
          }
        payload: { $ref: "#/components/schemas/GetClientAccountStatementInput" }
    GetClientAccountRequestConfirmation:
      type: object
      description: Retrieves the confirmation document for a specific investment, redemption, or switch request.
      x-errors:
        - ErrMissingParameter
        - ErrInsufficientAccess
        - ErrInvalidParameter
        - ErrInternal
      properties:
        name:
          {
            type: string,
            enum: [get_client_account_request_confirmation],
            example: get_client_account_request_confirmation,
          }
        payload:
          {
            $ref: "#/components/schemas/GetClientAccountRequestConfirmationInput",
          }
    GetClientReferral:
      type: object
      description: Retrieves the client's referral code and the count of successfully referred clients.
      x-errors:
        - ErrMissingParameter
        - ErrInsufficientAccess
        - ErrInternal
      properties:
        name:
          {
            type: string,
            enum: [get_client_referral],
            example: get_client_referral,
          }
        payload: { $ref: "#/components/schemas/GetClientReferralInput" }
    GetClientAccountRequestPolicy:
      type: object
      description: Retrieves the approval policy and participant information for a specific account request.
      x-errors:
        - ErrMissingParameter
        - ErrInsufficientAccess
        - ErrInvalidRequestPolicy
        - ErrInternal
      properties:
        name:
          {
            type: string,
            enum: [get_client_account_request_policy],
            example: get_client_account_request_policy,
          }
        payload:
          { $ref: "#/components/schemas/GetClientAccountRequestPolicyInput" }
    ListFundsForSubscription:
      type: object
      description: Lists all funds available for investment within a specific account based on the account's investor category and experience.
      x-errors:
        - ErrMissingParameter
        - ErrInsufficientAccess
        - ErrInternal
      properties:
        name:
          {
            type: string,
            enum: [list_funds_for_subscription],
            example: list_funds_for_subscription,
          }
        payload: { $ref: "#/components/schemas/ListFundsForSubscriptionInput" }
    ListClientAccountBalance:
      type: object
      description: Lists the current holdings and balances for each fund allocation in a specific account.
      x-errors:
        - ErrMissingParameter
        - ErrInsufficientAccess
        - ErrMissingResource
        - ErrInternal
      properties:
        name:
          {
            type: string,
            enum: [list_client_account_balance],
            example: list_client_account_balance,
          }
        payload: { $ref: "#/components/schemas/ListClientAccountBalanceInput" }
    ListClientAccountRequests:
      type: object
      description: Lists all transaction requests (investments, redemptions, switches) for a specific account with optional filtering and pagination.
      x-errors:
        - ErrMissingParameter
        - ErrInsufficientAccess
        - ErrInvalidParameter
        - ErrInternal
      properties:
        name:
          {
            type: string,
            enum: [list_client_account_requests],
            example: list_client_account_requests,
          }
        payload: { $ref: "#/components/schemas/ListClientAccountRequestsInput" }
    ListClientBankAccounts:
      type: object
      description: Lists all bank accounts registered to the client that can be used for fund transfers and redemptions.
      x-errors:
        - ErrMissingParameter
        - ErrInsufficientAccess
        - ErrInternal
      properties:
        name:
          {
            type: string,
            enum: [list_client_bank_accounts],
            example: list_client_bank_accounts,
          }
        payload: { $ref: "#/components/schemas/ListClientBankAccountsInput" }
    ListDisplayCurrencies:
      type: object
      description: Lists all available currencies that can be used to display portfolio values and transactions.
      x-errors:
        - ErrMissingParameter
        - ErrInsufficientAccess
        - ErrInternal
      properties:
        name:
          {
            type: string,
            enum: [list_display_currencies],
            example: list_display_currencies,
          }
        payload: { $ref: "#/components/schemas/ListDisplayCurrenciesInput" }
    ListClientSuitabilityAssessments:
      type: object
      description: Lists all suitability assessments completed by the client, including risk tolerance evaluations.
      x-errors:
        - ErrInsufficientAccess
        - ErrInternal
      properties:
        name:
          {
            type: string,
            enum: [list_client_suitability_assessments],
            example: list_client_suitability_assessments,
          }
        payload:
          { $ref: "#/components/schemas/ListClientSuitabilityAssessmentsInput" }
    ListInvestConsents:
      type: object
      description: Lists the required consent types that must be obtained before making an investment in a specific fund.
      x-errors:
        - ErrMissingParameter
        - ErrInsufficientAccess
        - ErrMissingResource
        - ErrInternal
      properties:
        name:
          {
            type: string,
            enum: [list_invest_consents],
            example: list_invest_consents,
          }
        payload: { $ref: "#/components/schemas/ListInvestConsentsInput" }
    ListBanks:
      type: object
      description: Lists all banks supported by the platform for withdrawing funds.
      x-errors:
        - ErrMissingParameter
        - ErrInsufficientAccess
        - ErrInternal
      properties:
        name: { type: string, enum: [list_banks], example: list_banks }
        payload: { $ref: "#/components/schemas/ListBanksInput" }
    ListClientPromos:
      type: object
      description: Lists available promotional offers that are applied to client investments.
      x-errors:
        - ErrMissingParameter
        - ErrInsufficientAccess
        - ErrInternal
      properties:
        name:
          {
            type: string,
            enum: [list_client_promos],
            example: list_client_promos,
          }
        payload: { $ref: "#/components/schemas/ListClientPromosInput" }
    ListClientAccountPerformance:
      type: object
      description: Lists historical performance data for one or more client accounts over a specified timeframe.
      x-errors:
        - ErrMissingParameter
        - ErrInsufficientAccess
        - ErrInvalidParameter
        - ErrInternal
      properties:
        name:
          {
            type: string,
            enum: [list_client_account_performance],
            example: list_client_account_performance,
          }
        payload:
          { $ref: "#/components/schemas/ListClientAccountPerformanceInput" }
    ListPaymentMethods:
      type: object
      description: Lists the available payment methods for fund transfers, such as DuitNow and bank transfers.
      x-errors:
        - ErrMissingParameter
        - ErrInsufficientAccess
        - ErrInternal
      properties:
        name:
          {
            type: string,
            enum: [list_payment_methods],
            example: list_payment_methods,
          }
        payload: { $ref: "#/components/schemas/ListPaymentMethodsInput" }
    GetVoucher:
      type: object
      description: Retrieves details and validates a specific voucher code, calculating the discounted fees for an investment.
      x-errors:
        - ErrMissingParameter
        - ErrInsufficientAccess
        - ErrMissingResource
        - ErrInternal
      properties:
        name: { type: string, enum: [get_voucher], example: get_voucher }
        payload: { $ref: "#/components/schemas/GetVoucherInput" }
    GetPreviewInvest:
      type: object
      description: Calculates a preview of an investment transaction, including applicable fees and any default voucher discounts.
      x-errors:
        - ErrMissingParameter
        - ErrInsufficientAccess
        - ErrMissingResource
        - ErrInternal
      properties:
        name:
          {
            type: string,
            enum: [get_preview_invest],
            example: get_preview_invest,
          }
        payload: { $ref: "#/components/schemas/GetPreviewInvestInput" }
    GetProjectedFundPrice:
      type: object
      description: Retrieves the projected unit net asset value per unit (NAV per unit) for a specific fund class.
      x-errors:
        - ErrMissingParameter
        - ErrInsufficientAccess
        - ErrMissingResource
        - ErrInternal
      properties:
        name:
          {
            type: string,
            enum: [get_projected_fund_price],
            example: get_projected_fund_price,
          }
        payload: { $ref: "#/components/schemas/GetProjectedFundPriceInput" }

    CreateInvestmentRequest:
      type: object
      description: Submits a new investment request to purchase units in a specified fund class with the provided amount.
      x-errors:
        - ErrMissingParameter
        - ErrInvalidParameter
        - ErrActionOutsideFundHours
        - ErrMissingResource
        - ErrInternal
      properties:
        name:
          {
            type: string,
            enum: [create_investment_request],
            example: create_investment_request,
          }
        payload: { $ref: "#/components/schemas/CreateInvestmentRequestInput" }
    CreateRedemptionRequest:
      type: object
      description: Submits a new redemption request to sell fund units or withdraw an amount from an account.
      x-errors:
        - ErrMissingParameter
        - ErrInvalidParameter
        - ErrInsufficientAccess
        - ErrInsufficientBalance
        - ErrActionOutsideFundHours
        - ErrMissingResource
        - ErrInternal
      properties:
        name:
          {
            type: string,
            enum: [create_redemption_request],
            example: create_redemption_request,
          }
        payload: { $ref: "#/components/schemas/CreateRedemptionRequestInput" }
    CreateSwitchRequest:
      type: object
      description: Submits a request to transfer units from one fund to another within the same account.
      x-errors:
        - ErrMissingParameter
        - ErrInvalidParameter
        - ErrInsufficientAccess
        - ErrInsufficientBalance
        - ErrActionOutsideFundHours
        - ErrMissingResource
        - ErrInternal
      properties:
        name:
          {
            type: string,
            enum: [create_switch_request],
            example: create_switch_request,
          }
        payload: { $ref: "#/components/schemas/CreateSwitchRequestInput" }
    CreateRequestCancellation:
      type: object
      description: Cancels a pending transaction request (investment, redemption, or switch) before it is executed.
      x-errors:
        - ErrMissingParameter
        - ErrRequestCannotBeCancelled
        - ErrInsufficientAccess
        - ErrMissingResource
        - ErrInternal
      properties:
        name:
          {
            type: string,
            enum: [create_request_cancellation],
            example: create_request_cancellation,
          }
        payload: { $ref: "#/components/schemas/CreateRequestCancellationInput" }
    CreateSuitabilityAssessment:
      type: object
      description: Submits a new risk suitability assessment for the client, evaluating investment risk tolerance.
      x-errors:
        - ErrMissingParameter
        - ErrInsufficientAccess
        - ErrInvalidParameter
        - ErrInternal
      properties:
        name:
          {
            type: string,
            enum: [create_suitability_assessment],
            example: create_suitability_assessment,
          }
        payload:
          { $ref: "#/components/schemas/CreateSuitabilityAssessmentInput" }
    CreateClientBankAccount:
      type: object
      description: Registers a new bank account with the client's profile for receiving redemption proceeds.
      x-errors:
        - ErrMissingParameter
        - ErrInsufficientAccess
        - ErrInvalidParameter
        - ErrAlreadyExists
        - ErrInternal
      properties:
        name:
          {
            type: string,
            enum: [create_client_bank_account],
            example: create_client_bank_account,
          }
        payload: { $ref: "#/components/schemas/CreateClientBankAccountInput" }
    UpdateDisplayCurrency:
      type: object
      description: Changes the currency in which the client's portfolio values and transactions are displayed.
      x-errors:
        - ErrMissingParameter
        - ErrInsufficientAccess
        - ErrInvalidParameter
        - ErrInternal
      properties:
        name:
          {
            type: string,
            enum: [update_display_currency],
            example: update_display_currency,
          }
        payload: { $ref: "#/components/schemas/UpdateDisplayCurrencyInput" }
    UpdateAccountName:
      type: object
      description: Changes the friendly name or label of a specific client investment account.
      x-errors:
        - ErrMissingParameter
        - ErrInsufficientAccess
        - ErrInvalidParameter
        - ErrInternal
      properties:
        name:
          {
            type: string,
            enum: [update_account_name],
            example: update_account_name,
          }
        payload: { $ref: "#/components/schemas/UpdateAccountNameInput" }
    UpdateClientProfile:
      type: object
      description: Updates the client's demographic information, ethnicity, and tax residency details.
      x-errors:
        - ErrMissingParameter
        - ErrInsufficientAccess
        - ErrInvalidParameter
        - ErrInternal
      properties:
        name:
          {
            type: string,
            enum: [update_client_profile],
            example: update_client_profile,
          }
        payload: { $ref: "#/components/schemas/UpdateClientProfileInput" }

security:
  - BearerAuth: []

paths:
  # -------------------------------------------------------------------------
  # QUERY ENDPOINT
  # -------------------------------------------------------------------------
  /query:
    post:
      tags: [Query]
      summary: Execute Query
      description: |
        Endpoint for all read-only operations.

        **Action Selection:**
        Use the `oneOf` dropdown in the **Request Body** to select the specific action you wish to perform. The schema will update to show the required payload.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              oneOf:
                - $ref: "#/components/schemas/ListClientAccounts"
                - $ref: "#/components/schemas/GetClientProfile"
                - $ref: "#/components/schemas/GetFund"
                - $ref: "#/components/schemas/GetClientAccountAllocationPerformance"
                - $ref: "#/components/schemas/GetClientAccountStatement"
                - $ref: "#/components/schemas/GetClientAccountRequestConfirmation"
                - $ref: "#/components/schemas/GetClientReferral"
                - $ref: "#/components/schemas/GetClientAccountRequestPolicy"
                - $ref: "#/components/schemas/ListFundsForSubscription"
                - $ref: "#/components/schemas/ListClientAccountBalance"
                - $ref: "#/components/schemas/ListClientAccountRequests"
                - $ref: "#/components/schemas/ListClientBankAccounts"
                - $ref: "#/components/schemas/ListDisplayCurrencies"
                - $ref: "#/components/schemas/ListClientSuitabilityAssessments"
                - $ref: "#/components/schemas/ListInvestConsents"
                - $ref: "#/components/schemas/ListBanks"
                - $ref: "#/components/schemas/ListClientPromos"
                - $ref: "#/components/schemas/ListClientAccountPerformance"
                - $ref: "#/components/schemas/ListPaymentMethods"
                - $ref: "#/components/schemas/GetVoucher"
                - $ref: "#/components/schemas/GetPreviewInvest"
                - $ref: "#/components/schemas/GetProjectedFundPrice"
              discriminator:
                propertyName: name
                mapping:
                  list_client_accounts: "#/components/schemas/ListClientAccounts"
                  get_client_profile: "#/components/schemas/GetClientProfile"
                  get_fund: "#/components/schemas/GetFund"
                  get_client_account_allocation_performance: "#/components/schemas/GetClientAccountAllocationPerformance"
                  get_client_account_statement: "#/components/schemas/GetClientAccountStatement"
                  get_client_account_request_confirmation: "#/components/schemas/GetClientAccountRequestConfirmation"
                  get_client_referral: "#/components/schemas/GetClientReferral"
                  get_client_account_request_policy: "#/components/schemas/GetClientAccountRequestPolicy"
                  list_funds_for_subscription: "#/components/schemas/ListFundsForSubscription"
                  list_client_account_balance: "#/components/schemas/ListClientAccountBalance"
                  list_client_account_requests: "#/components/schemas/ListClientAccountRequests"
                  list_client_bank_accounts: "#/components/schemas/ListClientBankAccounts"
                  list_display_currencies: "#/components/schemas/ListDisplayCurrencies"
                  list_client_suitability_assessments: "#/components/schemas/ListClientSuitabilityAssessments"
                  list_invest_consents: "#/components/schemas/ListInvestConsents"
                  list_banks: "#/components/schemas/ListBanks"
                  list_client_promos: "#/components/schemas/ListClientPromos"
                  list_client_account_performance: "#/components/schemas/ListClientAccountPerformance"
                  list_payment_methods: "#/components/schemas/ListPaymentMethods"
                  get_voucher: "#/components/schemas/GetVoucher"
                  get_preview_invest: "#/components/schemas/GetPreviewInvest"
                  get_projected_fund_price: "#/components/schemas/GetProjectedFundPrice"
      responses:
        "200":
          description: Successful Query Response
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: "#/components/schemas/ListClientAccountsOutput"
                  - $ref: "#/components/schemas/GetClientProfileOutput"
                  - $ref: "#/components/schemas/GetFundOutput"
                  - $ref: "#/components/schemas/GetClientAccountAllocationPerformanceOutput"
                  - $ref: "#/components/schemas/GetClientAccountStatementOutput"
                  - $ref: "#/components/schemas/GetClientAccountRequestConfirmationOutput"
                  - $ref: "#/components/schemas/GetClientReferralOutput"
                  - $ref: "#/components/schemas/GetClientAccountRequestPolicyOutput"
                  - $ref: "#/components/schemas/ListFundsForSubscriptionOutput"
                  - $ref: "#/components/schemas/ListClientAccountBalanceOutput"
                  - $ref: "#/components/schemas/ListClientAccountRequestsOutput"
                  - $ref: "#/components/schemas/ListClientBankAccountsOutput"
                  - $ref: "#/components/schemas/ListDisplayCurrenciesOutput"
                  - $ref: "#/components/schemas/ListClientSuitabilityAssessmentsOutput"
                  - $ref: "#/components/schemas/ListInvestConsentsOutput"
                  - $ref: "#/components/schemas/ListBanksOutput"
                  - $ref: "#/components/schemas/ListClientPromosOutput"
                  - $ref: "#/components/schemas/ListClientAccountPerformanceOutput"
                  - $ref: "#/components/schemas/ListPaymentMethodsOutput"
                  - $ref: "#/components/schemas/GetVoucherOutput"
                  - $ref: "#/components/schemas/GetPreviewInvestOutput"
                  - $ref: "#/components/schemas/GetProjectedFundPriceOutput"
        "400":
          $ref: "#/components/responses/BadRequestError"
        "401":
          $ref: "#/components/responses/UnauthorizedError"
        "403":
          $ref: "#/components/responses/ForbiddenError"
        "404":
          $ref: "#/components/responses/NotFoundError"
        "410":
          $ref: "#/components/responses/GoneError"
        "412":
          $ref: "#/components/responses/PreconditionFailedError"
        "429":
          $ref: "#/components/responses/TooManyRequestsError"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/ServiceUnavailableError"

  # -------------------------------------------------------------------------
  # COMMAND ENDPOINT
  # -------------------------------------------------------------------------
  /command:
    post:
      tags:
        - Command
      summary: Execute Command
      description: |
        Single endpoint for all write/mutate operations.

        **Action Selection:**
        Use the `oneOf` dropdown in the **Request Body** to select the specific action you wish to perform.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              oneOf:
                - $ref: "#/components/schemas/CreateInvestmentRequest"
                - $ref: "#/components/schemas/CreateRedemptionRequest"
                - $ref: "#/components/schemas/CreateSwitchRequest"
                - $ref: "#/components/schemas/CreateRequestCancellation"
                - $ref: "#/components/schemas/CreateSuitabilityAssessment"
                - $ref: "#/components/schemas/CreateClientBankAccount"
                - $ref: "#/components/schemas/UpdateDisplayCurrency"
                - $ref: "#/components/schemas/UpdateAccountName"
                - $ref: "#/components/schemas/UpdateClientProfile"
              discriminator:
                propertyName: name
                mapping:
                  create_investment_request: "#/components/schemas/CreateInvestmentRequest"
                  create_redemption_request: "#/components/schemas/CreateRedemptionRequest"
                  create_switch_request: "#/components/schemas/CreateSwitchRequest"
                  create_request_cancellation: "#/components/schemas/CreateRequestCancellation"
                  create_suitability_assessment: "#/components/schemas/CreateSuitabilityAssessment"
                  create_client_bank_account: "#/components/schemas/CreateClientBankAccount"
                  update_display_currency: "#/components/schemas/UpdateDisplayCurrency"
                  update_account_name: "#/components/schemas/UpdateAccountName"
                  update_client_profile: "#/components/schemas/UpdateClientProfile"
      responses:
        "200":
          description: Successful Command Response
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: "#/components/schemas/CreateInvestmentRequestOutput"
                  - $ref: "#/components/schemas/CreateRedemptionRequestOutput"
                  - $ref: "#/components/schemas/CreateSwitchRequestOutput"
                  - $ref: "#/components/schemas/CreateRequestCancellationOutput"
                  - $ref: "#/components/schemas/CreateSuitabilityAssessmentOutput"
                  - $ref: "#/components/schemas/CreateClientBankAccountOutput"
                  - $ref: "#/components/schemas/UpdateDisplayCurrencyOutput"
                  - $ref: "#/components/schemas/UpdateAccountNameOutput"
                  - $ref: "#/components/schemas/UpdateClientProfileOutput"
        "400":
          $ref: "#/components/responses/BadRequestError"
        "401":
          $ref: "#/components/responses/UnauthorizedError"
        "403":
          $ref: "#/components/responses/ForbiddenError"
        "404":
          $ref: "#/components/responses/NotFoundError"
        "410":
          $ref: "#/components/responses/GoneError"
        "412":
          $ref: "#/components/responses/PreconditionFailedError"
        "429":
          $ref: "#/components/responses/TooManyRequestsError"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/ServiceUnavailableError"
