Skip to content

Commit f3719c9

Browse files
authored
Merge pull request #3411 from DFXswiss/develop
Release: develop -> main
2 parents 14c31f7 + 1bc492c commit f3719c9

12 files changed

Lines changed: 109 additions & 50 deletions

File tree

src/subdomains/core/buy-crypto/process/services/buy-crypto-preparation.service.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -487,7 +487,12 @@ export class BuyCryptoPreparationService {
487487
const chargebackAllowedBy = 'API';
488488

489489
if (entity.bankTx) {
490-
await this.buyCryptoService.refundBankTx(entity, { chargebackAllowedDate, chargebackAllowedBy });
490+
if (
491+
Util.includesSameName(entity.userData.verifiedName, entity.creditorData.name) ||
492+
Util.includesSameName(entity.userData.completeName, entity.creditorData.name) ||
493+
(!entity.userData.verifiedName && !entity.userData.completeName)
494+
)
495+
await this.buyCryptoService.refundBankTx(entity, { chargebackAllowedDate, chargebackAllowedBy });
491496
} else if (entity.cryptoInput) {
492497
await this.buyCryptoService.refundCryptoInput(entity, { chargebackAllowedDate, chargebackAllowedBy });
493498
} else {

src/subdomains/core/buy-crypto/process/services/buy-crypto.service.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -559,7 +559,7 @@ export class BuyCryptoService {
559559
)
560560
throw new BadRequestException('IBAN not valid or BIC not available');
561561

562-
const creditorData = dto.creditorData ?? buyCrypto.creditorData;
562+
const creditorData = buyCrypto.creditorData ?? dto.creditorData;
563563
if ((dto.chargebackAllowedDate || dto.chargebackAllowedDateUser) && !creditorData)
564564
throw new BadRequestException('Creditor data is required for chargeback');
565565

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

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -435,6 +435,8 @@ export class TransactionController {
435435
const refundData = this.refundList.get(transaction.id);
436436
if (!refundData) throw new BadRequestException('Request refund data first');
437437
if (!this.isRefundDataValid(refundData)) throw new BadRequestException('Refund data request invalid');
438+
if (refundData.refundTarget && dto.refundTarget)
439+
throw new BadRequestException('RefundTarget is already set with refundData');
438440

439441
await this.executeRefund(transaction, transaction.targetEntity, refundData, dto);
440442

@@ -597,7 +599,7 @@ export class TransactionController {
597599
if (!dto.creditorData) throw new BadRequestException('Creditor data is required for bank refunds');
598600

599601
return this.buyCryptoService.refundBankTx(targetEntity, {
600-
refundIban: dto.refundTarget ?? refundData.refundTarget,
602+
refundIban: refundData.refundTarget ?? dto.refundTarget,
601603
creditorData: dto.creditorData,
602604
chargebackReferenceAmount: refundData.refundPrice.invert().convert(refundData.refundAmount),
603605
...refundDto,
@@ -615,19 +617,18 @@ export class TransactionController {
615617
private async getRefundTarget(transaction: Transaction): Promise<string | undefined> {
616618
if (transaction.refundTargetEntity instanceof BuyFiat) return transaction.refundTargetEntity.chargebackAddress;
617619

618-
// For bank transactions, always return the original IBAN - refund must go to the sender
619-
if (transaction.bankTx?.iban) return transaction.bankTx.iban;
620-
621-
// For BuyCrypto with checkout (card), return masked card number
622-
if (transaction.refundTargetEntity instanceof BuyCrypto && transaction.refundTargetEntity.checkoutTx)
623-
return `${transaction.refundTargetEntity.checkoutTx.cardBin}****${transaction.refundTargetEntity.checkoutTx.cardLast4}`;
624-
625-
// For other cases, return existing chargeback IBAN
626-
if (transaction.refundTargetEntity instanceof BankTx) return transaction.bankTx?.iban;
627-
if (transaction.refundTargetEntity instanceof BuyCrypto) return transaction.refundTargetEntity.chargebackIban;
628-
if (transaction.refundTargetEntity instanceof BankTxReturn) return transaction.refundTargetEntity.chargebackIban;
620+
try {
621+
if (transaction.bankTx && (await this.validateIban(transaction.bankTx.iban))) return transaction.bankTx.iban;
622+
} catch (_) {
623+
return transaction.refundTargetEntity instanceof BankTx
624+
? undefined
625+
: transaction.refundTargetEntity?.chargebackIban;
626+
}
629627

630-
return undefined;
628+
if (transaction.refundTargetEntity instanceof BuyCrypto)
629+
return transaction.refundTargetEntity.checkoutTx
630+
? `${transaction.refundTargetEntity.checkoutTx.cardBin}****${transaction.refundTargetEntity.checkoutTx.cardLast4}`
631+
: transaction.refundTargetEntity.chargebackIban;
631632
}
632633

633634
private async validateIban(iban: string): Promise<boolean> {

src/subdomains/core/monitoring/observers/exchange.observer.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ export class ExchangeObserver extends MetricObserver<ExchangeData[]> {
7474
const xtDeurBtcPrice = await this.pricingService.getPriceFrom(PriceSource.XT, 'BTC', 'DEURO');
7575
const xtDepsUsdtPrice = await this.pricingService.getPriceFrom(PriceSource.XT, 'USDT', 'DEPS');
7676
const xtDepsBtcPrice = await this.pricingService.getPriceFrom(PriceSource.XT, 'BTC', 'DEPS');
77+
const xtJusdUsdtPrice = await this.pricingService.getPriceFrom(PriceSource.XT, 'USDT', 'JUSD');
7778

7879
const referenceDeurUsdtPrice = await this.pricingService.getPrice(
7980
usdt,
@@ -83,6 +84,7 @@ export class ExchangeObserver extends MetricObserver<ExchangeData[]> {
8384
const referenceDeurBtcPrice = await this.pricingService.getPrice(btc, PriceCurrency.EUR, PriceValidity.VALID_ONLY);
8485
const referenceDepsUsdtPrice = await this.pricingService.getPriceFrom(PriceSource.DEURO, 'USDT', 'DEPS');
8586
const referenceDepsBtcPrice = await this.pricingService.getPriceFrom(PriceSource.DEURO, 'BTC', 'DEPS');
87+
const referenceJusdUsdtPrice = await this.pricingService.getPriceFrom(PriceSource.JUICE, 'USDT', 'JUSD');
8688

8789
return [
8890
{
@@ -101,6 +103,10 @@ export class ExchangeObserver extends MetricObserver<ExchangeData[]> {
101103
name: 'XT-DEPS-BTC',
102104
deviation: Util.round(xtDepsBtcPrice.price / referenceDepsBtcPrice.price - 1, 3),
103105
},
106+
{
107+
name: 'XT-JUSD-USDT',
108+
deviation: Util.round(xtJusdUsdtPrice.price / referenceJusdUsdtPrice.price - 1, 3),
109+
},
104110
];
105111
}
106112

src/subdomains/generic/kyc/services/kyc.service.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,7 @@ export class KycService {
237237
KycError.NATIONALITY_NOT_MATCHING,
238238
KycError.IP_COUNTRY_MISMATCH,
239239
KycError.COUNTRY_IP_COUNTRY_MISMATCH,
240+
KycError.RESIDENCE_PERMIT_CHECK_REQUIRED,
240241
].includes(e),
241242
)
242243
)

src/subdomains/generic/user/models/auth/auth.service.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import { MailKey, MailTranslationKey } from 'src/subdomains/supporting/notificat
3333
import { NotificationService } from 'src/subdomains/supporting/notification/services/notification.service';
3434
import { FeeService } from 'src/subdomains/supporting/payment/services/fee.service';
3535
import { CustodyProviderService } from '../custody-provider/custody-provider.service';
36+
import { RecommendationMethod, RecommendationType } from '../recommendation/recommendation.entity';
3637
import { RecommendationService } from '../recommendation/recommendation.service';
3738
import { UserData } from '../user-data/user-data.entity';
3839
import { KycType, TradeApprovalReason, UserDataStatus } from '../user-data/user-data.enum';
@@ -179,7 +180,16 @@ export class AuthService {
179180

180181
if (dto.recommendationCode) await this.confirmRecommendationCode(dto.recommendationCode, user.userData);
181182

182-
if (!user.userData.tradeApprovalDate) await this.checkPendingRecommendation(user.userData, wallet);
183+
if (!user.userData.tradeApprovalDate) {
184+
await this.checkPendingRecommendation(user.userData, wallet);
185+
} else {
186+
const recommendation = await this.recommendationService.getUserDataRecommendation(user.userData.id, {
187+
isConfirmed: true,
188+
type: RecommendationType.INVITATION,
189+
method: RecommendationMethod.MAIL,
190+
});
191+
await this.recommendationService.setRecommenderRefCode(recommendation);
192+
}
183193

184194
await this.checkIpBlacklistFor(user.userData, userIp);
185195

src/subdomains/generic/user/models/recommendation/recommendation.service.ts

Lines changed: 35 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { KycService } from 'src/subdomains/generic/kyc/services/kyc.service';
1010
import { MailContext, MailType } from 'src/subdomains/supporting/notification/enums';
1111
import { MailKey, MailTranslationKey } from 'src/subdomains/supporting/notification/factories/mail.factory';
1212
import { NotificationService } from 'src/subdomains/supporting/notification/services/notification.service';
13-
import { IsNull, MoreThan } from 'typeorm';
13+
import { FindOptionsWhere, IsNull, MoreThan } from 'typeorm';
1414
import { UserData } from '../user-data/user-data.entity';
1515
import { KycLevel, KycType, TradeApprovalReason, UserDataStatus } from '../user-data/user-data.enum';
1616
import { UserDataService } from '../user-data/user-data.service';
@@ -75,9 +75,6 @@ export class RecommendationService {
7575
})
7676
: undefined;
7777

78-
if (recommended?.tradeApprovalDate)
79-
await this.userDataService.createTradeApprovalLog(recommended, TradeApprovalReason.MAIL_INVITATION);
80-
8178
const entity = await this.createRecommendationInternal(
8279
RecommendationType.INVITATION,
8380
dto.recommendedMail ? RecommendationMethod.MAIL : RecommendationMethod.RECOMMENDATION_CODE,
@@ -102,6 +99,12 @@ export class RecommendationService {
10299
isConfirmed: true,
103100
confirmationDate: new Date(),
104101
});
102+
103+
if (recommended.tradeApprovalDate) {
104+
await this.userDataService.createTradeApprovalLog(recommended, TradeApprovalReason.MAIL_INVITATION);
105+
106+
await this.setRecommenderRefCode(entity);
107+
}
105108
}
106109

107110
if (dto.recommendedMail) await this.sendInvitationMail(entity);
@@ -228,20 +231,24 @@ export class RecommendationService {
228231
TradeApprovalReason.RECOMMENDATION_CONFIRMED,
229232
);
230233

231-
const refCode =
232-
entity.kycStep && entity.method === RecommendationMethod.REF_CODE
233-
? entity.kycStep.getResult<KycRecommendationData>().key
234-
: (entity.recommender.users.find((u) => u.ref)?.ref ?? Config.defaultRef);
235-
236-
for (const user of entity.recommended.users ??
237-
(await this.userService.getAllUserDataUsers(entity.recommended.id))) {
238-
if (user.usedRef === Config.defaultRef) await this.userService.updateUserInternal(user, { usedRef: refCode });
239-
}
234+
await this.setRecommenderRefCode(entity);
240235
}
241236

242237
return this.recommendationRepo.save(entity);
243238
}
244239

240+
async setRecommenderRefCode(entity: Recommendation): Promise<void> {
241+
const refCode =
242+
entity.kycStep && entity.method === RecommendationMethod.REF_CODE
243+
? entity.kycStep.getResult<KycRecommendationData>().key
244+
: (entity.recommender.users.find((u) => u.ref)?.ref ?? Config.defaultRef);
245+
246+
for (const user of entity.recommended.users ??
247+
(await this.userService.getAllUserDataUsers(entity.recommended.id))) {
248+
if (user.usedRef === Config.defaultRef) await this.userService.updateUserInternal(user, { usedRef: refCode });
249+
}
250+
}
251+
245252
async getAndCheckRecommendationByCode(code: string): Promise<Recommendation> {
246253
const entity = await this.recommendationRepo.findOne({
247254
where: { code },
@@ -260,13 +267,26 @@ export class RecommendationService {
260267
return entity;
261268
}
262269

263-
async getAllRecommendationForUserData(userDataId: number): Promise<Recommendation[]> {
270+
async getAllRecommendationForUserData(recommenderId: number): Promise<Recommendation[]> {
264271
return this.recommendationRepo.find({
265-
where: { recommender: { id: userDataId } },
272+
where: { recommender: { id: recommenderId } },
266273
relations: { recommended: true, recommender: true },
267274
});
268275
}
269276

277+
async getUserDataRecommendation(
278+
userDataId: number,
279+
where: FindOptionsWhere<Recommendation>,
280+
): Promise<Recommendation> {
281+
return this.recommendationRepo.findOne({
282+
where: { recommended: { id: userDataId }, ...where },
283+
relations: {
284+
recommended: { users: true },
285+
recommender: { users: true },
286+
},
287+
});
288+
}
289+
270290
async checkAndConfirmRecommendInvitation(recommendedId: number): Promise<Recommendation> {
271291
const entity = await this.recommendationRepo.findOne({
272292
where: { recommended: { id: recommendedId }, isConfirmed: IsNull(), expirationDate: MoreThan(new Date()) },

src/subdomains/generic/user/models/user-data/dto/update-user-data.dto.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,15 @@ import { AccountType } from '../account-type.enum';
2323
import { DfxPhoneTransform, IsDfxPhone } from '../is-dfx-phone.validator';
2424
import { KycIdentificationType } from '../kyc-identification-type.enum';
2525
import { UserData } from '../user-data.entity';
26-
import { KycLevel, KycStatus, LegalEntity, RiskStatus, SignatoryPower, UserDataStatus } from '../user-data.enum';
26+
import {
27+
KycLevel,
28+
KycStatus,
29+
LegalEntity,
30+
PhoneCallStatus,
31+
RiskStatus,
32+
SignatoryPower,
33+
UserDataStatus,
34+
} from '../user-data.enum';
2735

2836
export class UpdateUserDataDto {
2937
@IsOptional()
@@ -316,4 +324,8 @@ export class UpdateUserDataDto {
316324
@IsDate()
317325
@Type(() => Date)
318326
phoneCallIpCountryCheckDate?: Date;
327+
328+
@IsOptional()
329+
@IsEnum(PhoneCallStatus)
330+
phoneCallStatus?: PhoneCallStatus;
319331
}

src/subdomains/supporting/bank-tx/bank-tx-return/__tests__/refund-creditor-data.spec.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ describe('BankTxReturnService - refundBankTx Creditor Data', () => {
105105
);
106106
});
107107

108-
it('should use dto creditor data when provided (override)', async () => {
108+
it('should use chargeback creditor if set', async () => {
109109
const dto = {
110110
chargebackAllowedDate: new Date(),
111111
chargebackAllowedBy: 'Admin',
@@ -127,12 +127,12 @@ describe('BankTxReturnService - refundBankTx Creditor Data', () => {
127127
mockBankTxReturn.id,
128128
false,
129129
expect.objectContaining({
130-
name: 'Override Name',
131-
address: 'Override Address',
132-
houseNumber: '99',
133-
zip: '9999',
134-
city: 'Override City',
135-
country: 'DE',
130+
name: 'Max Mustermann',
131+
address: 'Hauptstrasse',
132+
houseNumber: '42',
133+
zip: '3000',
134+
city: 'Bern',
135+
country: 'CH',
136136
}),
137137
);
138138
});

src/subdomains/supporting/bank-tx/bank-tx-return/bank-tx-return.service.ts

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,6 @@ export class BankTxReturnService {
5353

5454
const entities = await this.bankTxReturnRepo.find({
5555
where: [
56-
{
57-
...baseWhere,
58-
userData: IsNull(),
59-
},
6056
{
6157
...baseWhere,
6258
userData: {
@@ -71,10 +67,15 @@ export class BankTxReturnService {
7167

7268
for (const entity of entities) {
7369
try {
74-
await this.refundBankTx(entity, {
75-
chargebackAllowedDate: new Date(),
76-
chargebackAllowedBy: 'API',
77-
});
70+
if (
71+
Util.includesSameName(entity.userData.verifiedName, entity.creditorData.name) ||
72+
Util.includesSameName(entity.userData.completeName, entity.creditorData.name) ||
73+
(!entity.userData.verifiedName && !entity.userData.completeName)
74+
)
75+
await this.refundBankTx(entity, {
76+
chargebackAllowedDate: new Date(),
77+
chargebackAllowedBy: 'API',
78+
});
7879
} catch (e) {
7980
this.logger.error(`Failed to chargeback bank-tx-return ${entity.id}:`, e);
8081
}
@@ -210,7 +211,7 @@ export class BankTxReturnService {
210211
)
211212
throw new BadRequestException('IBAN not valid or BIC not available');
212213

213-
const creditorData = dto.creditorData ?? bankTxReturn.creditorData;
214+
const creditorData = bankTxReturn.creditorData ?? dto.creditorData;
214215
if ((dto.chargebackAllowedDate || dto.chargebackAllowedDateUser) && !creditorData)
215216
throw new BadRequestException('Creditor data is required for chargeback');
216217

0 commit comments

Comments
 (0)