@cfxlabsinc/b2b-services
    Preparing search index...

    Root of the identity class hierarchy: it owns the one identity-search cache every identity surface shares, the single seam that busts it, and the serialization that defines the cached shape.

    Every reader and writer of identity extends this classIdentityQueryService (consumer read), IdentityService (consumer writer), IdentityAdminQueryService (admin read) and IdentityAdminService (admin writer, whose AiPrise verification callbacks mutate the table outside IdentityService entirely). A writer that forgets to bust is then a visible omission in one place rather than a silently stale cache, and a class that both reads and writes busts the instance it reads through — which is what keeps read-after-write correct inside a single request.

    serializeIdentity / deserializeIdentity are static so that Next.js can call them without a Valkey client: the dashboards need exactly this projection to cross the RSC → client boundary, and before this class existed each of them carried its own copy to drift against.

    Hierarchy (View Summary)

    Index
    cache: ServiceCache<CachedSearchPage<CachedIdentity>>
    • Bust every cached page — consumer or admin — that a write touching customerIds could have affected.

      The collection tag is always cleared alongside, because an unscoped admin page carries only that tag and no customer id can reach it.

      keys deletes exact entries in addition to the tag surgery, passing the key arguments — this cache hashes and prefixes them. It exists because invalidateTag resolves keys through the FT index, which does not exist in memory mode (valkeyClient: null); there the tag bust is a total no-op, and an exact-key delete is the only thing that keeps read-after-write honest. Services that folded get onto search pass that get's arguments.

      These deletes only reach this instance's own key space, because the key space is partitioned per surface. That is sufficient, and a writer must not try to name the other surface's key:

      • With Valkey, the other surface's get entry is itself tagged (by customer id, or by the collection tag when unscoped), so the tag bust above already evicts it from the shared L2 and publishes the peer eviction.
      • Without Valkey, the two surfaces are separate instances with separate L1 maps, so deleting a key from this instance could never have affected the other one anyway.

      Parameters

      • __namedParameters: { customerIds: readonly string[]; keys?: readonly CacheKey[] }

      Returns Promise<void>

    • Tags to write on a page, derived from the scope the query searched — never from the customers present in the result.

      That distinction is load-bearing. An entry tagged with the customer ids in the page carries no tags when the page is empty, so nothing can ever bust it and a later create leaves it stale for the full L2 TTL. Tagging by scope means an empty page still carries the tag for what it searched.

      An unscoped read falls back to the collection tag, since no customer-id tag can reach a row whose owner did not exist when the entry was written.

      Parameters

      • __namedParameters: { customerIds: readonly string[] | undefined }

      Returns string[]

    • The inverse of serializeIdentity: rehydrates the Date fields.

      Parameters

      Returns {
          achName: string;
          amlVerification?: {
              id: string;
              status: "REVIEW" | "PENDING" | "APPROVED" | "DECLINED";
          };
          capabilities: (
              | "TRANSACT"
              | "MANAGE_PAYMENT_INSTRUMENT"
              | "MANAGE_LEDGER_ACCOUNT"
              | "MANAGE_DEPOSIT_MEMO"
          )[];
          countryCode: string;
          countryOfResidence: string;
          createdAt: Date;
          customerVerification?: { id: string };
          dateOfBirth?: string;
          deletedAt?: Date;
          email: string;
          fullName: string;
          id: string;
          identityDocumentVerification?: {
              id: string;
              status: "REVIEW" | "PENDING" | "APPROVED" | "DECLINED";
          };
          name: { firstName: string; lastName: string; middleName?: string };
          phone: string;
          referenceId?: string;
          status:
              | "ACTIVE"
              | "DELETED"
              | "BLOCKED"
              | "DISABLED"
              | "PENDING_VERIFICATION"
              | "REVIEW";
          taxId?: string;
          type: "IDENTITY";
          updatedAt: Date;
          uuid: string;
          verificationProfileId?: string;
          wireName: string;
      }

      • achName: string
      • OptionalamlVerification?: { id: string; status: "REVIEW" | "PENDING" | "APPROVED" | "DECLINED" }

        Anti-money laundering verification

        Retrieve the other details directly from AiPrise

      • capabilities: (
            | "TRANSACT"
            | "MANAGE_PAYMENT_INSTRUMENT"
            | "MANAGE_LEDGER_ACCOUNT"
            | "MANAGE_DEPOSIT_MEMO"
        )[]

        What this identity is capable of executing currently

      • countryCode: string

        Country of jurisdiction; alias of countryOfResidence

      • countryOfResidence: string
      • createdAt: Date
      • OptionalcustomerVerification?: { id: string }

        Details of customer's own verification of the identity

      • OptionaldateOfBirth?: string

        In ISO format: yyyy-MM-dd

      • OptionaldeletedAt?: Date
      • email: string
      • fullName: string
      • id: string
      • OptionalidentityDocumentVerification?: { id: string; status: "REVIEW" | "PENDING" | "APPROVED" | "DECLINED" }

        Identity documents (passport, gov ID, etc.) verification

        Retrieve the other details directly from AiPrise

      • name: { firstName: string; lastName: string; middleName?: string }
      • phone: string
      • OptionalreferenceId?: string
      • status:
            | "ACTIVE"
            | "DELETED"
            | "BLOCKED"
            | "DISABLED"
            | "PENDING_VERIFICATION"
            | "REVIEW"
      • OptionaltaxId?: string

        The person's own tax identification number — an SSN or ITIN for a US person, the local equivalent elsewhere. Stored as entered.

        Mirrors related_person.data.taxId: the two are the same fact about the same kind of party, and both forward to AiPrise the same way (additional_info: TAX_IDENTIFICATION_NUMBER on the identity-document verification run — AiPrise's user profile has no field for it).

      • type: "IDENTITY"

        Discriminator shared with Organization for mixed entity lists

      • updatedAt: Date
      • uuid: string
      • OptionalverificationProfileId?: string

        AiPrise user profile ID

        Only set if we verify the identity.

      • wireName: string
    • The exact cache key IdentityQueryService.get reads through.

      get folds onto search, so its entry is an ordinary single-row search page — these args must stay identical to the ones get passes ({ customerId, ids: [id], pageSize: 1 }, with search's page default of 1). Every other search filter is undefined there, and serviceCacheKey canonicalizes with JCS, which drops undefined properties — so naming only the four that are set produces the same digest. identityCache.test.ts pins that equivalence.

      Writers pass this to SearchCache.invalidateCache's keys so read-after-write holds in memory mode, where tag invalidation no-ops. The cache hashes and surface-prefixes it; this returns the arguments only.

      Parameters

      • __namedParameters: { customerId: string; id: string }

      Returns Record<string, unknown>

    • Identity → its JSON-safe form. Safe to call without a cache instance.

      Parameters

      • identity: {
            achName: string;
            amlVerification?: {
                id: string;
                status: "REVIEW" | "PENDING" | "APPROVED" | "DECLINED";
            };
            capabilities: (
                | "TRANSACT"
                | "MANAGE_PAYMENT_INSTRUMENT"
                | "MANAGE_LEDGER_ACCOUNT"
                | "MANAGE_DEPOSIT_MEMO"
            )[];
            countryCode: string;
            countryOfResidence: string;
            createdAt: Date;
            customerVerification?: { id: string };
            dateOfBirth?: string;
            deletedAt?: Date;
            email: string;
            fullName: string;
            id: string;
            identityDocumentVerification?: {
                id: string;
                status: "REVIEW" | "PENDING" | "APPROVED" | "DECLINED";
            };
            name: { firstName: string; lastName: string; middleName?: string };
            phone: string;
            referenceId?: string;
            status:
                | "ACTIVE"
                | "DELETED"
                | "BLOCKED"
                | "DISABLED"
                | "PENDING_VERIFICATION"
                | "REVIEW";
            taxId?: string;
            type: "IDENTITY";
            updatedAt: Date;
            uuid: string;
            verificationProfileId?: string;
            wireName: string;
        }
        • achName: string
        • OptionalamlVerification?: { id: string; status: "REVIEW" | "PENDING" | "APPROVED" | "DECLINED" }

          Anti-money laundering verification

          Retrieve the other details directly from AiPrise

        • capabilities: (
              | "TRANSACT"
              | "MANAGE_PAYMENT_INSTRUMENT"
              | "MANAGE_LEDGER_ACCOUNT"
              | "MANAGE_DEPOSIT_MEMO"
          )[]

          What this identity is capable of executing currently

        • countryCode: string

          Country of jurisdiction; alias of countryOfResidence

        • countryOfResidence: string
        • createdAt: Date
        • OptionalcustomerVerification?: { id: string }

          Details of customer's own verification of the identity

        • OptionaldateOfBirth?: string

          In ISO format: yyyy-MM-dd

        • OptionaldeletedAt?: Date
        • email: string
        • fullName: string
        • id: string
        • OptionalidentityDocumentVerification?: { id: string; status: "REVIEW" | "PENDING" | "APPROVED" | "DECLINED" }

          Identity documents (passport, gov ID, etc.) verification

          Retrieve the other details directly from AiPrise

        • name: { firstName: string; lastName: string; middleName?: string }
        • phone: string
        • OptionalreferenceId?: string
        • status:
              | "ACTIVE"
              | "DELETED"
              | "BLOCKED"
              | "DISABLED"
              | "PENDING_VERIFICATION"
              | "REVIEW"
        • OptionaltaxId?: string

          The person's own tax identification number — an SSN or ITIN for a US person, the local equivalent elsewhere. Stored as entered.

          Mirrors related_person.data.taxId: the two are the same fact about the same kind of party, and both forward to AiPrise the same way (additional_info: TAX_IDENTIFICATION_NUMBER on the identity-document verification run — AiPrise's user profile has no field for it).

        • type: "IDENTITY"

          Discriminator shared with Organization for mixed entity lists

        • updatedAt: Date
        • uuid: string
        • OptionalverificationProfileId?: string

          AiPrise user profile ID

          Only set if we verify the identity.

        • wireName: string

      Returns CachedIdentity