From ea78e61f5750f45b1ca3d0c43893ca326c9e5efa Mon Sep 17 00:00:00 2001 From: TaprootFreak <142087526+TaprootFreak@users.noreply.github.com> Date: Tue, 13 Jan 2026 17:52:47 +0100 Subject: [PATCH] fix: trigger crypto return flow when chargebackAllowedDate is set via admin update (#2922) * fix: trigger crypto return flow when chargebackAllowedDate is set via admin update When an admin sets chargebackAllowedDate directly via the BuyFiat update endpoint, the CryptoInput was not being prepared for return. This left transactions stuck in Completed status with no way to execute the refund. - Save cryptoInput and chargebackAllowedDate before save operation - Add triggerCryptoInputReturn() method to handle return flow - Call PayoutService for FORWARD_CONFIRMED status (funds on liquidity) - Call PayInService for COMPLETED status (funds on deposit address) The fix ensures that admin-initiated refunds properly trigger the return flow, regardless of whether refundBuyFiatInternal() was called. * fix: add safety guards for return flow edge cases - Skip if return already in progress (TO_RETURN, RETURNED, RETURN_CONFIRMED) - Skip if returnTxId already set - Skip if action is FORWARD (coins being forwarded, not on deposit) - Import PayInAction enum * fix: add null check for cryptoInput.asset in return flow * fix: load cryptoInput.route.user relation for returnPayIn returnPayIn() requires route.user to update the transaction. Without this relation loaded, the transaction user would be set to undefined. * chore: cleanup * fix: fixed return check --------- Co-authored-by: David May --- .../process/services/buy-fiat.service.ts | 60 ++++++++++++++----- 1 file changed, 44 insertions(+), 16 deletions(-) diff --git a/src/subdomains/core/sell-crypto/process/services/buy-fiat.service.ts b/src/subdomains/core/sell-crypto/process/services/buy-fiat.service.ts index ccbbeaeca4..58c0c900cf 100644 --- a/src/subdomains/core/sell-crypto/process/services/buy-fiat.service.ts +++ b/src/subdomains/core/sell-crypto/process/services/buy-fiat.service.ts @@ -14,7 +14,7 @@ import { CreateBankDataDto } from 'src/subdomains/generic/user/models/bank-data/ import { UserService } from 'src/subdomains/generic/user/models/user/user.service'; import { WebhookService } from 'src/subdomains/generic/user/services/webhook/webhook.service'; import { BankTxService } from 'src/subdomains/supporting/bank-tx/bank-tx/services/bank-tx.service'; -import { CryptoInput, PayInStatus } from 'src/subdomains/supporting/payin/entities/crypto-input.entity'; +import { CryptoInput, PayInAction, PayInStatus } from 'src/subdomains/supporting/payin/entities/crypto-input.entity'; import { PayInService } from 'src/subdomains/supporting/payin/services/payin.service'; import { TransactionRequest } from 'src/subdomains/supporting/payment/entities/transaction-request.entity'; import { TransactionTypeInternal } from 'src/subdomains/supporting/payment/entities/transaction.entity'; @@ -135,7 +135,7 @@ export class BuyFiatService { sell: true, fiatOutput: true, bankTx: true, - cryptoInput: true, + cryptoInput: { route: { user: true }, transaction: true }, transaction: { user: { wallet: true }, userData: true }, bankData: true, }, @@ -144,6 +144,8 @@ export class BuyFiatService { const sellIdBefore = entity.sell?.id; const usedRefBefore = entity.usedRef; + const chargebackAllowedDateBefore = entity.chargebackAllowedDate; + const cryptoInputBefore = entity.cryptoInput; const update = this.buyFiatRepo.create(dto); @@ -212,9 +214,48 @@ export class BuyFiatService { if (dto.amountInChf) await this.updateSellVolume([sellIdBefore, entity.sell?.id]); if (dto.usedRef || dto.amountInEur) await this.updateRefVolume([usedRefBefore, entity.usedRef]); + // Trigger return flow when chargebackAllowedDate is newly set via admin update + if (dto.chargebackAllowedDate && !chargebackAllowedDateBefore) { + await this.triggerBuyFiatReturn(entity, cryptoInputBefore); + } + return entity; } + private async triggerBuyFiatReturn(buyFiat: BuyFiat, cryptoInput: CryptoInput): Promise { + const { chargebackAddress, chargebackAmount } = buyFiat; + + if (!chargebackAddress || !chargebackAmount || !cryptoInput?.asset) return; + + // Skip if return already in progress or completed + if ( + [PayInStatus.TO_RETURN, PayInStatus.RETURNED, PayInStatus.RETURN_CONFIRMED].includes(cryptoInput.status) || + cryptoInput.returnTxId + ) + return; + + await this.returnCrypto(buyFiat, cryptoInput, chargebackAddress, chargebackAmount); + } + + private async returnCrypto( + buyFiat: BuyFiat, + cryptoInput: CryptoInput, + returnAddress: string, + amount: number, + ): Promise { + if (cryptoInput.status === PayInStatus.FORWARD_CONFIRMED) { + await this.payoutService.doPayout({ + context: PayoutOrderContext.BUY_FIAT_RETURN, + correlationId: `${buyFiat.id}`, + asset: cryptoInput.asset, + amount, + destinationAddress: returnAddress, + }); + } else if (cryptoInput.action !== PayInAction.FORWARD) { + await this.payInService.returnPayIn(cryptoInput, returnAddress, amount); + } + } + async getBuyFiatByKey(key: string, value: any, onlyDefaultRelation = false): Promise { const query = this.buyFiatRepo .createQueryBuilder('buyFiat') @@ -306,20 +347,7 @@ export class BuyFiatService { blockchainFee = await this.transactionHelper.getBlockchainFee(buyFiat.cryptoInput.asset, true); const returnAddress = refundUser.address ?? buyFiat.chargebackAddress; - - if (buyFiat.cryptoInput.status === PayInStatus.FORWARD_CONFIRMED) { - // Funds already forwarded to liquidity - use PayoutOrder to return - await this.payoutService.doPayout({ - context: PayoutOrderContext.BUY_FIAT_RETURN, - correlationId: `${buyFiat.id}`, - asset: buyFiat.cryptoInput.asset, - amount: chargebackAmount, - destinationAddress: returnAddress, - }); - } else { - // Funds still on deposit address - use PayIn return - await this.payInService.returnPayIn(buyFiat.cryptoInput, returnAddress, chargebackAmount); - } + await this.returnCrypto(buyFiat, buyFiat.cryptoInput, returnAddress, chargebackAmount); } await this.buyFiatRepo.update(