diff --git a/src/subdomains/supporting/realunit/__tests__/realunit-dev.service.spec.ts b/src/subdomains/supporting/realunit/__tests__/realunit-dev.service.spec.ts deleted file mode 100644 index ee8b8bfa2b..0000000000 --- a/src/subdomains/supporting/realunit/__tests__/realunit-dev.service.spec.ts +++ /dev/null @@ -1,334 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { Blockchain } from 'src/integration/blockchain/shared/enums/blockchain.enum'; -import { createCustomAsset } from 'src/shared/models/asset/__mocks__/asset.entity.mock'; -import { AssetType } from 'src/shared/models/asset/asset.entity'; -import { FiatService } from 'src/shared/models/fiat/fiat.service'; -import { BuyCryptoRepository } from 'src/subdomains/core/buy-crypto/process/repositories/buy-crypto.repository'; -import { BuyService } from 'src/subdomains/core/buy-crypto/routes/buy/buy.service'; -import { BankTxService } from '../../bank-tx/bank-tx/services/bank-tx.service'; -import { BankService } from '../../bank/bank/bank.service'; -import { TransactionRequestStatus, TransactionRequestType } from '../../payment/entities/transaction-request.entity'; -import { TransactionRequestRepository } from '../../payment/repositories/transaction-request.repository'; -import { SpecialExternalAccountService } from '../../payment/services/special-external-account.service'; -import { TransactionService } from '../../payment/services/transaction.service'; -import { RealUnitDevService } from '../realunit-dev.service'; - -jest.mock('src/config/config', () => ({ - get Config() { - return { environment: 'loc' }; - }, - Environment: { - LOC: 'loc', - DEV: 'dev', - PRD: 'prd', - }, - GetConfig: jest.fn(() => ({ - blockchain: { - ethereum: { ethChainId: 1 }, - sepolia: { sepoliaChainId: 11155111 }, - arbitrum: { arbitrumChainId: 42161 }, - optimism: { optimismChainId: 10 }, - polygon: { polygonChainId: 137 }, - base: { baseChainId: 8453 }, - gnosis: { gnosisChainId: 100 }, - bsc: { bscChainId: 56 }, - citrea: { citreaChainId: 4114 }, - citreaTestnet: { citreaTestnetChainId: 5115 }, - }, - payment: { - fee: 0.01, - defaultPaymentTimeout: 900, - }, - formats: { - address: /.*/, - signature: /.*/, - key: /.*/, - ref: /.*/, - bankUsage: /.*/, - recommendationCode: /.*/, - kycHash: /.*/, - phone: /.*/, - accountServiceRef: /.*/, - number: /.*/, - transactionUid: /.*/, - }, - kyc: { - mandator: 'DFX', - prefix: 'DFX', - }, - defaults: { - language: 'EN', - currency: 'CHF', - }, - })), -})); - -// Mock DfxLogger -jest.mock('src/shared/services/dfx-logger', () => ({ - DfxLogger: jest.fn().mockImplementation(() => ({ - info: jest.fn(), - warn: jest.fn(), - error: jest.fn(), - })), -})); - -// Mock Util -jest.mock('src/shared/utils/util', () => ({ - Util: { - createUid: jest.fn().mockReturnValue('MOCK-UID'), - }, -})); - -describe('RealUnitDevService', () => { - let service: RealUnitDevService; - let transactionRequestRepo: jest.Mocked; - let fiatService: jest.Mocked; - let buyService: jest.Mocked; - let bankTxService: jest.Mocked; - let bankService: jest.Mocked; - let specialAccountService: jest.Mocked; - let transactionService: jest.Mocked; - let buyCryptoRepo: jest.Mocked; - - const sepoliaRealuAsset = createCustomAsset({ - id: 408, - name: 'REALU', - blockchain: Blockchain.SEPOLIA, - type: AssetType.TOKEN, - decimals: 0, - }); - - const mockFiat = { - id: 1, - name: 'CHF', - }; - - const mockBank = { - id: 1, - iban: 'CH1234567890', - }; - - const mockBuy = { - id: 1, - bankUsage: 'DFX123', - user: { - id: 1, - userData: { id: 1 }, - }, - }; - - const mockBankTx = { - id: 1, - transaction: { id: 1 }, - }; - - const mockTransactionRequest = { - id: 7, - amount: 100, - sourceId: 1, - targetId: 408, - routeId: 1, - status: TransactionRequestStatus.WAITING_FOR_PAYMENT, - type: TransactionRequestType.BUY, - }; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [ - RealUnitDevService, - { - provide: TransactionRequestRepository, - useValue: { - find: jest.fn(), - update: jest.fn(), - }, - }, - { - provide: FiatService, - useValue: { - getFiat: jest.fn(), - }, - }, - { - provide: BuyService, - useValue: { - getBuyByKey: jest.fn(), - }, - }, - { - provide: BankTxService, - useValue: { - create: jest.fn(), - getBankTxByKey: jest.fn(), - }, - }, - { - provide: BankService, - useValue: { - getBankInternal: jest.fn(), - }, - }, - { - provide: SpecialExternalAccountService, - useValue: { - getMultiAccounts: jest.fn(), - }, - }, - { - provide: TransactionService, - useValue: { - updateInternal: jest.fn(), - }, - }, - { - provide: BuyCryptoRepository, - useValue: { - create: jest.fn(), - save: jest.fn(), - }, - }, - ], - }).compile(); - - service = module.get(RealUnitDevService); - transactionRequestRepo = module.get(TransactionRequestRepository); - fiatService = module.get(FiatService); - buyService = module.get(BuyService); - bankTxService = module.get(BankTxService); - bankService = module.get(BankService); - specialAccountService = module.get(SpecialExternalAccountService); - transactionService = module.get(TransactionService); - buyCryptoRepo = module.get(BuyCryptoRepository); - }); - - afterEach(() => { - jest.clearAllMocks(); - }); - - describe('simulatePaymentForRequest', () => { - it('should skip if buy route not found', async () => { - buyService.getBuyByKey.mockResolvedValue(null); - - await service.simulatePaymentForRequest(mockTransactionRequest as any, sepoliaRealuAsset); - - expect(bankTxService.getBankTxByKey).not.toHaveBeenCalled(); - }); - - it('should skip if BankTx already exists (duplicate prevention)', async () => { - buyService.getBuyByKey.mockResolvedValue(mockBuy as any); - bankTxService.getBankTxByKey.mockResolvedValue({ id: 1 } as any); - - await service.simulatePaymentForRequest(mockTransactionRequest as any, sepoliaRealuAsset); - - expect(fiatService.getFiat).not.toHaveBeenCalled(); - }); - - it('should skip if fiat not found', async () => { - buyService.getBuyByKey.mockResolvedValue(mockBuy as any); - bankTxService.getBankTxByKey.mockResolvedValue(null); - fiatService.getFiat.mockResolvedValue(null); - - await service.simulatePaymentForRequest(mockTransactionRequest as any, sepoliaRealuAsset); - - expect(bankService.getBankInternal).not.toHaveBeenCalled(); - }); - - it('should skip if bank not found', async () => { - buyService.getBuyByKey.mockResolvedValue(mockBuy as any); - bankTxService.getBankTxByKey.mockResolvedValue(null); - fiatService.getFiat.mockResolvedValue(mockFiat as any); - bankService.getBankInternal.mockResolvedValue(null); - - await service.simulatePaymentForRequest(mockTransactionRequest as any, sepoliaRealuAsset); - - expect(bankTxService.create).not.toHaveBeenCalled(); - }); - - it('should use YAPEAL bank for CHF', async () => { - buyService.getBuyByKey.mockResolvedValue(mockBuy as any); - bankTxService.getBankTxByKey.mockResolvedValue(null); - fiatService.getFiat.mockResolvedValue({ id: 1, name: 'CHF' } as any); - bankService.getBankInternal.mockResolvedValue(null); - - await service.simulatePaymentForRequest(mockTransactionRequest as any, sepoliaRealuAsset); - - expect(bankService.getBankInternal).toHaveBeenCalledWith('Yapeal', 'CHF'); - }); - - it('should use OLKY bank for EUR', async () => { - buyService.getBuyByKey.mockResolvedValue(mockBuy as any); - bankTxService.getBankTxByKey.mockResolvedValue(null); - fiatService.getFiat.mockResolvedValue({ id: 2, name: 'EUR' } as any); - bankService.getBankInternal.mockResolvedValue(null); - - await service.simulatePaymentForRequest(mockTransactionRequest as any, sepoliaRealuAsset); - - expect(bankService.getBankInternal).toHaveBeenCalledWith('Olkypay', 'EUR'); - }); - - it('should create BankTx, BuyCrypto, update Transaction, and complete TransactionRequest', async () => { - buyService.getBuyByKey.mockResolvedValue(mockBuy as any); - bankTxService.getBankTxByKey.mockResolvedValue(null); - fiatService.getFiat.mockResolvedValue(mockFiat as any); - bankService.getBankInternal.mockResolvedValue(mockBank as any); - specialAccountService.getMultiAccounts.mockResolvedValue([]); - bankTxService.create.mockResolvedValue(mockBankTx as any); - buyCryptoRepo.create.mockReturnValue({ id: 1 } as any); - buyCryptoRepo.save.mockResolvedValue({ id: 1 } as any); - - await service.simulatePaymentForRequest(mockTransactionRequest as any, sepoliaRealuAsset); - - // 1. Should create BankTx - expect(bankTxService.create).toHaveBeenCalledWith( - expect.objectContaining({ - amount: 100, - currency: 'CHF', - remittanceInfo: 'DFX123', - txInfo: 'DEV simulation for TransactionRequest 7', - }), - [], - ); - - // 2. Should create BuyCrypto with Sepolia asset - expect(buyCryptoRepo.create).toHaveBeenCalledWith( - expect.objectContaining({ - inputAmount: 100, - inputAsset: 'CHF', - outputAsset: sepoliaRealuAsset, - amlCheck: 'Pass', - }), - ); - expect(buyCryptoRepo.save).toHaveBeenCalled(); - - // 3. Should update Transaction - expect(transactionService.updateInternal).toHaveBeenCalledWith( - { id: 1 }, - expect.objectContaining({ - type: 'BuyCrypto', - }), - ); - - // 4. Should complete TransactionRequest - expect(transactionRequestRepo.update).toHaveBeenCalledWith(7, { - isComplete: true, - status: TransactionRequestStatus.COMPLETED, - }); - }); - - it('should use unique txInfo per TransactionRequest for duplicate detection', async () => { - buyService.getBuyByKey.mockResolvedValue(mockBuy as any); - bankTxService.getBankTxByKey.mockResolvedValue(null); - fiatService.getFiat.mockResolvedValue(mockFiat as any); - bankService.getBankInternal.mockResolvedValue(mockBank as any); - specialAccountService.getMultiAccounts.mockResolvedValue([]); - bankTxService.create.mockResolvedValue(mockBankTx as any); - buyCryptoRepo.create.mockReturnValue({ id: 1 } as any); - buyCryptoRepo.save.mockResolvedValue({ id: 1 } as any); - - await service.simulatePaymentForRequest(mockTransactionRequest as any, sepoliaRealuAsset); - - // Should check for existing BankTx using txInfo field with TransactionRequest ID - expect(bankTxService.getBankTxByKey).toHaveBeenCalledWith('txInfo', 'DEV simulation for TransactionRequest 7'); - }); - }); -}); diff --git a/src/subdomains/supporting/realunit/__tests__/realunit.service.spec.ts b/src/subdomains/supporting/realunit/__tests__/realunit.service.spec.ts index 286d336d2c..ec4c108ba8 100644 --- a/src/subdomains/supporting/realunit/__tests__/realunit.service.spec.ts +++ b/src/subdomains/supporting/realunit/__tests__/realunit.service.spec.ts @@ -1,8 +1,8 @@ import { BadRequestException, ConflictException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; +import { RealUnitBlockchainService } from 'src/integration/blockchain/realunit/realunit-blockchain.service'; import { Blockchain } from 'src/integration/blockchain/shared/enums/blockchain.enum'; import { Eip7702DelegationService } from 'src/integration/blockchain/shared/evm/delegation/eip7702-delegation.service'; -import { RealUnitBlockchainService } from 'src/integration/blockchain/realunit/realunit-blockchain.service'; import { createCustomAsset } from 'src/shared/models/asset/__mocks__/asset.entity.mock'; import { AssetType } from 'src/shared/models/asset/asset.entity'; import { AssetService } from 'src/shared/models/asset/asset.service'; @@ -10,6 +10,7 @@ import { CountryService } from 'src/shared/models/country/country.service'; import { FiatService } from 'src/shared/models/fiat/fiat.service'; import { LanguageService } from 'src/shared/models/language/language.service'; import { HttpService } from 'src/shared/services/http.service'; +import { BuyCryptoRepository } from 'src/subdomains/core/buy-crypto/process/repositories/buy-crypto.repository'; import { BuyService } from 'src/subdomains/core/buy-crypto/routes/buy/buy.service'; import { SellService } from 'src/subdomains/core/sell-crypto/route/sell.service'; import { KycService } from 'src/subdomains/generic/kyc/services/kyc.service'; @@ -19,9 +20,11 @@ import { UserService } from 'src/subdomains/generic/user/models/user/user.servic import { SwissQRService } from 'src/subdomains/supporting/payment/services/swiss-qr.service'; import { TransactionRequestService } from 'src/subdomains/supporting/payment/services/transaction-request.service'; import { TransactionService } from 'src/subdomains/supporting/payment/services/transaction.service'; +import { BankTxService } from '../../bank-tx/bank-tx/services/bank-tx.service'; +import { BankService } from '../../bank/bank/bank.service'; +import { SpecialExternalAccountService } from '../../payment/services/special-external-account.service'; import { AssetPricesService } from '../../pricing/services/asset-prices.service'; import { PricingService } from '../../pricing/services/pricing.service'; -import { RealUnitDevService } from '../realunit-dev.service'; import { RealUnitService } from '../realunit.service'; jest.mock('src/config/config', () => ({ @@ -159,8 +162,33 @@ describe('RealUnitService', () => { }, { provide: TransactionService, useValue: {} }, { provide: AccountMergeService, useValue: {} }, - { provide: RealUnitDevService, useValue: {} }, { provide: SwissQRService, useValue: {} }, + { + provide: BuyCryptoRepository, + useValue: { + create: jest.fn(), + save: jest.fn(), + }, + }, + { + provide: BankTxService, + useValue: { + getBankTxByKey: jest.fn(), + create: jest.fn(), + }, + }, + { + provide: BankService, + useValue: { + getBankInternal: jest.fn(), + }, + }, + { + provide: SpecialExternalAccountService, + useValue: { + getMultiAccounts: jest.fn(), + }, + }, ], }).compile(); diff --git a/src/subdomains/supporting/realunit/realunit-dev.service.ts b/src/subdomains/supporting/realunit/realunit-dev.service.ts deleted file mode 100644 index 1e788182b6..0000000000 --- a/src/subdomains/supporting/realunit/realunit-dev.service.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { Asset } from 'src/shared/models/asset/asset.entity'; -import { FiatService } from 'src/shared/models/fiat/fiat.service'; -import { DfxLogger } from 'src/shared/services/dfx-logger'; -import { Util } from 'src/shared/utils/util'; -import { CheckStatus } from 'src/subdomains/core/aml/enums/check-status.enum'; -import { BuyCryptoRepository } from 'src/subdomains/core/buy-crypto/process/repositories/buy-crypto.repository'; -import { BuyService } from 'src/subdomains/core/buy-crypto/routes/buy/buy.service'; -import { BankTxIndicator } from '../bank-tx/bank-tx/entities/bank-tx.entity'; -import { BankTxService } from '../bank-tx/bank-tx/services/bank-tx.service'; -import { BankService } from '../bank/bank/bank.service'; -import { IbanBankName } from '../bank/bank/dto/bank.dto'; -import { TransactionRequest, TransactionRequestStatus } from '../payment/entities/transaction-request.entity'; -import { TransactionTypeInternal } from '../payment/entities/transaction.entity'; -import { TransactionRequestRepository } from '../payment/repositories/transaction-request.repository'; -import { SpecialExternalAccountService } from '../payment/services/special-external-account.service'; -import { TransactionService } from '../payment/services/transaction.service'; - -@Injectable() -export class RealUnitDevService { - private readonly logger = new DfxLogger(RealUnitDevService); - - constructor( - private readonly transactionRequestRepo: TransactionRequestRepository, - private readonly fiatService: FiatService, - private readonly buyService: BuyService, - private readonly bankTxService: BankTxService, - private readonly bankService: BankService, - private readonly specialAccountService: SpecialExternalAccountService, - private readonly transactionService: TransactionService, - private readonly buyCryptoRepo: BuyCryptoRepository, - ) {} - - async simulatePaymentForRequest(request: TransactionRequest, sepoliaRealuAsset: Asset): Promise { - // Get Buy route with user relation - const buy = await this.buyService.getBuyByKey('id', request.routeId); - if (!buy) { - this.logger.warn(`Buy route ${request.routeId} not found for TransactionRequest ${request.id}`); - return; - } - - // Check if this TransactionRequest was already processed (prevent duplicate simulation) - // We use the txInfo field to track which TransactionRequest a simulated BankTx belongs to - const simulationMarker = `DEV simulation for TransactionRequest ${request.id}`; - const existingBankTx = await this.bankTxService.getBankTxByKey('txInfo', simulationMarker); - if (existingBankTx) { - return; - } - - // Get source currency - const fiat = await this.fiatService.getFiat(request.sourceId); - if (!fiat) { - this.logger.warn(`Fiat ${request.sourceId} not found for TransactionRequest ${request.id}`); - return; - } - - // Get bank - const bankName = fiat.name === 'CHF' ? IbanBankName.YAPEAL : IbanBankName.OLKY; - const bank = await this.bankService.getBankInternal(bankName, fiat.name); - if (!bank) { - this.logger.warn(`Bank ${bankName} for ${fiat.name} not found - skipping simulation`); - return; - } - - // 1. Create BankTx - const accountServiceRef = `DEV-SIM-${Util.createUid('SIM')}-${Date.now()}`; - const multiAccounts = await this.specialAccountService.getMultiAccounts(); - - const bankTx = await this.bankTxService.create( - { - accountServiceRef, - bookingDate: new Date(), - valueDate: new Date(), - amount: request.amount, - txAmount: request.amount, - currency: fiat.name, - txCurrency: fiat.name, - creditDebitIndicator: BankTxIndicator.CREDIT, - remittanceInfo: buy.bankUsage, - iban: 'CH0000000000000000000', - name: 'DEV SIMULATION', - accountIban: bank.iban, - txInfo: `DEV simulation for TransactionRequest ${request.id}`, - }, - multiAccounts, - ); - - // 2. Create BuyCrypto with amlCheck: PASS - // Use Sepolia REALU asset for payout (not request.targetId which points to Mainnet) - const buyCrypto = this.buyCryptoRepo.create({ - bankTx: { id: bankTx.id } as any, - buy, - inputAmount: request.amount, - inputAsset: fiat.name, - inputReferenceAmount: request.amount, - inputReferenceAsset: fiat.name, - outputAsset: sepoliaRealuAsset, - outputReferenceAsset: sepoliaRealuAsset, - amlCheck: CheckStatus.PASS, - priceDefinitionAllowedDate: new Date(), - transaction: { id: bankTx.transaction.id } as any, - }); - - await this.buyCryptoRepo.save(buyCrypto); - - // 3. Update Transaction type - await this.transactionService.updateInternal(bankTx.transaction, { - type: TransactionTypeInternal.BUY_CRYPTO, - user: buy.user, - userData: buy.user.userData, - }); - - // 4. Complete TransactionRequest - await this.transactionRequestRepo.update(request.id, { - isComplete: true, - status: TransactionRequestStatus.COMPLETED, - }); - - this.logger.info( - `DEV simulation complete for TransactionRequest ${request.id}: ${request.amount} ${fiat.name} -> REALU (BuyCrypto created with amlCheck: PASS)`, - ); - } -} diff --git a/src/subdomains/supporting/realunit/realunit.module.ts b/src/subdomains/supporting/realunit/realunit.module.ts index b6b5e5e0da..22fab08e90 100644 --- a/src/subdomains/supporting/realunit/realunit.module.ts +++ b/src/subdomains/supporting/realunit/realunit.module.ts @@ -13,7 +13,6 @@ import { PaymentModule } from '../payment/payment.module'; import { TransactionModule } from '../payment/transaction.module'; import { PricingModule } from '../pricing/pricing.module'; import { RealUnitController } from './controllers/realunit.controller'; -import { RealUnitDevService } from './realunit-dev.service'; import { RealUnitService } from './realunit.service'; @Module({ @@ -33,7 +32,7 @@ import { RealUnitService } from './realunit.service'; forwardRef(() => SellCryptoModule), ], controllers: [RealUnitController], - providers: [RealUnitService, RealUnitDevService], + providers: [RealUnitService], exports: [RealUnitService], }) export class RealUnitModule {} diff --git a/src/subdomains/supporting/realunit/realunit.service.ts b/src/subdomains/supporting/realunit/realunit.service.ts index 2f0f270d09..b6b3f78643 100644 --- a/src/subdomains/supporting/realunit/realunit.service.ts +++ b/src/subdomains/supporting/realunit/realunit.service.ts @@ -29,21 +29,35 @@ import { HttpService } from 'src/shared/services/http.service'; import { AsyncCache, CacheItemResetPeriod } from 'src/shared/utils/async-cache'; import { PdfUtil } from 'src/shared/utils/pdf.util'; import { Util } from 'src/shared/utils/util'; +import { CheckStatus } from 'src/subdomains/core/aml/enums/check-status.enum'; +import { BuyCryptoRepository } from 'src/subdomains/core/buy-crypto/process/repositories/buy-crypto.repository'; +import { Buy } from 'src/subdomains/core/buy-crypto/routes/buy/buy.entity'; import { BuyService } from 'src/subdomains/core/buy-crypto/routes/buy/buy.service'; import { SellService } from 'src/subdomains/core/sell-crypto/route/sell.service'; import { KycStep } from 'src/subdomains/generic/kyc/entities/kyc-step.entity'; import { KycStepName } from 'src/subdomains/generic/kyc/enums/kyc-step-name.enum'; import { ReviewStatus } from 'src/subdomains/generic/kyc/enums/review-status.enum'; import { KycService } from 'src/subdomains/generic/kyc/services/kyc.service'; -import { AccountMergeService } from 'src/subdomains/generic/user/models/account-merge/account-merge.service'; import { AccountType } from 'src/subdomains/generic/user/models/user-data/account-type.enum'; import { UserData } from 'src/subdomains/generic/user/models/user-data/user-data.entity'; import { KycLevel } from 'src/subdomains/generic/user/models/user-data/user-data.enum'; import { UserDataService } from 'src/subdomains/generic/user/models/user-data/user-data.service'; import { User } from 'src/subdomains/generic/user/models/user/user.entity'; import { UserService } from 'src/subdomains/generic/user/models/user/user.service'; +import { BankTxIndicator } from 'src/subdomains/supporting/bank-tx/bank-tx/entities/bank-tx.entity'; +import { BankTxService } from 'src/subdomains/supporting/bank-tx/bank-tx/services/bank-tx.service'; +import { BankService } from 'src/subdomains/supporting/bank/bank/bank.service'; +import { IbanBankName } from 'src/subdomains/supporting/bank/bank/dto/bank.dto'; import { FiatPaymentMethod } from 'src/subdomains/supporting/payment/dto/payment-method.enum'; -import { TransactionRequestStatus } from 'src/subdomains/supporting/payment/entities/transaction-request.entity'; +import { + TransactionRequest, + TransactionRequestStatus, +} from 'src/subdomains/supporting/payment/entities/transaction-request.entity'; +import { + TransactionSourceType, + TransactionTypeInternal, +} from 'src/subdomains/supporting/payment/entities/transaction.entity'; +import { SpecialExternalAccountService } from 'src/subdomains/supporting/payment/services/special-external-account.service'; import { SwissQRService } from 'src/subdomains/supporting/payment/services/swiss-qr.service'; import { TransactionRequestService } from 'src/subdomains/supporting/payment/services/transaction-request.service'; import { TransactionService } from 'src/subdomains/supporting/payment/services/transaction.service'; @@ -81,7 +95,6 @@ import { TokenInfoDto, } from './dto/realunit.dto'; import { KycLevelRequiredException, RegistrationRequiredException } from './exceptions/buy-exceptions'; -import { RealUnitDevService } from './realunit-dev.service'; import { getAccountHistoryQuery, getAccountSummaryQuery, getHoldersQuery, getTokenInfoQuery } from './utils/queries'; import { TimeseriesUtils } from './utils/timeseries-utils'; @@ -116,9 +129,11 @@ export class RealUnitService { private readonly eip7702DelegationService: Eip7702DelegationService, private readonly transactionRequestService: TransactionRequestService, private readonly transactionService: TransactionService, - private readonly accountMergeService: AccountMergeService, - private readonly devService: RealUnitDevService, private readonly swissQrService: SwissQRService, + private readonly buyCryptoRepo: BuyCryptoRepository, + private readonly bankTxService: BankTxService, + private readonly bankService: BankService, + private readonly specialAccountService: SpecialExternalAccountService, ) { this.ponderUrl = GetConfig().blockchain.realunit.graphUrl; } @@ -896,22 +911,36 @@ export class RealUnitService { // --- Admin Methods --- async confirmPaymentReceived(requestId: number): Promise { - const request = await this.transactionRequestService.getTransactionRequest(requestId, { user: true }); + const request = await this.transactionRequestService.getTransactionRequest(requestId, { + user: { userData: true }, + }); if (!request) throw new NotFoundException('Transaction request not found'); if (request.status !== TransactionRequestStatus.WAITING_FOR_PAYMENT) { throw new BadRequestException('Transaction request is not in WaitingForPayment status'); } + const buy = await this.buyService.getBuyByKey('id', request.routeId); + if (!buy) { + this.logger.warn(`Buy route ${request.routeId} not found for TransactionRequest ${request.id}`); + return; + } + const fiat = await this.fiatService.getFiat(request.sourceId); + if (!fiat) { + this.logger.warn(`Fiat ${request.sourceId} not found for TransactionRequest ${request.id}`); + return; + } + const realuAsset = await this.getRealuAsset(); + if ([Environment.DEV, Environment.LOC].includes(Config.environment)) { - const realuAsset = await this.getRealuAsset(); - await this.devService.simulatePaymentForRequest(request, realuAsset); + // Simulate payment with BankTx and create transaction records + await this.createTransactionWithBankTx(request, buy, fiat.name, realuAsset); } else { + // Call Aktionariat API first, then create transaction records const aktionariatResponse = JSON.parse(request.aktionariatResponse); const reference = aktionariatResponse.reference; if (!reference) throw new BadRequestException('No reference found in aktionariat response'); // Convert amount to CHF Rappen for Aktionariat API - const fiat = await this.fiatService.getFiat(request.sourceId); let amountChf = request.amount; if (fiat.name !== 'CHF') { const price = await this.pricingService.getPrice(fiat, PriceCurrency.CHF, PriceValidity.ANY); @@ -922,10 +951,106 @@ export class RealUnitService { amount: Math.round(amountChf * 100), ref: reference, }); - await this.transactionRequestService.complete(request.id); + + // Create transaction records (without BankTx since payment went to RealUnit's bank) + await this.createTransactionWithoutBankTx(request, buy, fiat.name, realuAsset); } } + private async createTransactionWithBankTx( + request: TransactionRequest, + buy: Buy, + fiatName: string, + realuAsset: Asset, + ): Promise { + const simulationMarker = `DEV simulation payment for TransactionRequest ${request.id}`; + const existingBankTx = await this.bankTxService.getBankTxByKey('txInfo', simulationMarker); + if (existingBankTx) return; + const bankName = fiatName === 'CHF' ? IbanBankName.YAPEAL : IbanBankName.OLKY; + const bank = await this.bankService.getBankInternal(bankName, fiatName); + if (!bank) { + this.logger.warn(`Bank ${bankName} for ${fiatName} not found - skipping`); + return; + } + const accountServiceRef = `DEV-SIM-${Util.createUid('SIM')}-${Date.now()}`; + const multiAccounts = await this.specialAccountService.getMultiAccounts(); + + const bankTx = await this.bankTxService.create( + { + accountServiceRef, + bookingDate: new Date(), + valueDate: new Date(), + amount: request.amount, + txAmount: request.amount, + currency: fiatName, + txCurrency: fiatName, + creditDebitIndicator: BankTxIndicator.CREDIT, + remittanceInfo: buy.bankUsage, + iban: 'CH0000000000000000000', + name: 'DEV SIMULATION', + accountIban: bank.iban, + txInfo: `DEV simulation for TransactionRequest ${request.id}`, + }, + multiAccounts, + ); + await this.createBuyCryptoAndComplete(request, buy, fiatName, realuAsset, bankTx.transaction.id, bankTx.id); + await this.transactionService.updateInternal(bankTx.transaction, { + type: TransactionTypeInternal.BUY_CRYPTO, + user: buy.user, + userData: buy.user.userData, + request, + }); + this.logger.info( + `DEV payment confirmed for TransactionRequest ${request.id}: ${request.amount} ${fiatName} -> REALU (Transaction ${bankTx.transaction.id} created with BankTx)`, + ); + } + + private async createTransactionWithoutBankTx( + request: TransactionRequest, + buy: Buy, + fiatName: string, + realuAsset: Asset, + ): Promise { + const transaction = await this.transactionService.create({ + sourceType: TransactionSourceType.BANK_TX, + type: TransactionTypeInternal.BUY_CRYPTO, + user: buy.user, + userData: buy.user.userData, + }); + await this.transactionService.updateInternal(transaction, { request }); + await this.createBuyCryptoAndComplete(request, buy, fiatName, realuAsset, transaction.id); + this.logger.info( + `Payment confirmed for TransactionRequest ${request.id}: ${request.amount} ${fiatName} -> REALU (Transaction ${transaction.id} created)`, + ); + } + + private async createBuyCryptoAndComplete( + request: TransactionRequest, + buy: Buy, + fiatName: string, + realuAsset: Asset, + transactionId: number, + bankTxId?: number, + ): Promise { + const buyCrypto = this.buyCryptoRepo.create({ + ...(bankTxId && { bankTx: { id: bankTxId } as any }), + buy, + inputAmount: request.amount, + inputAsset: fiatName, + inputReferenceAmount: request.amount, + inputReferenceAsset: fiatName, + outputAsset: realuAsset, + outputReferenceAsset: realuAsset, + amlCheck: CheckStatus.PASS, + priceDefinitionAllowedDate: new Date(), + transaction: { id: transactionId } as any, + }); + + await this.buyCryptoRepo.save(buyCrypto); + + await this.transactionRequestService.complete(request.id); + } + async getAdminQuotes(limit = 50, offset = 0): Promise { const realuAsset = await this.getRealuAsset(); const requests = await this.transactionRequestService.getByAssetId(realuAsset.id, limit, offset);