From 7f35f0a655fb9ef4b6f7693e6f4dee4465bdf1c2 Mon Sep 17 00:00:00 2001 From: Dread Date: Fri, 5 Jun 2026 20:52:13 -0700 Subject: [PATCH] feat(bridge): push notification on deposit settlement [ENG-275] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fires a best-effort push when a Bridge USDT deposit settles via the IBEX crypto.received webhook (the must-have launch gate). Mirrors the existing sendBridgeWithdrawalNotification pattern: - new src/app/bridge/send-deposit-notification.ts - wired at the crypto-receive settlement success (idempotent — inside the per-txHash lock; fires once) - notification.bridgeDeposit i18n phrases (en + es) Withdrawal-completion push already exists (transfer.ts) — this completes the deposit side. IBEX→USD currency display mirrors the withdrawal notif. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/app/bridge/send-deposit-notification.ts | 98 +++++++++++++++++++ src/config/locales/en.json | 4 + src/config/locales/es.json | 4 + .../webhook-server/routes/crypto-receive.ts | 7 ++ 4 files changed, 113 insertions(+) create mode 100644 src/app/bridge/send-deposit-notification.ts diff --git a/src/app/bridge/send-deposit-notification.ts b/src/app/bridge/send-deposit-notification.ts new file mode 100644 index 000000000..9845f676a --- /dev/null +++ b/src/app/bridge/send-deposit-notification.ts @@ -0,0 +1,98 @@ +import { getI18nInstance } from "@config" +import { checkedToAccountId } from "@domain/accounts" +import { getLanguageOrDefault } from "@domain/locale" +import { + DeviceTokensNotRegisteredNotificationsServiceError, + FlashNotificationCategories, + NotificationsServiceError, +} from "@domain/notifications" +import { removeDeviceTokens } from "@app/users/remove-device-tokens" +import { baseLogger } from "@services/logger" +import { AccountsRepository } from "@services/mongoose/accounts" +import { UsersRepository } from "@services/mongoose/users" +import { + PushNotificationsService, + SendFilteredPushNotificationStatus, +} from "@services/notifications/push-notifications" + +const i18n = getI18nInstance() + +const formatDepositAmount = (amount: string, currency: string): string => + `${amount} ${currency.toUpperCase()}` + +export const sendBridgeDepositNotification = async ({ + accountId: accountIdRaw, + amount, + currency, +}: { + accountId: string + amount: string + currency: string +}): Promise => { + const accountId = checkedToAccountId(accountIdRaw) + if (accountId instanceof Error) return accountId + + const account = await AccountsRepository().findById(accountId) + if (account instanceof Error) return account + + const user = await UsersRepository().findById(account.kratosUserId) + if (user instanceof Error) return user + + const locale = getLanguageOrDefault(user.language) + const formattedAmount = formatDepositAmount(amount, currency) + const phraseBase = "notification.bridgeDeposit" + + const title = i18n.__({ phrase: `${phraseBase}.title`, locale }) + const body = i18n.__( + { phrase: `${phraseBase}.body`, locale }, + { amount: formattedAmount }, + ) + + const result = await PushNotificationsService().sendFilteredNotification({ + deviceTokens: user.deviceTokens, + title, + body, + notificationCategory: FlashNotificationCategories.Payments, + notificationSettings: account.notificationSettings, + data: { + type: "bridge_deposit_completed", + amount, + currency: currency == "usdt" ? "USD" : currency.toUpperCase(), + }, + }) + + if (result instanceof NotificationsServiceError) return result + + if (result.status === SendFilteredPushNotificationStatus.Filtered) { + return true + } + + return true +} + +export const sendBridgeDepositNotificationBestEffort = async ( + args: Parameters[0], +): Promise => { + const result = await sendBridgeDepositNotification(args) + + if (result instanceof DeviceTokensNotRegisteredNotificationsServiceError) { + const accountId = checkedToAccountId(args.accountId) + if (accountId instanceof Error) return + + const account = await AccountsRepository().findById(accountId) + if (account instanceof Error) return + + await removeDeviceTokens({ + userId: account.kratosUserId, + deviceTokens: result.tokens, + }) + return + } + + if (result instanceof Error) { + baseLogger.warn( + { accountId: args.accountId, error: result }, + "Failed to send Bridge deposit push notification", + ) + } +} diff --git a/src/config/locales/en.json b/src/config/locales/en.json index 55eafc82d..da4398833 100644 --- a/src/config/locales/en.json +++ b/src/config/locales/en.json @@ -41,6 +41,10 @@ "body": "Your cashout of {{amount}} has been deposited to your bank account.", "title": "Cashout" }, + "bridgeDeposit": { + "body": "Your deposit of {{amount}} has been added to your account.", + "title": "Deposit received" + }, "bridgeWithdrawal": { "completed": { "body": "Your withdrawal of {{amount}} has been sent to your bank account.", diff --git a/src/config/locales/es.json b/src/config/locales/es.json index 7325c9fe0..aa4cef58f 100644 --- a/src/config/locales/es.json +++ b/src/config/locales/es.json @@ -37,6 +37,10 @@ "title": "Transacción {{walletCurrency}}" } }, + "bridgeDeposit": { + "body": "Su depósito de {{amount}} se agregó a su cuenta.", + "title": "Depósito recibido" + }, "bridgeWithdrawal": { "completed": { "body": "Su retiro de {{amount}} se envió a su cuenta bancaria.", diff --git a/src/services/ibex/webhook-server/routes/crypto-receive.ts b/src/services/ibex/webhook-server/routes/crypto-receive.ts index cb5e46666..7a45d129c 100644 --- a/src/services/ibex/webhook-server/routes/crypto-receive.ts +++ b/src/services/ibex/webhook-server/routes/crypto-receive.ts @@ -2,6 +2,7 @@ import express, { Request, Response } from "express" import { AccountsRepository } from "@services/mongoose/accounts" import { createIbexCryptoReceive } from "@services/mongoose/ibex-crypto-receive-log" import { listWalletsByAccountId } from "@app/wallets" +import { sendBridgeDepositNotificationBestEffort } from "@app/bridge/send-deposit-notification" import { WalletCurrency, USDTAmount } from "@domain/shared" import { baseLogger } from "@services/logger" import { LockService } from "@services/lock" @@ -125,6 +126,12 @@ const cryptoReceiveHandler = async (req: Request, res: Response) => { return { status: "error", code: "erpnext_audit_failed" } as CryptoReceiveResult } + await sendBridgeDepositNotificationBestEffort({ + accountId: account.id, + amount: String(usdtAmount.asNumber()), + currency: normalizedCurrency, + }) + return { status: "success" } as CryptoReceiveResult } catch (error) { baseLogger.error({ error, tx_hash }, "Error processing crypto receive webhook")