From 2d6dceea43383023b760d8f436601c45e426420b Mon Sep 17 00:00:00 2001 From: Vandana Date: Fri, 5 Jun 2026 21:17:39 -0700 Subject: [PATCH] feat(bridge): add KYC tier ceiling error code (ENG-354) --- docs/bridge-integration/API.md | 1 + src/graphql/error-map.ts | 7 ++++ src/services/bridge/errors.ts | 41 ++++++++++++++++++- .../unit/graphql/bridge-error-map.spec.ts | 40 ++++++++++++++++++ 4 files changed, 87 insertions(+), 2 deletions(-) diff --git a/docs/bridge-integration/API.md b/docs/bridge-integration/API.md index 7628b9b90..32c0afbdc 100644 --- a/docs/bridge-integration/API.md +++ b/docs/bridge-integration/API.md @@ -199,6 +199,7 @@ query BridgeWithdrawals { | `BRIDGE_KYC_PENDING` | Operation requires approved KYC, but it is still pending. | | `BRIDGE_KYC_REJECTED` | KYC was rejected. | | `BRIDGE_KYC_OFFBOARDED` | Bridge offboarded the customer. | +| `BRIDGE_KYC_TIER_CEILING_EXCEEDED` | Withdrawal amount exceeds the KYC tier ceiling. | | `BRIDGE_CUSTOMER_NOT_FOUND` | Bridge customer record not found for the user. | | `BRIDGE_INSUFFICIENT_FUNDS` | USDT balance is insufficient for the withdrawal. | | `BRIDGE_RATE_LIMIT` | Bridge rate-limited the request. | diff --git a/src/graphql/error-map.ts b/src/graphql/error-map.ts index 6dce737f1..024fb52a5 100644 --- a/src/graphql/error-map.ts +++ b/src/graphql/error-map.ts @@ -541,6 +541,13 @@ export const mapError = (error: ApplicationError): CustomApolloError => { message, }) + case "BridgeKycTierCeilingExceededError": + message = error.message || "Withdrawal amount exceeds the KYC tier ceiling" + return bridgeGqlError({ + code: "BRIDGE_KYC_TIER_CEILING_EXCEEDED", + message, + }) + case "BridgeCustomerNotFoundError": message = "Bridge customer not found" return bridgeGqlError({ diff --git a/src/services/bridge/errors.ts b/src/services/bridge/errors.ts index b0b6178fc..5c0eabb68 100644 --- a/src/services/bridge/errors.ts +++ b/src/services/bridge/errors.ts @@ -46,7 +46,9 @@ export class BridgeKycRejectedError extends BridgeError { } export class BridgeKycOffboardedError extends BridgeError { - constructor(message: string = "Your account has been offboarded from Bridge. Please contact support.") { + constructor( + message: string = "Your account has been offboarded from Bridge. Please contact support.", + ) { super(message) } } @@ -58,7 +60,9 @@ export class BridgeInsufficientFundsError extends BridgeError { } export class BridgeAccountLevelError extends BridgeError { - constructor(message: string = "Bridge requires at least a Personal account (Level 1+)") { + constructor( + message: string = "Bridge requires at least a Personal account (Level 1+)", + ) { super(message) } } @@ -95,13 +99,46 @@ export class BridgeWebhookValidationError extends BridgeError { } } +export class BridgeKycTierCeilingExceededError extends BridgeError { + constructor(message: string = "Withdrawal amount exceeds the KYC tier ceiling") { + super(message) + } +} + /** * Maps HTTP status codes from Bridge API to domain error types + * + * Checks the response body for specific Bridge error types when applicable. */ export const mapBridgeHttpError = ( statusCode: number, response?: unknown, ): BridgeError => { + // Bridge returns 422/400 with a specific error type for KYC tier ceiling violations. + if ( + (statusCode === 422 || statusCode === 400) && + typeof response === "object" && + response !== null + ) { + const resp = response as Record + const errorObj = (resp.error ?? resp) as Record | undefined + const errorType = String(errorObj?.type ?? "").toLowerCase() + const errorMessage = String(errorObj?.message ?? resp?.message ?? "").toLowerCase() + + if ( + errorType.includes("kyc_tier_limit") || + errorType.includes("kyc_limit") || + errorType.includes("tier_ceiling") || + (errorMessage.includes("kyc") && + (errorMessage.includes("limit") || + errorMessage.includes("ceiling") || + errorMessage.includes("tier"))) + ) { + const message = typeof resp.message === "string" ? resp.message : undefined + return new BridgeKycTierCeilingExceededError(message) + } + } + switch (statusCode) { case 404: return new BridgeCustomerNotFoundError() diff --git a/test/flash/unit/graphql/bridge-error-map.spec.ts b/test/flash/unit/graphql/bridge-error-map.spec.ts index 194a83b39..0157443b6 100644 --- a/test/flash/unit/graphql/bridge-error-map.spec.ts +++ b/test/flash/unit/graphql/bridge-error-map.spec.ts @@ -11,10 +11,12 @@ import { BridgeKycOffboardedError, BridgeKycPendingError, BridgeKycRejectedError, + BridgeKycTierCeilingExceededError, BridgeRateLimitError, BridgeTimeoutError, BridgeTransferFailedError, BridgeWebhookValidationError, + mapBridgeHttpError, } from "@services/bridge/errors" describe("error-map: Bridge errors", () => { @@ -32,6 +34,7 @@ describe("error-map: Bridge errors", () => { [new BridgeTimeoutError(), "BRIDGE_TIMEOUT"], [new BridgeTransferFailedError(), "BRIDGE_TRANSFER_FAILED"], [new BridgeWebhookValidationError(), "BRIDGE_WEBHOOK_VALIDATION"], + [new BridgeKycTierCeilingExceededError(), "BRIDGE_KYC_TIER_CEILING_EXCEEDED"], [new BridgeApiError("Bridge API error", 500), "BRIDGE_API_ERROR"], [new BridgeError("Bridge unavailable"), "BRIDGE_ERROR"], ] @@ -52,3 +55,40 @@ describe("error-map: Bridge errors", () => { expect(result.message).toBeTruthy() }) }) + +describe("error-map: mapBridgeHttpError KYC tier ceiling detection", () => { + it("detects KYC tier ceiling via error.type", () => { + const result = mapBridgeHttpError(422, { + error: { type: "kyc_tier_limit_exceeded", message: "KYC tier limit reached" }, + }) + expect(result).toBeInstanceOf(BridgeKycTierCeilingExceededError) + }) + + it("detects KYC tier ceiling via error.type kyc_limit", () => { + const result = mapBridgeHttpError(400, { + error: { type: "kyc_limit_exceeded", message: "KYC limit exceeded" }, + }) + expect(result).toBeInstanceOf(BridgeKycTierCeilingExceededError) + }) + + it("detects KYC tier ceiling via response.message", () => { + const result = mapBridgeHttpError(422, { + message: "exceeds kyc ceiling", + }) + expect(result).toBeInstanceOf(BridgeKycTierCeilingExceededError) + }) + + it("does not detect on unrelated 422 errors", () => { + const result = mapBridgeHttpError(422, { + error: { type: "validation_error", message: "Invalid amount" }, + }) + expect(result).not.toBeInstanceOf(BridgeKycTierCeilingExceededError) + }) + + it("does not detect on non-422/400 errors", () => { + const result = mapBridgeHttpError(500, { + error: { type: "kyc_tier_limit_exceeded", message: "KYC tier limit" }, + }) + expect(result).not.toBeInstanceOf(BridgeKycTierCeilingExceededError) + }) +})