diff --git a/src/subdomains/core/buy-crypto/process/services/buy-crypto-preparation.service.ts b/src/subdomains/core/buy-crypto/process/services/buy-crypto-preparation.service.ts index e5b2cf76d8..b8cb72f540 100644 --- a/src/subdomains/core/buy-crypto/process/services/buy-crypto-preparation.service.ts +++ b/src/subdomains/core/buy-crypto/process/services/buy-crypto-preparation.service.ts @@ -487,7 +487,12 @@ export class BuyCryptoPreparationService { const chargebackAllowedBy = 'API'; if (entity.bankTx) { - await this.buyCryptoService.refundBankTx(entity, { chargebackAllowedDate, chargebackAllowedBy }); + if ( + Util.includesSameName(entity.userData.verifiedName, entity.creditorData.name) || + Util.includesSameName(entity.userData.completeName, entity.creditorData.name) || + (!entity.userData.verifiedName && !entity.userData.completeName) + ) + await this.buyCryptoService.refundBankTx(entity, { chargebackAllowedDate, chargebackAllowedBy }); } else if (entity.cryptoInput) { await this.buyCryptoService.refundCryptoInput(entity, { chargebackAllowedDate, chargebackAllowedBy }); } else { diff --git a/src/subdomains/core/buy-crypto/process/services/buy-crypto.service.ts b/src/subdomains/core/buy-crypto/process/services/buy-crypto.service.ts index ff43c6d6ed..3c4e3b4c48 100644 --- a/src/subdomains/core/buy-crypto/process/services/buy-crypto.service.ts +++ b/src/subdomains/core/buy-crypto/process/services/buy-crypto.service.ts @@ -559,7 +559,7 @@ export class BuyCryptoService { ) throw new BadRequestException('IBAN not valid or BIC not available'); - const creditorData = dto.creditorData ?? buyCrypto.creditorData; + const creditorData = buyCrypto.creditorData ?? dto.creditorData; if ((dto.chargebackAllowedDate || dto.chargebackAllowedDateUser) && !creditorData) throw new BadRequestException('Creditor data is required for chargeback'); diff --git a/src/subdomains/core/history/controllers/transaction.controller.ts b/src/subdomains/core/history/controllers/transaction.controller.ts index 77feb5ec8a..9c4428b72f 100644 --- a/src/subdomains/core/history/controllers/transaction.controller.ts +++ b/src/subdomains/core/history/controllers/transaction.controller.ts @@ -435,6 +435,8 @@ export class TransactionController { const refundData = this.refundList.get(transaction.id); if (!refundData) throw new BadRequestException('Request refund data first'); if (!this.isRefundDataValid(refundData)) throw new BadRequestException('Refund data request invalid'); + if (refundData.refundTarget && dto.refundTarget) + throw new BadRequestException('RefundTarget is already set with refundData'); await this.executeRefund(transaction, transaction.targetEntity, refundData, dto); @@ -597,7 +599,7 @@ export class TransactionController { if (!dto.creditorData) throw new BadRequestException('Creditor data is required for bank refunds'); return this.buyCryptoService.refundBankTx(targetEntity, { - refundIban: dto.refundTarget ?? refundData.refundTarget, + refundIban: refundData.refundTarget ?? dto.refundTarget, creditorData: dto.creditorData, chargebackReferenceAmount: refundData.refundPrice.invert().convert(refundData.refundAmount), ...refundDto, @@ -615,19 +617,18 @@ export class TransactionController { private async getRefundTarget(transaction: Transaction): Promise { if (transaction.refundTargetEntity instanceof BuyFiat) return transaction.refundTargetEntity.chargebackAddress; - // For bank transactions, always return the original IBAN - refund must go to the sender - if (transaction.bankTx?.iban) return transaction.bankTx.iban; - - // For BuyCrypto with checkout (card), return masked card number - if (transaction.refundTargetEntity instanceof BuyCrypto && transaction.refundTargetEntity.checkoutTx) - return `${transaction.refundTargetEntity.checkoutTx.cardBin}****${transaction.refundTargetEntity.checkoutTx.cardLast4}`; - - // For other cases, return existing chargeback IBAN - if (transaction.refundTargetEntity instanceof BankTx) return transaction.bankTx?.iban; - if (transaction.refundTargetEntity instanceof BuyCrypto) return transaction.refundTargetEntity.chargebackIban; - if (transaction.refundTargetEntity instanceof BankTxReturn) return transaction.refundTargetEntity.chargebackIban; + try { + if (transaction.bankTx && (await this.validateIban(transaction.bankTx.iban))) return transaction.bankTx.iban; + } catch (_) { + return transaction.refundTargetEntity instanceof BankTx + ? undefined + : transaction.refundTargetEntity?.chargebackIban; + } - return undefined; + if (transaction.refundTargetEntity instanceof BuyCrypto) + return transaction.refundTargetEntity.checkoutTx + ? `${transaction.refundTargetEntity.checkoutTx.cardBin}****${transaction.refundTargetEntity.checkoutTx.cardLast4}` + : transaction.refundTargetEntity.chargebackIban; } private async validateIban(iban: string): Promise { diff --git a/src/subdomains/core/monitoring/observers/exchange.observer.ts b/src/subdomains/core/monitoring/observers/exchange.observer.ts index 81df979708..e2f0573cbd 100644 --- a/src/subdomains/core/monitoring/observers/exchange.observer.ts +++ b/src/subdomains/core/monitoring/observers/exchange.observer.ts @@ -74,6 +74,7 @@ export class ExchangeObserver extends MetricObserver { const xtDeurBtcPrice = await this.pricingService.getPriceFrom(PriceSource.XT, 'BTC', 'DEURO'); const xtDepsUsdtPrice = await this.pricingService.getPriceFrom(PriceSource.XT, 'USDT', 'DEPS'); const xtDepsBtcPrice = await this.pricingService.getPriceFrom(PriceSource.XT, 'BTC', 'DEPS'); + const xtJusdUsdtPrice = await this.pricingService.getPriceFrom(PriceSource.XT, 'USDT', 'JUSD'); const referenceDeurUsdtPrice = await this.pricingService.getPrice( usdt, @@ -83,6 +84,7 @@ export class ExchangeObserver extends MetricObserver { const referenceDeurBtcPrice = await this.pricingService.getPrice(btc, PriceCurrency.EUR, PriceValidity.VALID_ONLY); const referenceDepsUsdtPrice = await this.pricingService.getPriceFrom(PriceSource.DEURO, 'USDT', 'DEPS'); const referenceDepsBtcPrice = await this.pricingService.getPriceFrom(PriceSource.DEURO, 'BTC', 'DEPS'); + const referenceJusdUsdtPrice = await this.pricingService.getPriceFrom(PriceSource.JUICE, 'USDT', 'JUSD'); return [ { @@ -101,6 +103,10 @@ export class ExchangeObserver extends MetricObserver { name: 'XT-DEPS-BTC', deviation: Util.round(xtDepsBtcPrice.price / referenceDepsBtcPrice.price - 1, 3), }, + { + name: 'XT-JUSD-USDT', + deviation: Util.round(xtJusdUsdtPrice.price / referenceJusdUsdtPrice.price - 1, 3), + }, ]; } diff --git a/src/subdomains/generic/kyc/services/kyc.service.ts b/src/subdomains/generic/kyc/services/kyc.service.ts index 4322a0a24a..5173807f09 100644 --- a/src/subdomains/generic/kyc/services/kyc.service.ts +++ b/src/subdomains/generic/kyc/services/kyc.service.ts @@ -237,6 +237,7 @@ export class KycService { KycError.NATIONALITY_NOT_MATCHING, KycError.IP_COUNTRY_MISMATCH, KycError.COUNTRY_IP_COUNTRY_MISMATCH, + KycError.RESIDENCE_PERMIT_CHECK_REQUIRED, ].includes(e), ) ) diff --git a/src/subdomains/generic/user/models/auth/auth.service.ts b/src/subdomains/generic/user/models/auth/auth.service.ts index 644f538a7e..d9fc96467d 100644 --- a/src/subdomains/generic/user/models/auth/auth.service.ts +++ b/src/subdomains/generic/user/models/auth/auth.service.ts @@ -33,6 +33,7 @@ import { MailKey, MailTranslationKey } from 'src/subdomains/supporting/notificat import { NotificationService } from 'src/subdomains/supporting/notification/services/notification.service'; import { FeeService } from 'src/subdomains/supporting/payment/services/fee.service'; import { CustodyProviderService } from '../custody-provider/custody-provider.service'; +import { RecommendationMethod, RecommendationType } from '../recommendation/recommendation.entity'; import { RecommendationService } from '../recommendation/recommendation.service'; import { UserData } from '../user-data/user-data.entity'; import { KycType, TradeApprovalReason, UserDataStatus } from '../user-data/user-data.enum'; @@ -179,7 +180,16 @@ export class AuthService { if (dto.recommendationCode) await this.confirmRecommendationCode(dto.recommendationCode, user.userData); - if (!user.userData.tradeApprovalDate) await this.checkPendingRecommendation(user.userData, wallet); + if (!user.userData.tradeApprovalDate) { + await this.checkPendingRecommendation(user.userData, wallet); + } else { + const recommendation = await this.recommendationService.getUserDataRecommendation(user.userData.id, { + isConfirmed: true, + type: RecommendationType.INVITATION, + method: RecommendationMethod.MAIL, + }); + await this.recommendationService.setRecommenderRefCode(recommendation); + } await this.checkIpBlacklistFor(user.userData, userIp); diff --git a/src/subdomains/generic/user/models/recommendation/recommendation.service.ts b/src/subdomains/generic/user/models/recommendation/recommendation.service.ts index 96ebcfcdf8..a7d48fdc41 100644 --- a/src/subdomains/generic/user/models/recommendation/recommendation.service.ts +++ b/src/subdomains/generic/user/models/recommendation/recommendation.service.ts @@ -10,7 +10,7 @@ import { KycService } from 'src/subdomains/generic/kyc/services/kyc.service'; import { MailContext, MailType } from 'src/subdomains/supporting/notification/enums'; import { MailKey, MailTranslationKey } from 'src/subdomains/supporting/notification/factories/mail.factory'; import { NotificationService } from 'src/subdomains/supporting/notification/services/notification.service'; -import { IsNull, MoreThan } from 'typeorm'; +import { FindOptionsWhere, IsNull, MoreThan } from 'typeorm'; import { UserData } from '../user-data/user-data.entity'; import { KycLevel, KycType, TradeApprovalReason, UserDataStatus } from '../user-data/user-data.enum'; import { UserDataService } from '../user-data/user-data.service'; @@ -75,9 +75,6 @@ export class RecommendationService { }) : undefined; - if (recommended?.tradeApprovalDate) - await this.userDataService.createTradeApprovalLog(recommended, TradeApprovalReason.MAIL_INVITATION); - const entity = await this.createRecommendationInternal( RecommendationType.INVITATION, dto.recommendedMail ? RecommendationMethod.MAIL : RecommendationMethod.RECOMMENDATION_CODE, @@ -102,6 +99,12 @@ export class RecommendationService { isConfirmed: true, confirmationDate: new Date(), }); + + if (recommended.tradeApprovalDate) { + await this.userDataService.createTradeApprovalLog(recommended, TradeApprovalReason.MAIL_INVITATION); + + await this.setRecommenderRefCode(entity); + } } if (dto.recommendedMail) await this.sendInvitationMail(entity); @@ -228,20 +231,24 @@ export class RecommendationService { TradeApprovalReason.RECOMMENDATION_CONFIRMED, ); - const refCode = - entity.kycStep && entity.method === RecommendationMethod.REF_CODE - ? entity.kycStep.getResult().key - : (entity.recommender.users.find((u) => u.ref)?.ref ?? Config.defaultRef); - - for (const user of entity.recommended.users ?? - (await this.userService.getAllUserDataUsers(entity.recommended.id))) { - if (user.usedRef === Config.defaultRef) await this.userService.updateUserInternal(user, { usedRef: refCode }); - } + await this.setRecommenderRefCode(entity); } return this.recommendationRepo.save(entity); } + async setRecommenderRefCode(entity: Recommendation): Promise { + const refCode = + entity.kycStep && entity.method === RecommendationMethod.REF_CODE + ? entity.kycStep.getResult().key + : (entity.recommender.users.find((u) => u.ref)?.ref ?? Config.defaultRef); + + for (const user of entity.recommended.users ?? + (await this.userService.getAllUserDataUsers(entity.recommended.id))) { + if (user.usedRef === Config.defaultRef) await this.userService.updateUserInternal(user, { usedRef: refCode }); + } + } + async getAndCheckRecommendationByCode(code: string): Promise { const entity = await this.recommendationRepo.findOne({ where: { code }, @@ -260,13 +267,26 @@ export class RecommendationService { return entity; } - async getAllRecommendationForUserData(userDataId: number): Promise { + async getAllRecommendationForUserData(recommenderId: number): Promise { return this.recommendationRepo.find({ - where: { recommender: { id: userDataId } }, + where: { recommender: { id: recommenderId } }, relations: { recommended: true, recommender: true }, }); } + async getUserDataRecommendation( + userDataId: number, + where: FindOptionsWhere, + ): Promise { + return this.recommendationRepo.findOne({ + where: { recommended: { id: userDataId }, ...where }, + relations: { + recommended: { users: true }, + recommender: { users: true }, + }, + }); + } + async checkAndConfirmRecommendInvitation(recommendedId: number): Promise { const entity = await this.recommendationRepo.findOne({ where: { recommended: { id: recommendedId }, isConfirmed: IsNull(), expirationDate: MoreThan(new Date()) }, diff --git a/src/subdomains/generic/user/models/user-data/dto/update-user-data.dto.ts b/src/subdomains/generic/user/models/user-data/dto/update-user-data.dto.ts index 5e605151c5..c82e300656 100644 --- a/src/subdomains/generic/user/models/user-data/dto/update-user-data.dto.ts +++ b/src/subdomains/generic/user/models/user-data/dto/update-user-data.dto.ts @@ -23,7 +23,15 @@ import { AccountType } from '../account-type.enum'; import { DfxPhoneTransform, IsDfxPhone } from '../is-dfx-phone.validator'; import { KycIdentificationType } from '../kyc-identification-type.enum'; import { UserData } from '../user-data.entity'; -import { KycLevel, KycStatus, LegalEntity, RiskStatus, SignatoryPower, UserDataStatus } from '../user-data.enum'; +import { + KycLevel, + KycStatus, + LegalEntity, + PhoneCallStatus, + RiskStatus, + SignatoryPower, + UserDataStatus, +} from '../user-data.enum'; export class UpdateUserDataDto { @IsOptional() @@ -316,4 +324,8 @@ export class UpdateUserDataDto { @IsDate() @Type(() => Date) phoneCallIpCountryCheckDate?: Date; + + @IsOptional() + @IsEnum(PhoneCallStatus) + phoneCallStatus?: PhoneCallStatus; } diff --git a/src/subdomains/supporting/bank-tx/bank-tx-return/__tests__/refund-creditor-data.spec.ts b/src/subdomains/supporting/bank-tx/bank-tx-return/__tests__/refund-creditor-data.spec.ts index 8bed0288c8..6373508354 100644 --- a/src/subdomains/supporting/bank-tx/bank-tx-return/__tests__/refund-creditor-data.spec.ts +++ b/src/subdomains/supporting/bank-tx/bank-tx-return/__tests__/refund-creditor-data.spec.ts @@ -105,7 +105,7 @@ describe('BankTxReturnService - refundBankTx Creditor Data', () => { ); }); - it('should use dto creditor data when provided (override)', async () => { + it('should use chargeback creditor if set', async () => { const dto = { chargebackAllowedDate: new Date(), chargebackAllowedBy: 'Admin', @@ -127,12 +127,12 @@ describe('BankTxReturnService - refundBankTx Creditor Data', () => { mockBankTxReturn.id, false, expect.objectContaining({ - name: 'Override Name', - address: 'Override Address', - houseNumber: '99', - zip: '9999', - city: 'Override City', - country: 'DE', + name: 'Max Mustermann', + address: 'Hauptstrasse', + houseNumber: '42', + zip: '3000', + city: 'Bern', + country: 'CH', }), ); }); diff --git a/src/subdomains/supporting/bank-tx/bank-tx-return/bank-tx-return.service.ts b/src/subdomains/supporting/bank-tx/bank-tx-return/bank-tx-return.service.ts index ef7c217d38..9a98488e3c 100644 --- a/src/subdomains/supporting/bank-tx/bank-tx-return/bank-tx-return.service.ts +++ b/src/subdomains/supporting/bank-tx/bank-tx-return/bank-tx-return.service.ts @@ -53,10 +53,6 @@ export class BankTxReturnService { const entities = await this.bankTxReturnRepo.find({ where: [ - { - ...baseWhere, - userData: IsNull(), - }, { ...baseWhere, userData: { @@ -71,10 +67,15 @@ export class BankTxReturnService { for (const entity of entities) { try { - await this.refundBankTx(entity, { - chargebackAllowedDate: new Date(), - chargebackAllowedBy: 'API', - }); + if ( + Util.includesSameName(entity.userData.verifiedName, entity.creditorData.name) || + Util.includesSameName(entity.userData.completeName, entity.creditorData.name) || + (!entity.userData.verifiedName && !entity.userData.completeName) + ) + await this.refundBankTx(entity, { + chargebackAllowedDate: new Date(), + chargebackAllowedBy: 'API', + }); } catch (e) { this.logger.error(`Failed to chargeback bank-tx-return ${entity.id}:`, e); } @@ -210,7 +211,7 @@ export class BankTxReturnService { ) throw new BadRequestException('IBAN not valid or BIC not available'); - const creditorData = dto.creditorData ?? bankTxReturn.creditorData; + const creditorData = bankTxReturn.creditorData ?? dto.creditorData; if ((dto.chargebackAllowedDate || dto.chargebackAllowedDateUser) && !creditorData) throw new BadRequestException('Creditor data is required for chargeback'); diff --git a/src/subdomains/supporting/payment/entities/fee.entity.ts b/src/subdomains/supporting/payment/entities/fee.entity.ts index fdf3b5e677..b0db4a6351 100644 --- a/src/subdomains/supporting/payment/entities/fee.entity.ts +++ b/src/subdomains/supporting/payment/entities/fee.entity.ts @@ -207,8 +207,9 @@ export class Fee extends IEntity { ); } - verifyForUser(accountType: AccountType, wallet?: Wallet): void { - if (this.isExpired()) throw new BadRequestException('Discount code is expired'); + verifyForUser(accountType: AccountType, wallet?: Wallet, userDataId?: number): void { + if (!this.active) throw new BadRequestException('Fee is not active'); + if (this.isExpired(userDataId)) throw new BadRequestException('Discount code is expired'); if (this.accountType && this.accountType !== accountType) throw new BadRequestException('Account Type not matching'); if (this.wallet && wallet && this.wallet.id !== wallet.id) throw new BadRequestException('Wallet not matching'); diff --git a/src/subdomains/supporting/payment/services/fee.service.ts b/src/subdomains/supporting/payment/services/fee.service.ts index 341f0f8558..8c7791b00b 100644 --- a/src/subdomains/supporting/payment/services/fee.service.ts +++ b/src/subdomains/supporting/payment/services/fee.service.ts @@ -198,6 +198,8 @@ export class FeeService { async addFeeInternal(userData: UserData, feeId: number): Promise { const cachedFee = await this.getFee(feeId); + cachedFee.verifyForUser(userData.accountType, userData.wallet, userData.id); + await this.feeRepo.update(...cachedFee.increaseUsage(userData.accountType)); await this.userDataService.addFee(userData, cachedFee.id);