Skip to content

Commit 3101865

Browse files
authored
Merge pull request #2999 from DFXswiss/develop
Release: develop -> main
2 parents 1a3f9ee + 9c1f4f5 commit 3101865

3 files changed

Lines changed: 61 additions & 185 deletions

File tree

src/subdomains/core/history/controllers/transaction.controller.ts

Lines changed: 41 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ import { ChainReportCsvHistoryDto } from '../dto/output/chain-report-history.dto
7777
import { CoinTrackingCsvHistoryDto } from '../dto/output/coin-tracking-history.dto';
7878
import { RefundDataDto } from '../dto/refund-data.dto';
7979
import { TransactionFilter } from '../dto/transaction-filter.dto';
80-
import { BankRefundDto, TransactionRefundDto } from '../dto/transaction-refund.dto';
80+
import { TransactionRefundDto } from '../dto/transaction-refund.dto';
8181
import { TransactionDtoMapper } from '../mappers/transaction-dto.mapper';
8282
import { ExportType, HistoryService } from '../services/history.service';
8383

@@ -390,11 +390,7 @@ export class TransactionController {
390390
@Param('id') id: string,
391391
@Body() dto: TransactionRefundDto,
392392
): Promise<void> {
393-
return this.processRefund(+id, jwt, dto);
394-
}
395-
396-
private async processRefund(transactionId: number, jwt: JwtPayload, dto: TransactionRefundDto): Promise<void> {
397-
const transaction = await this.transactionService.getTransactionById(transactionId, {
393+
const transaction = await this.transactionService.getTransactionById(+id, {
398394
bankTxReturn: { bankTx: true, chargebackOutput: true },
399395
userData: true,
400396
refReward: true,
@@ -434,6 +430,44 @@ export class TransactionController {
434430
this.refundList.delete(transaction.id);
435431
}
436432

433+
@Put(':id/invoice')
434+
@ApiBearerAuth()
435+
@UseGuards(AuthGuard(), RoleGuard(UserRole.ACCOUNT), IpGuard, UserActiveGuard())
436+
@ApiOkResponse({ type: PdfDto })
437+
async generateInvoiceFromTransaction(@GetJwt() jwt: JwtPayload, @Param('id') id: string): Promise<PdfDto> {
438+
const txStatementDetails = await this.transactionHelper.getTxStatementDetails(
439+
jwt.account,
440+
+id,
441+
TxStatementType.INVOICE,
442+
);
443+
444+
if (!Config.invoice.currencies.includes(txStatementDetails.currency)) {
445+
throw new Error('PDF invoice is only available for CHF and EUR transactions');
446+
}
447+
448+
return { pdfData: await this.swissQrService.createTxStatement(txStatementDetails) };
449+
}
450+
451+
@Put(':id/receipt')
452+
@ApiBearerAuth()
453+
@UseGuards(AuthGuard(), RoleGuard(UserRole.ACCOUNT), IpGuard, UserActiveGuard())
454+
@ApiOkResponse({ type: PdfDto })
455+
async generateReceiptFromTransaction(@GetJwt() jwt: JwtPayload, @Param('id') id: string): Promise<PdfDto> {
456+
const txStatementDetails = await this.transactionHelper.getTxStatementDetails(
457+
jwt.account,
458+
+id,
459+
TxStatementType.RECEIPT,
460+
);
461+
462+
if (!Config.invoice.currencies.includes(txStatementDetails.currency)) {
463+
throw new Error('PDF receipt is only available for CHF and EUR transactions');
464+
}
465+
466+
return { pdfData: await this.swissQrService.createTxStatement(txStatementDetails) };
467+
}
468+
469+
// --- HELPER METHODS --- //
470+
437471
private async executeRefund(
438472
transaction: Transaction,
439473
targetEntity: BuyCrypto | BuyFiat | BankTxReturn | undefined,
@@ -446,7 +480,7 @@ export class TransactionController {
446480
const refundDto = { chargebackAmount: refundData.refundAmount, chargebackAllowedDateUser: new Date() };
447481

448482
if (!targetEntity) {
449-
transaction.bankTxReturn = await this.bankTxService
483+
targetEntity = await this.bankTxService
450484
.updateInternal(transaction.bankTx, { type: BankTxType.BANK_TX_RETURN })
451485
.then((b) => b.bankTxReturn);
452486
}
@@ -492,72 +526,6 @@ export class TransactionController {
492526
});
493527
}
494528

495-
// Deprecated - use PUT :id/refund with creditorData instead
496-
@Put(':id/refund/bank')
497-
@ApiBearerAuth()
498-
@UseGuards(
499-
AuthGuard(),
500-
RoleGuard(UserRole.ACCOUNT),
501-
UserActiveGuard([UserStatus.BLOCKED, UserStatus.DELETED], [UserDataStatus.BLOCKED]),
502-
)
503-
@ApiOkResponse()
504-
async setBankRefundTarget(
505-
@GetJwt() jwt: JwtPayload,
506-
@Param('id') id: string,
507-
@Body() dto: BankRefundDto,
508-
): Promise<void> {
509-
const refundDto: TransactionRefundDto = {
510-
refundTarget: dto.refundTarget,
511-
creditorData: {
512-
name: dto.name,
513-
address: dto.address,
514-
houseNumber: dto.houseNumber,
515-
zip: dto.zip,
516-
city: dto.city,
517-
country: dto.country,
518-
},
519-
};
520-
return this.processRefund(+id, jwt, refundDto);
521-
}
522-
523-
@Put(':id/invoice')
524-
@ApiBearerAuth()
525-
@UseGuards(AuthGuard(), RoleGuard(UserRole.ACCOUNT), IpGuard, UserActiveGuard())
526-
@ApiOkResponse({ type: PdfDto })
527-
async generateInvoiceFromTransaction(@GetJwt() jwt: JwtPayload, @Param('id') id: string): Promise<PdfDto> {
528-
const txStatementDetails = await this.transactionHelper.getTxStatementDetails(
529-
jwt.account,
530-
+id,
531-
TxStatementType.INVOICE,
532-
);
533-
534-
if (!Config.invoice.currencies.includes(txStatementDetails.currency)) {
535-
throw new Error('PDF invoice is only available for CHF and EUR transactions');
536-
}
537-
538-
return { pdfData: await this.swissQrService.createTxStatement(txStatementDetails) };
539-
}
540-
541-
@Put(':id/receipt')
542-
@ApiBearerAuth()
543-
@UseGuards(AuthGuard(), RoleGuard(UserRole.ACCOUNT), IpGuard, UserActiveGuard())
544-
@ApiOkResponse({ type: PdfDto })
545-
async generateReceiptFromTransaction(@GetJwt() jwt: JwtPayload, @Param('id') id: string): Promise<PdfDto> {
546-
const txStatementDetails = await this.transactionHelper.getTxStatementDetails(
547-
jwt.account,
548-
+id,
549-
TxStatementType.RECEIPT,
550-
);
551-
552-
if (!Config.invoice.currencies.includes(txStatementDetails.currency)) {
553-
throw new Error('PDF receipt is only available for CHF and EUR transactions');
554-
}
555-
556-
return { pdfData: await this.swissQrService.createTxStatement(txStatementDetails) };
557-
}
558-
559-
// --- HELPER METHODS --- //
560-
561529
private async getTransactionDto(
562530
tx: Transaction | TransactionRequest | undefined,
563531
detailed = false,

src/subdomains/supporting/fiat-output/__tests__/fiat-output-job.service.spec.ts

Lines changed: 18 additions & 112 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ import { BankTxReturnService } from '../../bank-tx/bank-tx-return/bank-tx-return
1919
import { createCustomBankTx } from '../../bank-tx/bank-tx/__mocks__/bank-tx.entity.mock';
2020
import { createCustomBank, yapealEUR } from '../../bank/bank/__mocks__/bank.entity.mock';
2121
import { BankService } from '../../bank/bank/bank.service';
22-
import { IbanBankName } from '../../bank/bank/dto/bank.dto';
2322
import { createCustomVirtualIban } from '../../bank/virtual-iban/__mocks__/virtual-iban.entity.mock';
2423
import { VirtualIbanService } from '../../bank/virtual-iban/virtual-iban.service';
2524
import { createCustomLog } from '../../log/__mocks__/log.entity.mock';
@@ -185,19 +184,19 @@ describe('FiatOutputJobService', () => {
185184
});
186185

187186
describe('setReadyDate', () => {
188-
it('should set ready date when balance is available and cryptoInput is confirmed', async () => {
187+
it('should set ready date for non-EUR transactions and skip EUR transactions', async () => {
189188
jest.spyOn(fiatOutputRepo, 'find').mockResolvedValue([
190189
createCustomFiatOutput({
191190
id: 1,
192-
accountIban: 'DE123456789',
191+
accountIban: 'CH123456789',
193192
iban: 'CH123456789',
194193
isReadyDate: null,
195194
buyFiats: [
196195
createCustomBuyFiat({
197196
cryptoInput: createCustomCryptoInput({ isConfirmed: true, asset: createDefaultAsset() }),
198197
}),
199198
],
200-
amount: 15000,
199+
amount: 100,
201200
currency: 'EUR',
202201
type: FiatOutputType.BUY_FIAT,
203202
}),
@@ -211,154 +210,61 @@ describe('FiatOutputJobService', () => {
211210
cryptoInput: createCustomCryptoInput({ isConfirmed: true, asset: createDefaultAsset() }),
212211
}),
213212
],
214-
amount: 5000,
213+
amount: 100,
215214
currency: 'CHF',
216215
type: FiatOutputType.BUY_FIAT,
217216
}),
218217
createCustomFiatOutput({
219218
id: 3,
220-
accountIban: 'DE123456789',
219+
accountIban: 'CH123456789',
221220
iban: 'CH123456789',
222221
isReadyDate: null,
223222
buyFiats: [
224223
createCustomBuyFiat({
225224
cryptoInput: createCustomCryptoInput({ isConfirmed: true, asset: createDefaultAsset() }),
226225
}),
227226
],
228-
amount: 100,
227+
amount: 200,
229228
currency: 'EUR',
230229
type: FiatOutputType.BUY_FIAT,
231230
}),
232231
createCustomFiatOutput({
233232
id: 4,
234-
accountIban: 'DE123456789',
235-
iban: 'CH123456789',
236-
isReadyDate: null,
237-
buyFiats: [
238-
createCustomBuyFiat({
239-
cryptoInput: createCustomCryptoInput({ isConfirmed: true, asset: createDefaultAsset() }),
240-
}),
241-
],
242-
amount: 300,
243-
currency: 'EUR',
244-
type: FiatOutputType.BUY_FIAT,
245-
}),
246-
createCustomFiatOutput({
247-
id: 5,
248-
accountIban: 'DE123456789',
249-
isReadyDate: null,
250-
amount: 9500,
251-
currency: 'EUR',
252-
type: FiatOutputType.LIQ_MANAGEMENT,
253-
}),
254-
createCustomFiatOutput({
255-
id: 6,
256-
accountIban: 'DE123456789',
257-
iban: 'DE123459876',
258-
isReadyDate: new Date(),
259-
buyFiats: [
260-
createCustomBuyFiat({
261-
cryptoInput: createCustomCryptoInput({ isConfirmed: true, asset: createDefaultAsset() }),
262-
}),
263-
],
264-
amount: 1200,
265-
currency: 'EUR',
266-
type: FiatOutputType.BUY_FIAT,
267-
}),
268-
createCustomFiatOutput({
269-
id: 7,
270-
accountIban: 'DE123456789',
271-
iban: 'CH123456789',
272-
isReadyDate: null,
273-
buyFiats: [
274-
createCustomBuyFiat({
275-
cryptoInput: createCustomCryptoInput({ isConfirmed: true, asset: createDefaultAsset() }),
276-
amountInChf: 4000,
277-
amountInEur: 4500,
278-
}),
279-
],
280-
amount: 20000,
281-
currency: 'AED',
282-
type: FiatOutputType.BUY_FIAT,
283-
}),
284-
createCustomFiatOutput({
285-
id: 8,
286-
accountIban: 'DE123456789',
233+
accountIban: 'CH123456789',
287234
iban: 'CH123456789',
288235
isReadyDate: null,
289-
buyFiats: [
290-
createCustomBuyFiat({
291-
cryptoInput: createCustomCryptoInput({ isConfirmed: true, asset: createDefaultAsset() }),
292-
amountInChf: 20000,
293-
amountInEur: 22000,
294-
}),
295-
],
296-
amount: 400,
297-
currency: 'XYZ',
298-
type: FiatOutputType.BUY_FIAT,
299-
}),
300-
createCustomFiatOutput({
301-
id: 9,
302-
accountIban: 'CH4243843938',
303-
iban: 'CH12345987645',
304-
isReadyDate: new Date(),
305-
buyFiats: [
306-
createCustomBuyFiat({
307-
cryptoInput: createCustomCryptoInput({ isConfirmed: true, asset: createDefaultAsset() }),
308-
}),
309-
],
310-
amount: 200,
311-
currency: 'EUR',
312-
type: FiatOutputType.BUY_FIAT,
313-
bank: createCustomBank({ name: IbanBankName.YAPEAL, iban: 'CH475843938' }),
314-
}),
315-
createCustomFiatOutput({
316-
id: 10,
317-
accountIban: 'CH4244043938',
318-
iban: 'CH12345987645',
319236
buyFiats: [
320237
createCustomBuyFiat({
321238
cryptoInput: createCustomCryptoInput({ isConfirmed: true, asset: createDefaultAsset() }),
322239
}),
323240
],
324-
amount: 100,
325-
currency: 'EUR',
241+
amount: 150,
242+
currency: 'USD',
326243
type: FiatOutputType.BUY_FIAT,
327-
bank: createCustomBank({ name: IbanBankName.YAPEAL, iban: 'CH475843938' }),
328244
}),
329245
]);
330246
jest.spyOn(assetService, 'getAssetsWith').mockResolvedValue([
331247
createCustomAsset({
332248
id: 1,
333249
type: AssetType.CUSTODY,
334-
bank: createCustomBank({ iban: 'DE123456789' }),
335-
name: 'EUR',
336-
balance: createCustomLiquidityBalance({ amount: 13000 }),
337-
}),
338-
createCustomAsset({
339-
id: 2,
340-
type: AssetType.CUSTODY,
341250
bank: createCustomBank({ iban: 'CH123456789' }),
342251
name: 'CHF',
343252
balance: createCustomLiquidityBalance({ amount: 9000 }),
344253
}),
345-
createCustomAsset({
346-
id: 3,
347-
type: AssetType.CUSTODY,
348-
bank: createCustomBank({ iban: 'CH475843938', name: IbanBankName.YAPEAL }),
349-
name: 'CHF',
350-
balance: createCustomLiquidityBalance({ amount: 500 }),
351-
}),
352254
]);
353255

354256
await service['setReadyDate']();
355257

356258
const updateCalls = (fiatOutputRepo.update as jest.Mock).mock.calls;
357-
expect(updateCalls[0][0]).toBe(3);
358-
expect(updateCalls[1][0]).toBe(4);
359-
expect(updateCalls[2][0]).toBe(7);
360-
expect(updateCalls[3][0]).toBe(2);
361-
expect(updateCalls[4][0]).toBe(10);
259+
const updatedIds = updateCalls.map((call) => call[0]);
260+
261+
// EUR transactions (id 1 and 3) should NOT be updated
262+
expect(updatedIds).not.toContain(1);
263+
expect(updatedIds).not.toContain(3);
264+
265+
// Non-EUR transactions (id 2 CHF and id 4 USD) should be updated
266+
expect(updatedIds).toContain(2);
267+
expect(updatedIds).toContain(4);
362268
});
363269
});
364270

src/subdomains/supporting/fiat-output/fiat-output-job.service.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,8 @@ export class FiatOutputJobService {
209209
const availableBalance =
210210
asset.balance.amount - pendingBalance - updatedFiatOutputAmount - Config.liquidityManagement.bankMinBalance;
211211

212+
if (entity.currency === 'EUR') continue;
213+
212214
if (availableBalance > entity.bankAmount) {
213215
updatedFiatOutputAmount += entity.bankAmount;
214216
const ibanCountry = entity.iban.substring(0, 2);

0 commit comments

Comments
 (0)