Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/bridge-integration/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. |
Expand Down
7 changes: 7 additions & 0 deletions src/graphql/error-map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
41 changes: 39 additions & 2 deletions src/services/bridge/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
Expand All @@ -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)
}
}
Expand Down Expand Up @@ -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<string, unknown>
const errorObj = (resp.error ?? resp) as Record<string, unknown> | 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()
Expand Down
40 changes: 40 additions & 0 deletions test/flash/unit/graphql/bridge-error-map.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,12 @@ import {
BridgeKycOffboardedError,
BridgeKycPendingError,
BridgeKycRejectedError,
BridgeKycTierCeilingExceededError,
BridgeRateLimitError,
BridgeTimeoutError,
BridgeTransferFailedError,
BridgeWebhookValidationError,
mapBridgeHttpError,
} from "@services/bridge/errors"

describe("error-map: Bridge errors", () => {
Expand All @@ -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"],
]
Expand All @@ -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)
})
})