diff --git a/migration/1770791640000-FixBankTx189011MissingTxAmount.js b/migration/1770791640000-FixBankTx189011MissingTxAmount.js new file mode 100644 index 0000000000..a463fbde4d --- /dev/null +++ b/migration/1770791640000-FixBankTx189011MissingTxAmount.js @@ -0,0 +1,98 @@ +/** + * @typedef {import('typeorm').MigrationInterface} MigrationInterface + * @typedef {import('typeorm').QueryRunner} QueryRunner + */ + +/** + * Fix bank_tx 189011: Set missing txAmount and txCurrency. + * + * The Raiffeisen CAMT.053 XML has AmtDtls at Ntry level, not at NtryDtls.TxDtls level. + * The SEPA parser only reads from NtryDtls.TxDtls.AmtDtls, causing txAmount and txCurrency + * to be NULL. This prevents automatic BUY_CRYPTO assignment because createFromBankTx uses + * txAmount/txCurrency as inputAmount/inputAsset, and the NULL inputAsset causes a TypeError + * in getAndCompleteTxRequest (Cannot read properties of null reading 'id'). + * + * Fix: Copy the values from amount/currency (which were correctly parsed from NtryDtls.TxDtls.Amt). + * + * BankTx: 189011 + * Amount: 363000 EUR + * AccountServiceRef: CUSTOM/CH7780808002608614092/2026-02-10/Gutschrift Eucon Digital GmbH + * + * @class + * @implements {MigrationInterface} + */ +module.exports = class FixBankTx189011MissingTxAmount1770791640000 { + name = 'FixBankTx189011MissingTxAmount1770791640000'; + + /** + * @param {QueryRunner} queryRunner + */ + async up(queryRunner) { + const bankTxId = 189011; + + console.log('=== Fix BankTx 189011: Missing txAmount/txCurrency ===\n'); + + // Verify current state + const current = await queryRunner.query(` + SELECT id, amount, currency, txAmount, txCurrency, type + FROM dbo.bank_tx + WHERE id = ${bankTxId} + `); + + if (current.length === 0) { + console.log('ERROR: BankTx not found. Aborting.'); + return; + } + + const bt = current[0]; + console.log('Current state:'); + console.log(` ID: ${bt.id}`); + console.log(` amount: ${bt.amount}, currency: ${bt.currency}`); + console.log(` txAmount: ${bt.txAmount}, txCurrency: ${bt.txCurrency}`); + console.log(` type: ${bt.type}`); + console.log(''); + + if (bt.txAmount !== null) { + console.log('txAmount already set. Skipping.'); + return; + } + + // Update txAmount and txCurrency from amount/currency + console.log('Updating txAmount and txCurrency...'); + await queryRunner.query(` + UPDATE dbo.bank_tx + SET + txAmount = amount, + txCurrency = currency, + updated = GETDATE() + WHERE id = ${bankTxId} + `); + + // Verify final state + console.log('\n=== Verification ==='); + const final = await queryRunner.query(` + SELECT id, amount, currency, txAmount, txCurrency, type + FROM dbo.bank_tx + WHERE id = ${bankTxId} + `); + console.log('Final state:', JSON.stringify(final[0], null, 2)); + + console.log('\n=== Migration Complete ==='); + console.log('The next checkBankTx cron cycle (every 30s) should now automatically'); + console.log('assign type=BUY_CRYPTO and create the BuyCrypto entity.'); + } + + /** + * @param {QueryRunner} queryRunner + */ + async down(queryRunner) { + await queryRunner.query(` + UPDATE dbo.bank_tx + SET + txAmount = NULL, + txCurrency = NULL, + updated = GETDATE() + WHERE id = 189011 + `); + } +}; diff --git a/src/subdomains/core/history/controllers/transaction.controller.ts b/src/subdomains/core/history/controllers/transaction.controller.ts index 5e0eecb5f8..d8bcd0b5d1 100644 --- a/src/subdomains/core/history/controllers/transaction.controller.ts +++ b/src/subdomains/core/history/controllers/transaction.controller.ts @@ -528,6 +528,7 @@ export class TransactionController { const refundDto = { chargebackAmount: refundData.refundAmount, chargebackAllowedDateUser: new Date() }; if (!targetEntity) { + if (!dto.creditorData) throw new BadRequestException('Creditor data is required for bank refunds'); targetEntity = await this.bankTxService .updateInternal(transaction.bankTx, { type: BankTxType.BANK_TX_RETURN }) .then((b) => b.bankTxReturn); diff --git a/src/subdomains/core/history/mappers/transaction-dto.mapper.ts b/src/subdomains/core/history/mappers/transaction-dto.mapper.ts index 028ee87101..91baca9443 100644 --- a/src/subdomains/core/history/mappers/transaction-dto.mapper.ts +++ b/src/subdomains/core/history/mappers/transaction-dto.mapper.ts @@ -304,7 +304,9 @@ export class TransactionDtoMapper { ? TransactionState.RETURNED : bankTxReturn?.chargebackAllowedDateUser ? TransactionState.RETURN_PENDING - : TransactionState.UNASSIGNED, + : bankTxReturn?.id + ? TransactionState.FAILED + : TransactionState.UNASSIGNED, inputAmount: tx.txAmount, inputAsset: tx.txCurrency, inputAssetId: currency.id, diff --git a/src/subdomains/supporting/support-issue/services/support-issue-job.service.ts b/src/subdomains/supporting/support-issue/services/support-issue-job.service.ts index 5b872ef7ad..9f5ca4daa8 100644 --- a/src/subdomains/supporting/support-issue/services/support-issue-job.service.ts +++ b/src/subdomains/supporting/support-issue/services/support-issue-job.service.ts @@ -11,7 +11,7 @@ import { MailFactory } from '../../notification/factories/mail.factory'; import { TransactionRequestType } from '../../payment/entities/transaction-request.entity'; import { SupportMessageTranslationKey } from '../dto/support-issue.dto'; import { SupportIssue } from '../entities/support-issue.entity'; -import { AutoResponder } from '../entities/support-message.entity'; +import { AutoResponder, CustomerAuthor } from '../entities/support-message.entity'; import { SupportIssueInternalState, SupportIssueReason, SupportIssueType } from '../enums/support-issue.enum'; import { SupportIssueRepository } from '../repositories/support-issue.repository'; import { SupportIssueService } from './support-issue.service'; @@ -87,13 +87,16 @@ export class SupportIssueJobService { // --- HELPER METHODS --- // private async getAutoResponseIssues(where: FindOptionsWhere): Promise { - return this.supportIssueRepo.find({ - where: { - state: SupportIssueInternalState.CREATED, - messages: { author: Not(AutoResponder) }, - ...where, - }, - }); + return this.supportIssueRepo + .find({ + where: { + state: SupportIssueInternalState.CREATED, + messages: { author: Not(AutoResponder) }, + ...where, + }, + relations: { messages: true }, + }) + .then((issues) => issues.filter((i) => i.messages.at(-1).author === CustomerAuthor)); } private async sendAutoResponse(