From 3e44791b7093973eb611d920b7d49622db8e27bb Mon Sep 17 00:00:00 2001 From: Yannick1712 <52333989+Yannick1712@users.noreply.github.com> Date: Tue, 7 Jan 2025 11:17:22 +0100 Subject: [PATCH 1/7] [DEV-3501] add specialCode in ipLog --- src/shared/auth/ip-country.guard.ts | 7 ++++++- src/shared/models/ip-log/ip-log.entity.ts | 3 +++ src/shared/models/ip-log/ip-log.service.ts | 3 ++- .../generic/user/models/auth/auth-alby.service.ts | 4 ++-- .../generic/user/models/auth/auth-lnurl.service.ts | 2 +- src/subdomains/generic/user/models/auth/auth.service.ts | 1 + 6 files changed, 15 insertions(+), 5 deletions(-) diff --git a/src/shared/auth/ip-country.guard.ts b/src/shared/auth/ip-country.guard.ts index 8677b32084..425e06a3b8 100644 --- a/src/shared/auth/ip-country.guard.ts +++ b/src/shared/auth/ip-country.guard.ts @@ -10,7 +10,12 @@ export class IpCountryGuard implements CanActivate { const req = context.switchToHttp().getRequest(); const ip = getClientIp(req); - const ipLog = await this.ipLogService.create(ip, req.url, req.body?.address ?? req.user.address); + const ipLog = await this.ipLogService.create( + ip, + req.url, + req.body?.address ?? req.user.address, + req.body?.specialCode, + ); if (!ipLog.result) throw new ForbiddenException('The country of IP address is not allowed'); if (req.body?.region && !Config.loginCountries[req.body?.region]?.includes(ipLog.country)) diff --git a/src/shared/models/ip-log/ip-log.entity.ts b/src/shared/models/ip-log/ip-log.entity.ts index 8f9eb22c02..70a02b4e2f 100644 --- a/src/shared/models/ip-log/ip-log.entity.ts +++ b/src/shared/models/ip-log/ip-log.entity.ts @@ -16,6 +16,9 @@ export class IpLog extends IEntity { @Column({ length: 256 }) url: string; + @Column({ length: 256, nullable: true }) + specialCode: string; + @Column() result: boolean; diff --git a/src/shared/models/ip-log/ip-log.service.ts b/src/shared/models/ip-log/ip-log.service.ts index c3a8e4ebea..f738fc5e27 100644 --- a/src/shared/models/ip-log/ip-log.service.ts +++ b/src/shared/models/ip-log/ip-log.service.ts @@ -17,7 +17,7 @@ export class IpLogService { private readonly repos: RepositoryFactory, ) {} - async create(ip: string, url: string, address: string): Promise { + async create(ip: string, url: string, address: string, specialCode?: string): Promise { const { country, result, user } = await this.checkIpCountry(ip, address); const ipLog = this.ipLogRepo.create({ ip, @@ -26,6 +26,7 @@ export class IpLogService { url, address, user, + specialCode, }); return this.ipLogRepo.save(ipLog); diff --git a/src/subdomains/generic/user/models/auth/auth-alby.service.ts b/src/subdomains/generic/user/models/auth/auth-alby.service.ts index e591396878..5964b591e5 100644 --- a/src/subdomains/generic/user/models/auth/auth-alby.service.ts +++ b/src/subdomains/generic/user/models/auth/auth-alby.service.ts @@ -65,7 +65,7 @@ export class AuthAlbyService { { client_id: Config.alby.clientId, client_secret: Config.alby.clientSecret, - code: code, + code, grant_type: 'authorization_code', redirect_uri: this.redirectUri(id), }, @@ -84,7 +84,7 @@ export class AuthAlbyService { // construct session and create IP log const session = { address: LightningHelper.addressToLnurlp(lightning_address), signature: identifier }; - const ipLog = await this.ipLogService.create(userIp, requestUrl, session.address); + const ipLog = await this.ipLogService.create(userIp, requestUrl, session.address, dto.specialCode); if (!ipLog.result) throw new ForbiddenException('The country of IP address is not allowed'); const { accessToken } = await this.authService.signIn(session, userIp, true).catch((e) => { diff --git a/src/subdomains/generic/user/models/auth/auth-lnurl.service.ts b/src/subdomains/generic/user/models/auth/auth-lnurl.service.ts index 3148c5f0b7..50f1201dec 100644 --- a/src/subdomains/generic/user/models/auth/auth-lnurl.service.ts +++ b/src/subdomains/generic/user/models/auth/auth-lnurl.service.ts @@ -91,7 +91,7 @@ export class AuthLnUrlService { const authCacheEntry = this.authCache.get(k1); const { servicesIp, servicesUrl } = authCacheEntry; - const ipLog = await this.ipLogService.create(servicesIp, servicesUrl, address); + const ipLog = await this.ipLogService.create(servicesIp, servicesUrl, address, signupDto.specialCode); if (!ipLog.result) { this.authCache.delete(k1); diff --git a/src/subdomains/generic/user/models/auth/auth.service.ts b/src/subdomains/generic/user/models/auth/auth.service.ts index 35f4e4fecb..4dad47acce 100644 --- a/src/subdomains/generic/user/models/auth/auth.service.ts +++ b/src/subdomains/generic/user/models/auth/auth.service.ts @@ -51,6 +51,7 @@ export interface MailKeyData { mail: string; userDataId: number; loginUrl: string; + specialCode: string; redirectUri?: string; } From 92dcc0f295ef87ff24151457aa32da7e619e2bfe Mon Sep 17 00:00:00 2001 From: Yannick1712 <52333989+Yannick1712@users.noreply.github.com> Date: Tue, 7 Jan 2025 11:23:17 +0100 Subject: [PATCH 2/7] [DEV-3501] fix code --- src/subdomains/generic/user/models/auth/auth.service.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/subdomains/generic/user/models/auth/auth.service.ts b/src/subdomains/generic/user/models/auth/auth.service.ts index 4dad47acce..35f4e4fecb 100644 --- a/src/subdomains/generic/user/models/auth/auth.service.ts +++ b/src/subdomains/generic/user/models/auth/auth.service.ts @@ -51,7 +51,6 @@ export interface MailKeyData { mail: string; userDataId: number; loginUrl: string; - specialCode: string; redirectUri?: string; } From b096dee7c4cd7d71bc093248b5e5186f69cf9579 Mon Sep 17 00:00:00 2001 From: Yannick1712 <52333989+Yannick1712@users.noreply.github.com> Date: Fri, 10 Jan 2025 14:28:20 +0100 Subject: [PATCH 3/7] [DEV-3501] add specialCode, integrator --- .../buy-crypto/routes/buy/buy.controller.ts | 4 +- .../routes/buy/dto/get-buy-quote.dto.ts | 8 +-- .../routes/swap/dto/get-swap-quote.dto.ts | 6 +- .../buy-crypto/routes/swap/swap.controller.ts | 4 +- .../route/dto/get-sell-quote.dto.ts | 6 +- .../core/sell-crypto/route/sell.controller.ts | 4 +- .../integrator/dto/create-integrator.dto.ts | 15 +++++ .../models/integrator/integrator.entity.ts | 60 +++++++++++++++++++ .../integrator/integrator.repository.ts | 11 ++++ .../models/integrator/integrator.service.ts | 23 +++++++ .../user/models/wallet/wallet.entity.ts | 17 +++++- src/subdomains/generic/user/user.module.ts | 4 ++ .../dto/input/create-special-code.dto.ts | 12 ++++ .../supporting/payment/entities/fee.entity.ts | 9 ++- .../payment/entities/special-code.entity.ts | 25 ++++++++ .../supporting/payment/payment.module.ts | 4 ++ .../repositories/special-code.repository.ts | 11 ++++ .../payment/services/fee.service.ts | 16 +++-- .../payment/services/special-code.service.ts | 18 ++++++ .../payment/services/transaction-helper.ts | 21 +------ 20 files changed, 232 insertions(+), 46 deletions(-) create mode 100644 src/subdomains/generic/user/models/integrator/dto/create-integrator.dto.ts create mode 100644 src/subdomains/generic/user/models/integrator/integrator.entity.ts create mode 100644 src/subdomains/generic/user/models/integrator/integrator.repository.ts create mode 100644 src/subdomains/generic/user/models/integrator/integrator.service.ts create mode 100644 src/subdomains/supporting/payment/dto/input/create-special-code.dto.ts create mode 100644 src/subdomains/supporting/payment/entities/special-code.entity.ts create mode 100644 src/subdomains/supporting/payment/repositories/special-code.repository.ts create mode 100644 src/subdomains/supporting/payment/services/special-code.service.ts diff --git a/src/subdomains/core/buy-crypto/routes/buy/buy.controller.ts b/src/subdomains/core/buy-crypto/routes/buy/buy.controller.ts index 95a9e9dd6a..3967b2a041 100644 --- a/src/subdomains/core/buy-crypto/routes/buy/buy.controller.ts +++ b/src/subdomains/core/buy-crypto/routes/buy/buy.controller.ts @@ -99,6 +99,7 @@ export class BuyController { targetAmount, paymentMethod, specialCode, + specialCodes, } = await this.paymentInfoService.buyCheck(dto); const { @@ -124,8 +125,7 @@ export class BuyController { CryptoPaymentMethod.CRYPTO, true, undefined, - dto.wallet, - specialCode ? [specialCode] : [], + specialCodes ?? (specialCode ? [specialCode] : []), ); return { diff --git a/src/subdomains/core/buy-crypto/routes/buy/dto/get-buy-quote.dto.ts b/src/subdomains/core/buy-crypto/routes/buy/dto/get-buy-quote.dto.ts index 7dee7be6ad..fc94bf3286 100644 --- a/src/subdomains/core/buy-crypto/routes/buy/dto/get-buy-quote.dto.ts +++ b/src/subdomains/core/buy-crypto/routes/buy/dto/get-buy-quote.dto.ts @@ -49,18 +49,18 @@ export class GetBuyQuoteDto { @IsEnum(FiatPaymentMethod) paymentMethod: FiatPaymentMethod = FiatPaymentMethod.BANK; - @ApiPropertyOptional({ description: 'This field is deprecated, use "specialCode" instead.', deprecated: true }) + @ApiPropertyOptional({ description: 'This field is deprecated, use "specialCodes" instead.', deprecated: true }) @IsOptional() @IsString() discountCode: string; - @ApiPropertyOptional({ description: 'Special code' }) + @ApiPropertyOptional({ description: 'This field is deprecated, use "specialCodes" instead.', deprecated: true }) @IsOptional() @IsString() specialCode: string; - @ApiPropertyOptional() + @ApiPropertyOptional({ description: 'Special codes' }) @IsOptional() @IsString() - wallet: string; + specialCodes: string[]; } diff --git a/src/subdomains/core/buy-crypto/routes/swap/dto/get-swap-quote.dto.ts b/src/subdomains/core/buy-crypto/routes/swap/dto/get-swap-quote.dto.ts index 7cb2fbda96..cd7156103a 100644 --- a/src/subdomains/core/buy-crypto/routes/swap/dto/get-swap-quote.dto.ts +++ b/src/subdomains/core/buy-crypto/routes/swap/dto/get-swap-quote.dto.ts @@ -46,13 +46,13 @@ export class GetSwapQuoteDto { @IsString() discountCode: string; - @ApiPropertyOptional({ description: 'Special code' }) + @ApiPropertyOptional({ description: 'This field is deprecated, use "specialCodes" instead.', deprecated: true }) @IsOptional() @IsString() specialCode: string; - @ApiPropertyOptional() + @ApiPropertyOptional({ description: 'Special codes' }) @IsOptional() @IsString() - wallet: string; + specialCodes: string[]; } diff --git a/src/subdomains/core/buy-crypto/routes/swap/swap.controller.ts b/src/subdomains/core/buy-crypto/routes/swap/swap.controller.ts index 10cc0095b5..806558c16b 100644 --- a/src/subdomains/core/buy-crypto/routes/swap/swap.controller.ts +++ b/src/subdomains/core/buy-crypto/routes/swap/swap.controller.ts @@ -93,6 +93,7 @@ export class SwapController { targetAsset, targetAmount, specialCode, + specialCodes, } = await this.paymentInfoService.swapCheck(dto); const { @@ -117,8 +118,7 @@ export class SwapController { CryptoPaymentMethod.CRYPTO, true, undefined, - dto.wallet, - specialCode ? [specialCode] : [], + specialCodes ?? (specialCode ? [specialCode] : []), ); return { diff --git a/src/subdomains/core/sell-crypto/route/dto/get-sell-quote.dto.ts b/src/subdomains/core/sell-crypto/route/dto/get-sell-quote.dto.ts index e8809b11c8..4d6a2e291a 100644 --- a/src/subdomains/core/sell-crypto/route/dto/get-sell-quote.dto.ts +++ b/src/subdomains/core/sell-crypto/route/dto/get-sell-quote.dto.ts @@ -47,13 +47,13 @@ export class GetSellQuoteDto { @IsString() discountCode: string; - @ApiPropertyOptional({ description: 'Special code' }) + @ApiPropertyOptional({ description: 'This field is deprecated, use "specialCodes" instead.', deprecated: true }) @IsOptional() @IsString() specialCode: string; - @ApiPropertyOptional() + @ApiPropertyOptional({ description: 'Special codes' }) @IsOptional() @IsString() - wallet: string; + specialCodes: string[]; } diff --git a/src/subdomains/core/sell-crypto/route/sell.controller.ts b/src/subdomains/core/sell-crypto/route/sell.controller.ts index 48f19f54b0..6fc3e1457c 100644 --- a/src/subdomains/core/sell-crypto/route/sell.controller.ts +++ b/src/subdomains/core/sell-crypto/route/sell.controller.ts @@ -95,6 +95,7 @@ export class SellController { currency, targetAmount, specialCode, + specialCodes, } = await this.paymentInfoService.sellCheck(dto); const { @@ -120,8 +121,7 @@ export class SellController { FiatPaymentMethod.BANK, true, undefined, - dto.wallet, - specialCode ? [specialCode] : [], + specialCodes ?? (specialCode ? [specialCode] : []), ); return { diff --git a/src/subdomains/generic/user/models/integrator/dto/create-integrator.dto.ts b/src/subdomains/generic/user/models/integrator/dto/create-integrator.dto.ts new file mode 100644 index 0000000000..ee968a2474 --- /dev/null +++ b/src/subdomains/generic/user/models/integrator/dto/create-integrator.dto.ts @@ -0,0 +1,15 @@ +import { IsNotEmpty, IsOptional, IsString } from 'class-validator'; + +export class CreateIntegratorDto { + @IsNotEmpty() + @IsString() + name: string; + + @IsOptional() + @IsString() + mail: string; + + @IsNotEmpty() + @IsString() + masterKey: string; +} diff --git a/src/subdomains/generic/user/models/integrator/integrator.entity.ts b/src/subdomains/generic/user/models/integrator/integrator.entity.ts new file mode 100644 index 0000000000..f4963517a0 --- /dev/null +++ b/src/subdomains/generic/user/models/integrator/integrator.entity.ts @@ -0,0 +1,60 @@ +import { IEntity } from 'src/shared/models/entity'; +import { AmlRule } from 'src/subdomains/core/aml/enums/aml-rule.enum'; +import { KycStepType } from 'src/subdomains/generic/kyc/enums/kyc.enum'; +import { Column, Entity } from 'typeorm'; +import { WebhookType } from '../../services/webhook/dto/webhook.dto'; +import { KycType } from '../user-data/user-data.entity'; +import { WebhookConfig, WebhookConfigOption } from '../wallet/wallet.entity'; + +@Entity() +export class Integrator extends IEntity { + @Column({ length: 256, nullable: true }) + name?: string; + + @Column({ nullable: true }) + customKyc?: KycType; + + @Column({ length: 256, nullable: true }) + identMethod?: KycStepType; + + @Column({ length: 256, nullable: true }) + apiUrl?: string; + + @Column({ length: 256, nullable: true }) + apiKey?: string; + + @Column({ length: 'MAX', nullable: true }) + webhookConfig?: string; // JSON string + + // TODO: nullable or default? + @Column({ nullable: true }) + amlRule: AmlRule; + + // --- ENTITY METHODS --- // + + get webhookConfigObject(): WebhookConfig | undefined { + return this.webhookConfig ? (JSON.parse(this.webhookConfig) as WebhookConfig) : undefined; + } + + isValidForWebhook(type: WebhookType, consented: boolean): boolean { + if (!this.apiUrl) return false; + + switch (type) { + case WebhookType.KYC_CHANGED: + case WebhookType.KYC_FAILED: + case WebhookType.ACCOUNT_CHANGED: + return this.isOptionValid(this.webhookConfigObject?.kyc, consented); + + case WebhookType.PAYMENT: + return this.isOptionValid(this.webhookConfigObject?.payment, consented); + } + } + + private isOptionValid(option: WebhookConfigOption | undefined, consented: boolean): boolean { + return ( + option === WebhookConfigOption.TRUE || + (option === WebhookConfigOption.CONSENT_ONLY && consented) || + (option === WebhookConfigOption.WALLET_ONLY && !consented) + ); + } +} diff --git a/src/subdomains/generic/user/models/integrator/integrator.repository.ts b/src/subdomains/generic/user/models/integrator/integrator.repository.ts new file mode 100644 index 0000000000..9fad3a9a9e --- /dev/null +++ b/src/subdomains/generic/user/models/integrator/integrator.repository.ts @@ -0,0 +1,11 @@ +import { Injectable } from '@nestjs/common'; +import { CachedRepository } from 'src/shared/repositories/cached.repository'; +import { EntityManager } from 'typeorm'; +import { Integrator } from './integrator.entity'; + +@Injectable() +export class IntegratorRepository extends CachedRepository { + constructor(manager: EntityManager) { + super(Integrator, manager); + } +} diff --git a/src/subdomains/generic/user/models/integrator/integrator.service.ts b/src/subdomains/generic/user/models/integrator/integrator.service.ts new file mode 100644 index 0000000000..2d2600d540 --- /dev/null +++ b/src/subdomains/generic/user/models/integrator/integrator.service.ts @@ -0,0 +1,23 @@ +import { Injectable, NotFoundException } from '@nestjs/common'; +import { CreateIntegratorDto } from './dto/create-integrator.dto'; +import { Integrator } from './integrator.entity'; +import { IntegratorRepository } from './integrator.repository'; + +@Injectable() +export class IntegratorService { + constructor(private readonly repo: IntegratorRepository) {} + + async createIntegrator(dto: CreateIntegratorDto): Promise { + const entity = this.repo.create(dto); + return this.repo.save(entity); + } + + async updateIntegrator(id: number, dto: CreateIntegratorDto): Promise { + const entity = await this.repo.findOneBy({ id }); + if (!entity) throw new NotFoundException('Integrator not found'); + + Object.assign(entity, dto); + + return this.repo.save(entity); + } +} diff --git a/src/subdomains/generic/user/models/wallet/wallet.entity.ts b/src/subdomains/generic/user/models/wallet/wallet.entity.ts index 49b29863e9..cd109f855d 100644 --- a/src/subdomains/generic/user/models/wallet/wallet.entity.ts +++ b/src/subdomains/generic/user/models/wallet/wallet.entity.ts @@ -33,37 +33,47 @@ export class Wallet extends IEntity { @Column({ length: 256, nullable: true }) masterKey?: string; + // TODO: remove @Column({ default: false }) isKycClient: boolean; + // TODO: remove? @Column({ default: false }) usesDummyAddresses: boolean; + // TODO: remove @Column({ nullable: true }) customKyc?: KycType; - @OneToMany(() => User, (user) => user.wallet) - users: User[]; - + // TODO: remove @Column({ length: 256, nullable: true }) identMethod?: KycStepType; + // TODO: remove @Column({ length: 256, nullable: true }) apiUrl?: string; + // TODO: remove @Column({ length: 256, nullable: true }) apiKey?: string; + // TODO: remove @Column({ default: AmlRule.DEFAULT }) amlRule: AmlRule; + // TODO: remove @Column({ length: 'MAX', nullable: true }) webhookConfig?: string; // JSON string + @OneToMany(() => User, (user) => user.wallet) + users: User[]; + + // TODO: remove get webhookConfigObject(): WebhookConfig | undefined { return this.webhookConfig ? (JSON.parse(this.webhookConfig) as WebhookConfig) : undefined; } + // TODO: remove isValidForWebhook(type: WebhookType, consented: boolean): boolean { if (!this.apiUrl) return false; @@ -78,6 +88,7 @@ export class Wallet extends IEntity { } } + // TODO: remove private isOptionValid(option: WebhookConfigOption | undefined, consented: boolean): boolean { return ( option === WebhookConfigOption.TRUE || diff --git a/src/subdomains/generic/user/user.module.ts b/src/subdomains/generic/user/user.module.ts index 67f5e37254..ea18c8fcf2 100644 --- a/src/subdomains/generic/user/user.module.ts +++ b/src/subdomains/generic/user/user.module.ts @@ -30,6 +30,8 @@ import { CustodyProviderController } from './models/custody-provider/custody-pro import { CustodyProvider } from './models/custody-provider/custody-provider.entity'; import { CustodyProviderRepository } from './models/custody-provider/custody-provider.repository'; import { CustodyProviderService } from './models/custody-provider/custody-provider.service'; +import { IntegratorRepository } from './models/integrator/integrator.repository'; +import { IntegratorService } from './models/integrator/integrator.service'; import { KycClientController, KycController } from './models/kyc/kyc.controller'; import { KycService } from './models/kyc/kyc.service'; import { UserDataRelationController } from './models/user-data-relation/user-data-relation.controller'; @@ -97,6 +99,8 @@ import { WebhookService } from './services/webhook/webhook.service'; AccountMergeService, CustodyProviderService, CustodyProviderRepository, + IntegratorRepository, + IntegratorService, ], exports: [ UserService, diff --git a/src/subdomains/supporting/payment/dto/input/create-special-code.dto.ts b/src/subdomains/supporting/payment/dto/input/create-special-code.dto.ts new file mode 100644 index 0000000000..9f202ca1e7 --- /dev/null +++ b/src/subdomains/supporting/payment/dto/input/create-special-code.dto.ts @@ -0,0 +1,12 @@ +import { IsEnum, IsNotEmpty, IsOptional, IsString } from 'class-validator'; +import { SpecialCodeType } from '../../entities/special-code.entity'; + +export class CreateSpecialCodeDto { + @IsNotEmpty() + @IsEnum(SpecialCodeType) + type: SpecialCodeType; + + @IsOptional() + @IsString() + code: string; +} diff --git a/src/subdomains/supporting/payment/entities/fee.entity.ts b/src/subdomains/supporting/payment/entities/fee.entity.ts index 82cfe70423..2d25750de1 100644 --- a/src/subdomains/supporting/payment/entities/fee.entity.ts +++ b/src/subdomains/supporting/payment/entities/fee.entity.ts @@ -8,6 +8,7 @@ import { Wallet } from 'src/subdomains/generic/user/models/wallet/wallet.entity' import { Column, Entity, ManyToOne } from 'typeorm'; import { Bank } from '../../bank/bank/bank.entity'; import { FeeRequest } from '../services/fee.service'; +import { SpecialCode } from './special-code.entity'; export enum FeeType { BASE = 'Base', @@ -43,6 +44,8 @@ export class Fee extends IEntity { active: boolean; // Filter columns + + // TODO: remove @Column({ length: 256, nullable: true }) specialCode?: string; @@ -71,7 +74,11 @@ export class Fee extends IEntity { wallet?: Wallet; @ManyToOne(() => Bank, { nullable: true, eager: true }) - bank: Bank; + bank?: Bank; + + // TODO: rename + @ManyToOne(() => SpecialCode, { nullable: true, eager: true }) + code?: SpecialCode; // Volume columns diff --git a/src/subdomains/supporting/payment/entities/special-code.entity.ts b/src/subdomains/supporting/payment/entities/special-code.entity.ts new file mode 100644 index 0000000000..dde7585767 --- /dev/null +++ b/src/subdomains/supporting/payment/entities/special-code.entity.ts @@ -0,0 +1,25 @@ +import { IEntity } from 'src/shared/models/entity'; +import { Integrator } from 'src/subdomains/generic/user/models/integrator/integrator.entity'; +import { Column, Entity, JoinColumn, OneToMany, OneToOne } from 'typeorm'; +import { Fee } from './fee.entity'; + +export enum SpecialCodeType { + FEE = 'Fee', + INTEGRATOR = 'Integrator', +} + +@Entity() +export class SpecialCode extends IEntity { + @Column({ length: 256, unique: true }) + code: string; + + @Column({ length: 256 }) + type: SpecialCodeType; + + @OneToMany(() => Fee, (fee) => fee.code, { nullable: true }) + fees?: Fee[]; + + @OneToOne(() => Integrator, { nullable: true }) + @JoinColumn() + integrator?: Integrator; +} diff --git a/src/subdomains/supporting/payment/payment.module.ts b/src/subdomains/supporting/payment/payment.module.ts index 4784c61d12..20085da8aa 100644 --- a/src/subdomains/supporting/payment/payment.module.ts +++ b/src/subdomains/supporting/payment/payment.module.ts @@ -17,10 +17,12 @@ import { TransactionRequest } from './entities/transaction-request.entity'; import { TransactionSpecification } from './entities/transaction-specification.entity'; import { BlockchainFeeRepository } from './repositories/blockchain-fee.repository'; import { FeeRepository } from './repositories/fee.repository'; +import { SpecialCodeRepository } from './repositories/special-code.repository'; import { SpecialExternalAccountRepository } from './repositories/special-external-account.repository'; import { TransactionRequestRepository } from './repositories/transaction-request.repository'; import { TransactionSpecificationRepository } from './repositories/transaction-specification.repository'; import { FeeService } from './services/fee.service'; +import { SpecialCodeService } from './services/special-code.service'; import { SpecialExternalAccountService } from './services/special-external-account.service'; import { SwissQRService } from './services/swiss-qr.service'; import { TransactionHelper } from './services/transaction-helper'; @@ -53,6 +55,8 @@ import { TransactionModule } from './transaction.module'; TransactionRequestService, SpecialExternalAccountService, SpecialExternalAccountRepository, + SpecialCodeRepository, + SpecialCodeService, ], exports: [TransactionHelper, FeeService, SwissQRService, TransactionRequestService, SpecialExternalAccountService], }) diff --git a/src/subdomains/supporting/payment/repositories/special-code.repository.ts b/src/subdomains/supporting/payment/repositories/special-code.repository.ts new file mode 100644 index 0000000000..c2c32a1d93 --- /dev/null +++ b/src/subdomains/supporting/payment/repositories/special-code.repository.ts @@ -0,0 +1,11 @@ +import { Injectable } from '@nestjs/common'; +import { CachedRepository } from 'src/shared/repositories/cached.repository'; +import { EntityManager } from 'typeorm'; +import { SpecialCode } from '../entities/special-code.entity'; + +@Injectable() +export class SpecialCodeRepository extends CachedRepository { + constructor(manager: EntityManager) { + super(SpecialCode, manager); + } +} diff --git a/src/subdomains/supporting/payment/services/fee.service.ts b/src/subdomains/supporting/payment/services/fee.service.ts index 297d3e116b..8f519b0196 100644 --- a/src/subdomains/supporting/payment/services/fee.service.ts +++ b/src/subdomains/supporting/payment/services/fee.service.ts @@ -33,8 +33,10 @@ import { InternalFeeDto } from '../dto/fee.dto'; import { CreateFeeDto } from '../dto/input/create-fee.dto'; import { PaymentMethod } from '../dto/payment-method.enum'; import { Fee, FeeType } from '../entities/fee.entity'; +import { SpecialCodeType } from '../entities/special-code.entity'; import { BlockchainFeeRepository } from '../repositories/blockchain-fee.repository'; import { FeeRepository } from '../repositories/fee.repository'; +import { SpecialCodeService } from './special-code.service'; export interface UserFeeRequest extends FeeRequestBase { user: User; @@ -48,12 +50,10 @@ export interface FeeRequest extends FeeRequestBase { export interface OptionalFeeRequest extends FeeRequestBase { user?: User; - wallet?: Wallet; accountType?: AccountType; } export interface FeeRequestBase { - wallet?: Wallet; paymentMethodIn: PaymentMethod; paymentMethodOut: PaymentMethod; bankIn: CardBankName | IbanBankName; @@ -84,6 +84,7 @@ export class FeeService implements OnModuleInit { private readonly payoutService: PayoutService, private readonly pricingService: PricingService, private readonly bankService: BankService, + private readonly specialCodeService: SpecialCodeService, ) {} onModuleInit() { @@ -158,7 +159,10 @@ export class FeeService implements OnModuleInit { if (dto.createSpecialCode) { // create hash const hash = Util.createHash(fee.label + fee.type).toUpperCase(); - fee.specialCode = `${hash.slice(0, 4)}-${hash.slice(4, 8)}-${hash.slice(8, 12)}`; + fee.code = await this.specialCodeService.createSpecialCode({ + code: `${hash.slice(0, 4)}-${hash.slice(4, 8)}-${hash.slice(8, 12)}`, + type: SpecialCodeType.FEE, + }); } // save @@ -209,7 +213,7 @@ export class FeeService implements OnModuleInit { } async getFeeBySpecialCode(specialCode: string): Promise { - const fee = await this.getAllFees().then((fees) => fees.find((f) => f.specialCode === specialCode)); + const fee = await this.getAllFees().then((fees) => fees.find((f) => f.code.code === specialCode)); if (!fee) throw new NotFoundException(`Discount code ${specialCode} not found`); return fee; } @@ -402,7 +406,7 @@ export class FeeService implements OnModuleInit { private async getValidFees(request: OptionalFeeRequest): Promise { const accountType = request.user?.userData?.accountType ?? request.accountType ?? AccountType.PERSONAL; - const wallet = request.wallet ?? request.user?.wallet; + const wallet = request.user?.wallet; const userDataId = request.user?.userData?.id; const discountFeeIds = request.user?.userData?.individualFeeList ?? []; @@ -414,7 +418,7 @@ export class FeeService implements OnModuleInit { ([FeeType.DISCOUNT, FeeType.ADDITION, FeeType.RELATIVE_DISCOUNT, FeeType.BANK].includes(f.type) && !f.specialCode) || discountFeeIds.includes(f.id) || - request.specialCodes.includes(f.specialCode) || + request.specialCodes.includes(f.code.code) || (f.wallet && f.wallet.id === wallet?.id), ), ); diff --git a/src/subdomains/supporting/payment/services/special-code.service.ts b/src/subdomains/supporting/payment/services/special-code.service.ts new file mode 100644 index 0000000000..0e21898103 --- /dev/null +++ b/src/subdomains/supporting/payment/services/special-code.service.ts @@ -0,0 +1,18 @@ +import { BadRequestException, Injectable } from '@nestjs/common'; +import { CreateSpecialCodeDto } from '../dto/input/create-special-code.dto'; +import { SpecialCode } from '../entities/special-code.entity'; +import { SpecialCodeRepository } from '../repositories/special-code.repository'; + +@Injectable() +export class SpecialCodeService { + constructor(private readonly specialCodeRepo: SpecialCodeRepository) {} + + async createSpecialCode(dto: CreateSpecialCodeDto): Promise { + const existing = await this.specialCodeRepo.findOneBy({ code: dto.code }); + if (existing) throw new BadRequestException('Special code already created'); + + const specialCode = this.specialCodeRepo.create(dto); + + return this.specialCodeRepo.save(specialCode); + } +} diff --git a/src/subdomains/supporting/payment/services/transaction-helper.ts b/src/subdomains/supporting/payment/services/transaction-helper.ts index 4a63cca18a..878c505ae9 100644 --- a/src/subdomains/supporting/payment/services/transaction-helper.ts +++ b/src/subdomains/supporting/payment/services/transaction-helper.ts @@ -16,7 +16,6 @@ import { BuyCryptoService } from 'src/subdomains/core/buy-crypto/process/service import { BuyFiatService } from 'src/subdomains/core/sell-crypto/process/services/buy-fiat.service'; import { KycLevel, UserDataStatus } from 'src/subdomains/generic/user/models/user-data/user-data.entity'; import { User } from 'src/subdomains/generic/user/models/user/user.entity'; -import { Wallet } from 'src/subdomains/generic/user/models/wallet/wallet.entity'; import { WalletService } from 'src/subdomains/generic/user/models/wallet/wallet.service'; import { MinAmount } from 'src/subdomains/supporting/payment/dto/transaction-helper/min-amount.dto'; import { FeeService, UserFeeRequest } from 'src/subdomains/supporting/payment/services/fee.service'; @@ -168,19 +167,7 @@ export class TransactionHelper implements OnModuleInit { ): Promise { // get fee const [fee, networkStartFee] = await Promise.all([ - this.getTxFee( - user, - undefined, - paymentMethodIn, - paymentMethodOut, - bankIn, - bankOut, - from, - to, - inputAmountChf, - [], - false, - ), + this.getTxFee(user, paymentMethodIn, paymentMethodOut, bankIn, bankOut, from, to, inputAmountChf, [], false), this.getNetworkStartFee(to, false, user), ]); @@ -225,7 +212,6 @@ export class TransactionHelper implements OnModuleInit { paymentMethodOut: PaymentMethod, allowExpiredPrice: boolean, user?: User, - walletName?: string, specialCodes: string[] = [], ): Promise { const txAsset = targetAmount ? to : from; @@ -237,13 +223,10 @@ export class TransactionHelper implements OnModuleInit { const bankIn = this.getDefaultBankByPaymentMethod(paymentMethodIn); const bankOut = this.getDefaultBankByPaymentMethod(paymentMethodOut); - const wallet = walletName ? await this.walletService.getByIdOrName(undefined, walletName) : undefined; - // get fee const [fee, networkStartFee] = await Promise.all([ this.getTxFee( user, - wallet, paymentMethodIn, paymentMethodOut, bankIn, @@ -379,7 +362,6 @@ export class TransactionHelper implements OnModuleInit { private async getTxFee( user: User | undefined, - wallet: Wallet | undefined, paymentMethodIn: PaymentMethod, paymentMethodOut: PaymentMethod, bankIn: CardBankName | IbanBankName, @@ -392,7 +374,6 @@ export class TransactionHelper implements OnModuleInit { ): Promise { const feeRequest: UserFeeRequest = { user, - wallet, paymentMethodIn, paymentMethodOut, bankIn, From 35cfddd14b441b3c606931eee453983a96b31c65 Mon Sep 17 00:00:00 2001 From: Yannick1712 <52333989+Yannick1712@users.noreply.github.com> Date: Thu, 16 Jan 2025 18:22:19 +0100 Subject: [PATCH 4/7] [DEV-3501] add isArray definition --- .../core/buy-crypto/routes/buy/dto/get-buy-quote.dto.ts | 2 +- .../core/buy-crypto/routes/swap/dto/get-swap-quote.dto.ts | 2 +- src/subdomains/core/sell-crypto/route/dto/get-sell-quote.dto.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/subdomains/core/buy-crypto/routes/buy/dto/get-buy-quote.dto.ts b/src/subdomains/core/buy-crypto/routes/buy/dto/get-buy-quote.dto.ts index fc94bf3286..9e32672190 100644 --- a/src/subdomains/core/buy-crypto/routes/buy/dto/get-buy-quote.dto.ts +++ b/src/subdomains/core/buy-crypto/routes/buy/dto/get-buy-quote.dto.ts @@ -59,7 +59,7 @@ export class GetBuyQuoteDto { @IsString() specialCode: string; - @ApiPropertyOptional({ description: 'Special codes' }) + @ApiPropertyOptional({ description: 'Special codes', isArray: true }) @IsOptional() @IsString() specialCodes: string[]; diff --git a/src/subdomains/core/buy-crypto/routes/swap/dto/get-swap-quote.dto.ts b/src/subdomains/core/buy-crypto/routes/swap/dto/get-swap-quote.dto.ts index cd7156103a..4e3edbf8c1 100644 --- a/src/subdomains/core/buy-crypto/routes/swap/dto/get-swap-quote.dto.ts +++ b/src/subdomains/core/buy-crypto/routes/swap/dto/get-swap-quote.dto.ts @@ -51,7 +51,7 @@ export class GetSwapQuoteDto { @IsString() specialCode: string; - @ApiPropertyOptional({ description: 'Special codes' }) + @ApiPropertyOptional({ description: 'Special codes', isArray: true }) @IsOptional() @IsString() specialCodes: string[]; diff --git a/src/subdomains/core/sell-crypto/route/dto/get-sell-quote.dto.ts b/src/subdomains/core/sell-crypto/route/dto/get-sell-quote.dto.ts index 4d6a2e291a..b9fca41d94 100644 --- a/src/subdomains/core/sell-crypto/route/dto/get-sell-quote.dto.ts +++ b/src/subdomains/core/sell-crypto/route/dto/get-sell-quote.dto.ts @@ -52,7 +52,7 @@ export class GetSellQuoteDto { @IsString() specialCode: string; - @ApiPropertyOptional({ description: 'Special codes' }) + @ApiPropertyOptional({ description: 'Special codes', isArray: true }) @IsOptional() @IsString() specialCodes: string[]; From c1e3b04490cd1b32a2caf859c5803f5c67745078 Mon Sep 17 00:00:00 2001 From: Yannick1712 <52333989+Yannick1712@users.noreply.github.com> Date: Thu, 16 Jan 2025 21:42:06 +0100 Subject: [PATCH 5/7] [DEV-3501] add specialCode in jwt token --- src/shared/auth/jwt-payload.interface.ts | 1 + .../generic/user/models/auth/auth.service.ts | 8 ++++--- .../user/models/user-data/user-data.entity.ts | 14 ++++++------ .../models/user-data/user-data.service.ts | 12 +++++----- .../user/models/user/user.controller.ts | 8 +++---- .../generic/user/models/user/user.service.ts | 5 ++++- .../supporting/payment/payment.module.ts | 9 +++++++- .../payment/services/fee.service.ts | 12 +++------- .../payment/services/special-code.service.ts | 22 ++++++++++++++++++- .../payment/services/transaction-helper.ts | 1 + 10 files changed, 60 insertions(+), 32 deletions(-) diff --git a/src/shared/auth/jwt-payload.interface.ts b/src/shared/auth/jwt-payload.interface.ts index 31ca0a98bd..27a0b929ad 100644 --- a/src/shared/auth/jwt-payload.interface.ts +++ b/src/shared/auth/jwt-payload.interface.ts @@ -7,5 +7,6 @@ export interface JwtPayload { address?: string; // user/wallet address role: UserRole; blockchains?: Blockchain[]; + specialCodes?: number[]; // special code id's ip: string; } diff --git a/src/subdomains/generic/user/models/auth/auth.service.ts b/src/subdomains/generic/user/models/auth/auth.service.ts index f62dcaf3cf..4c5bdf8d91 100644 --- a/src/subdomains/generic/user/models/auth/auth.service.ts +++ b/src/subdomains/generic/user/models/auth/auth.service.ts @@ -24,7 +24,7 @@ import { RefService } from 'src/subdomains/core/referral/process/ref.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 { FeeService } from 'src/subdomains/supporting/payment/services/fee.service'; +import { SpecialCodeService } from 'src/subdomains/supporting/payment/services/special-code.service'; import { CustodyProviderService } from '../custody-provider/custody-provider.service'; import { KycType, UserData, UserDataStatus } from '../user-data/user-data.entity'; import { UserDataService } from '../user-data/user-data.service'; @@ -71,7 +71,7 @@ export class AuthService { private readonly cryptoService: CryptoService, private readonly lightningService: LightningService, private readonly refService: RefService, - private readonly feeService: FeeService, + private readonly specialCodeService: SpecialCodeService, private readonly userDataService: UserDataService, private readonly notificationService: NotificationService, private readonly ipLogService: IpLogService, @@ -187,7 +187,7 @@ export class AuthService { try { if (dto.specialCode || dto.discountCode) - await this.feeService.addSpecialCodeUser(user, dto.specialCode ?? dto.discountCode); + await this.specialCodeService.addSpecialCodeUser(user, dto.specialCode ?? dto.discountCode); } catch (e) { this.logger.warn(`Error while adding specialCode in user signIn ${user.id}:`, e); } @@ -394,6 +394,7 @@ export class AuthService { role: user.role, account: user.userData.id, blockchains: user.blockchains, + specialCodes: user.userData.specialCodeList, ip, }; return this.jwtService.sign(payload); @@ -404,6 +405,7 @@ export class AuthService { role: UserRole.ACCOUNT, account: userData.id, blockchains: [], + specialCodes: userData.specialCodeList, ip, }; return this.jwtService.sign(payload); diff --git a/src/subdomains/generic/user/models/user-data/user-data.entity.ts b/src/subdomains/generic/user/models/user-data/user-data.entity.ts index 3385c19ffd..14e5401097 100644 --- a/src/subdomains/generic/user/models/user-data/user-data.entity.ts +++ b/src/subdomains/generic/user/models/user-data/user-data.entity.ts @@ -297,7 +297,7 @@ export class UserData extends IEntity { // Fee / Discounts @Column({ length: 256, nullable: true }) - individualFees?: string; // semicolon separated id's + specialCodes?: string; // semicolon separated id's // CT @Column({ length: 256, nullable: true }) @@ -398,9 +398,9 @@ export class UserData extends IEntity { }; } - addFee(feeId: number): UpdateResult { + addSpecialCode(specialCodeId: number): UpdateResult { const update: Partial = { - individualFees: !this.individualFees ? feeId.toString() : `${this.individualFees};${feeId}`, + specialCodes: !this.specialCodes ? specialCodeId.toString() : `${this.specialCodes};${specialCodeId}`, }; Object.assign(this, update); @@ -408,9 +408,9 @@ export class UserData extends IEntity { return [this.id, update]; } - removeFee(feeId: number): UpdateResult { + removeSpecialCode(specialCodeId: number): UpdateResult { const update: Partial = { - individualFees: this.individualFeeList.filter((id) => id !== feeId).join(';'), + specialCodes: this.specialCodeList.filter((id) => id !== specialCodeId).join(';'), }; Object.assign(this, update); @@ -476,8 +476,8 @@ export class UserData extends IEntity { return this.kycType === KycType.DFX; } - get individualFeeList(): number[] | undefined { - return this.individualFees?.split(';')?.map(Number); + get specialCodeList(): number[] | undefined { + return this.specialCodes?.split(';')?.map(Number); } get kycClientList(): number[] { diff --git a/src/subdomains/generic/user/models/user-data/user-data.service.ts b/src/subdomains/generic/user/models/user-data/user-data.service.ts index d67d7367f1..ad65005622 100644 --- a/src/subdomains/generic/user/models/user-data/user-data.service.ts +++ b/src/subdomains/generic/user/models/user-data/user-data.service.ts @@ -678,15 +678,15 @@ export class UserDataService { // --- FEES --- // async addFee(userData: UserData, feeId: number): Promise { - if (userData.individualFeeList?.includes(feeId)) return; + if (userData.specialCodeList?.includes(feeId)) return; - await this.userDataRepo.update(...userData.addFee(feeId)); + await this.userDataRepo.update(...userData.addSpecialCode(feeId)); } async removeFee(userData: UserData, feeId: number): Promise { - if (!userData.individualFeeList?.includes(feeId)) throw new BadRequestException('Discount code already removed'); + if (!userData.specialCodeList?.includes(feeId)) throw new BadRequestException('Discount code already removed'); - await this.userDataRepo.update(...userData.removeFee(feeId)); + await this.userDataRepo.update(...userData.removeSpecialCode(feeId)); } // --- VOLUMES --- // @@ -765,7 +765,7 @@ export class UserDataService { slave.relatedAccountRelations.length > 0 && `relatedAccountRelations ${slave.relatedAccountRelations.map((a) => a.id)}`, slave.kycSteps.length && `kycSteps ${slave.kycSteps.map((k) => k.id)}`, - slave.individualFees && `individualFees ${slave.individualFees}`, + slave.specialCodes && `individualFees ${slave.specialCodes}`, slave.kycClients && `kycClients ${slave.kycClients}`, slave.supportIssues.length > 0 && `supportIssues ${slave.supportIssues.map((s) => s.id)}`, ] @@ -808,7 +808,7 @@ export class UserDataService { master.relatedAccountRelations = master.relatedAccountRelations.concat(slave.relatedAccountRelations); master.kycSteps = master.kycSteps.concat(slave.kycSteps); master.supportIssues = master.supportIssues.concat(slave.supportIssues); - slave.individualFeeList?.forEach((fee) => !master.individualFeeList?.includes(fee) && master.addFee(fee)); + slave.specialCodeList?.forEach((s) => !master.specialCodeList?.includes(s) && master.addSpecialCode(s)); slave.kycClientList.forEach((kc) => !master.kycClientList.includes(kc) && master.addKycClient(kc)); // copy all documents diff --git a/src/subdomains/generic/user/models/user/user.controller.ts b/src/subdomains/generic/user/models/user/user.controller.ts index c81e989cc2..e813a6bc8b 100644 --- a/src/subdomains/generic/user/models/user/user.controller.ts +++ b/src/subdomains/generic/user/models/user/user.controller.ts @@ -18,7 +18,7 @@ import { RoleGuard } from 'src/shared/auth/role.guard'; import { UserRole } from 'src/shared/auth/user-role.enum'; import { HistoryFilter, HistoryFilterKey } from 'src/subdomains/core/history/dto/history-filter.dto'; import { KycInputDataDto } from 'src/subdomains/generic/kyc/dto/input/kyc-data.dto'; -import { FeeService } from 'src/subdomains/supporting/payment/services/fee.service'; +import { SpecialCodeService } from 'src/subdomains/supporting/payment/services/special-code.service'; import { AuthService } from '../auth/auth.service'; import { AuthResponseDto } from '../auth/dto/auth-response.dto'; import { UserDataService } from '../user-data/user-data.service'; @@ -42,7 +42,7 @@ export class UserController { constructor( private readonly userService: UserService, private readonly authService: AuthService, - private readonly feeService: FeeService, + private readonly specialCodeService: SpecialCodeService, private readonly userDataService: UserDataService, ) {} @@ -82,7 +82,7 @@ export class UserController { async addDiscountCode(@GetJwt() jwt: JwtPayload, @Query('code') code: string): Promise { const user = await this.userService.getUser(jwt.user, { userData: true, wallet: true }); - return this.feeService.addSpecialCodeUser(user, code); + return this.specialCodeService.addSpecialCodeUser(user, code); } @Put('specialCodes') @@ -92,7 +92,7 @@ export class UserController { async addSpecialCode(@GetJwt() jwt: JwtPayload, @Query('code') code: string): Promise { const user = await this.userService.getUser(jwt.user, { userData: true, wallet: true }); - return this.feeService.addSpecialCodeUser(user, code); + return this.specialCodeService.addSpecialCodeUser(user, code); } @Post('change') diff --git a/src/subdomains/generic/user/models/user/user.service.ts b/src/subdomains/generic/user/models/user/user.service.ts index 57d55c2340..04b5c6700c 100644 --- a/src/subdomains/generic/user/models/user/user.service.ts +++ b/src/subdomains/generic/user/models/user/user.service.ts @@ -26,6 +26,7 @@ import { CardBankName, IbanBankName } from 'src/subdomains/supporting/bank/bank/ import { InternalFeeDto } from 'src/subdomains/supporting/payment/dto/fee.dto'; import { PaymentMethod } from 'src/subdomains/supporting/payment/dto/payment-method.enum'; import { FeeService } from 'src/subdomains/supporting/payment/services/fee.service'; +import { SpecialCodeService } from 'src/subdomains/supporting/payment/services/special-code.service'; import { Between, FindOptionsRelations, Not } from 'typeorm'; import { SignUpDto } from '../auth/dto/auth-credentials.dto'; import { CustodyProvider } from '../custody-provider/custody-provider.entity'; @@ -60,6 +61,7 @@ export class UserService { private readonly languageService: LanguageService, private readonly fiatService: FiatService, private readonly siftService: SiftService, + private readonly specialCodeService: SpecialCodeService, ) {} async getAllUser(): Promise { @@ -192,7 +194,7 @@ export class UserService { userIsActive && (await this.userRepo.setUserRef(user, userData?.kycLevel)); try { - if (specialCode) await this.feeService.addSpecialCodeUser(user, specialCode); + if (specialCode) await this.specialCodeService.addSpecialCodeUser(user, specialCode); if (usedRef || wallet) await this.feeService.addCustomSignUpFees(user, user.usedRef); } catch (e) { this.logger.warn(`Error while adding specialCode to new user ${user.id}:`, e); @@ -404,6 +406,7 @@ export class UserService { to, txVolume: undefined, specialCodes: [], + specialCodeIds: user.userData.specialCodeList, allowCachedBlockchainFee: true, bankIn, bankOut, diff --git a/src/subdomains/supporting/payment/payment.module.ts b/src/subdomains/supporting/payment/payment.module.ts index 20085da8aa..f244d6e1a5 100644 --- a/src/subdomains/supporting/payment/payment.module.ts +++ b/src/subdomains/supporting/payment/payment.module.ts @@ -58,6 +58,13 @@ import { TransactionModule } from './transaction.module'; SpecialCodeRepository, SpecialCodeService, ], - exports: [TransactionHelper, FeeService, SwissQRService, TransactionRequestService, SpecialExternalAccountService], + exports: [ + TransactionHelper, + FeeService, + SwissQRService, + TransactionRequestService, + SpecialExternalAccountService, + SpecialCodeService, + ], }) export class PaymentModule {} diff --git a/src/subdomains/supporting/payment/services/fee.service.ts b/src/subdomains/supporting/payment/services/fee.service.ts index 8f519b0196..d99a4b5a51 100644 --- a/src/subdomains/supporting/payment/services/fee.service.ts +++ b/src/subdomains/supporting/payment/services/fee.service.ts @@ -62,6 +62,7 @@ export interface FeeRequestBase { to: Active; txVolume?: number; specialCodes: string[]; + specialCodeIds: number[]; allowCachedBlockchainFee: boolean; } @@ -187,14 +188,6 @@ export class FeeService implements OnModuleInit { } } - async addSpecialCodeUser(user: User, specialCode: string): Promise { - const cachedFee = await this.getFeeBySpecialCode(specialCode); - - await this.feeRepo.update(...cachedFee.increaseUsage(user.userData.accountType, user.wallet)); - - await this.userDataService.addFee(user.userData, cachedFee.id); - } - async addFeeInternal(userData: UserData, feeId: number): Promise { const cachedFee = await this.getFee(feeId); @@ -409,7 +402,7 @@ export class FeeService implements OnModuleInit { const wallet = request.user?.wallet; const userDataId = request.user?.userData?.id; - const discountFeeIds = request.user?.userData?.individualFeeList ?? []; + const discountFeeIds = request.user?.userData?.specialCodeList ?? []; const userFees = await this.getAllFees().then((fees) => fees.filter( @@ -419,6 +412,7 @@ export class FeeService implements OnModuleInit { !f.specialCode) || discountFeeIds.includes(f.id) || request.specialCodes.includes(f.code.code) || + request.specialCodeIds.includes(f.code.id) || (f.wallet && f.wallet.id === wallet?.id), ), ); diff --git a/src/subdomains/supporting/payment/services/special-code.service.ts b/src/subdomains/supporting/payment/services/special-code.service.ts index 0e21898103..1a5eac0368 100644 --- a/src/subdomains/supporting/payment/services/special-code.service.ts +++ b/src/subdomains/supporting/payment/services/special-code.service.ts @@ -1,11 +1,18 @@ import { BadRequestException, Injectable } from '@nestjs/common'; +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 { CreateSpecialCodeDto } from '../dto/input/create-special-code.dto'; import { SpecialCode } from '../entities/special-code.entity'; +import { FeeRepository } from '../repositories/fee.repository'; import { SpecialCodeRepository } from '../repositories/special-code.repository'; @Injectable() export class SpecialCodeService { - constructor(private readonly specialCodeRepo: SpecialCodeRepository) {} + constructor( + private readonly specialCodeRepo: SpecialCodeRepository, + private readonly userDataService: UserDataService, + private readonly feeRepo: FeeRepository, + ) {} async createSpecialCode(dto: CreateSpecialCodeDto): Promise { const existing = await this.specialCodeRepo.findOneBy({ code: dto.code }); @@ -15,4 +22,17 @@ export class SpecialCodeService { return this.specialCodeRepo.save(specialCode); } + + async addSpecialCodeUser(user: User, specialCode: string): Promise { + const cachedSpecialCode = await this.specialCodeRepo.findOneCached(specialCode, { + where: { code: specialCode }, + relations: { fees: true }, + }); + + for (const fee of cachedSpecialCode.fees) { + await this.feeRepo.update(...fee.increaseUsage(user.userData.accountType, user.wallet)); + } + + await this.userDataService.addFee(user.userData, cachedSpecialCode.id); + } } diff --git a/src/subdomains/supporting/payment/services/transaction-helper.ts b/src/subdomains/supporting/payment/services/transaction-helper.ts index 8ee05372d2..3a912fecea 100644 --- a/src/subdomains/supporting/payment/services/transaction-helper.ts +++ b/src/subdomains/supporting/payment/services/transaction-helper.ts @@ -392,6 +392,7 @@ export class TransactionHelper implements OnModuleInit { to, txVolume: txVolumeChf, specialCodes, + specialCodeIds: user?.userData.specialCodeList, allowCachedBlockchainFee, }; From b68b1825a7fdffee3d0cac8232b6acb4824d8db7 Mon Sep 17 00:00:00 2001 From: Yannick1712 <52333989+Yannick1712@users.noreply.github.com> Date: Thu, 16 Jan 2025 21:47:07 +0100 Subject: [PATCH 6/7] [DEV-3501] add usedSpecialCodes in paymentRequest --- .../core/buy-crypto/routes/buy/buy.controller.ts | 8 +++++++- .../core/buy-crypto/routes/swap/swap.controller.ts | 8 +++++++- src/subdomains/core/sell-crypto/route/sell.controller.ts | 8 +++++++- .../payment/entities/transaction-request.entity.ts | 3 +++ .../payment/services/transaction-request.service.ts | 2 ++ 5 files changed, 26 insertions(+), 3 deletions(-) diff --git a/src/subdomains/core/buy-crypto/routes/buy/buy.controller.ts b/src/subdomains/core/buy-crypto/routes/buy/buy.controller.ts index 3967b2a041..e09412b7fc 100644 --- a/src/subdomains/core/buy-crypto/routes/buy/buy.controller.ts +++ b/src/subdomains/core/buy-crypto/routes/buy/buy.controller.ts @@ -353,7 +353,13 @@ export class BuyController { ), }; - await this.transactionRequestService.create(TransactionRequestType.Buy, dto, buyDto, user.id); + await this.transactionRequestService.create( + TransactionRequestType.Buy, + dto, + buyDto, + user.id, + user.userData.specialCodes, + ); return buyDto; } diff --git a/src/subdomains/core/buy-crypto/routes/swap/swap.controller.ts b/src/subdomains/core/buy-crypto/routes/swap/swap.controller.ts index 806558c16b..0bb1467e1e 100644 --- a/src/subdomains/core/buy-crypto/routes/swap/swap.controller.ts +++ b/src/subdomains/core/buy-crypto/routes/swap/swap.controller.ts @@ -291,7 +291,13 @@ export class SwapController { error, }; - await this.transactionRequestService.create(TransactionRequestType.Swap, dto, swapDto, user.id); + await this.transactionRequestService.create( + TransactionRequestType.Swap, + dto, + swapDto, + user.id, + user.userData.specialCodes, + ); return swapDto; } diff --git a/src/subdomains/core/sell-crypto/route/sell.controller.ts b/src/subdomains/core/sell-crypto/route/sell.controller.ts index 6fc3e1457c..b5967decc6 100644 --- a/src/subdomains/core/sell-crypto/route/sell.controller.ts +++ b/src/subdomains/core/sell-crypto/route/sell.controller.ts @@ -294,7 +294,13 @@ export class SellController { error, }; - await this.transactionRequestService.create(TransactionRequestType.Sell, dto, sellDto, user.id); + await this.transactionRequestService.create( + TransactionRequestType.Sell, + dto, + sellDto, + user.id, + user.userData.specialCodes, + ); return sellDto; } diff --git a/src/subdomains/supporting/payment/entities/transaction-request.entity.ts b/src/subdomains/supporting/payment/entities/transaction-request.entity.ts index 3f7a71254f..961818277c 100644 --- a/src/subdomains/supporting/payment/entities/transaction-request.entity.ts +++ b/src/subdomains/supporting/payment/entities/transaction-request.entity.ts @@ -15,6 +15,9 @@ export class TransactionRequest extends IEntity { @Column() type: TransactionRequestType; + @Column({ length: 256, nullable: true }) + usedSpecialCodes?: string; // Semicolon separated id's + @Column({ type: 'integer' }) routeId: number; diff --git a/src/subdomains/supporting/payment/services/transaction-request.service.ts b/src/subdomains/supporting/payment/services/transaction-request.service.ts index ea556be05a..b5530b6d2b 100644 --- a/src/subdomains/supporting/payment/services/transaction-request.service.ts +++ b/src/subdomains/supporting/payment/services/transaction-request.service.ts @@ -28,6 +28,7 @@ export class TransactionRequestService { request: GetBuyPaymentInfoDto | GetSellPaymentInfoDto | GetSwapPaymentInfoDto, response: BuyPaymentInfoDto | SellPaymentInfoDto | SwapPaymentInfoDto, userId: number, + usedSpecialCodes: string, ): Promise { try { // create the entity @@ -47,6 +48,7 @@ export class TransactionRequestService { networkFee: response.fees.network, totalFee: response.fees.total, user: { id: userId }, + usedSpecialCodes, }); let sourceCurrencyName: string; From e7a40c1d78475912df65dbd0027cf78d91f3b131 Mon Sep 17 00:00:00 2001 From: Yannick1712 <52333989+Yannick1712@users.noreply.github.com> Date: Thu, 16 Jan 2025 21:48:56 +0100 Subject: [PATCH 7/7] [DEV-3501] fix unit tests --- .../generic/user/models/user/tests/user.service.spec.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/subdomains/generic/user/models/user/tests/user.service.spec.ts b/src/subdomains/generic/user/models/user/tests/user.service.spec.ts index 1038333b9b..4d9a107eb9 100644 --- a/src/subdomains/generic/user/models/user/tests/user.service.spec.ts +++ b/src/subdomains/generic/user/models/user/tests/user.service.spec.ts @@ -10,6 +10,7 @@ import { TestUtil } from 'src/shared/utils/test.util'; import { TfaService } from 'src/subdomains/generic/kyc/services/tfa.service'; import { NotificationService } from 'src/subdomains/supporting/notification/services/notification.service'; import { FeeService } from 'src/subdomains/supporting/payment/services/fee.service'; +import { SpecialCodeService } from 'src/subdomains/supporting/payment/services/special-code.service'; import { UserDataRepository } from '../../user-data/user-data.repository'; import { UserDataService } from '../../user-data/user-data.service'; import { WalletService } from '../../wallet/wallet.service'; @@ -32,6 +33,7 @@ describe('UserService', () => { let fiatService: FiatService; let tfaService: TfaService; let siftService: SiftService; + let specialCodeService: SpecialCodeService; beforeEach(async () => { userRepo = createMock(); @@ -47,6 +49,7 @@ describe('UserService', () => { fiatService = createMock(); tfaService = createMock(); siftService = createMock(); + specialCodeService = createMock(); const module: TestingModule = await Test.createTestingModule({ providers: [ @@ -64,6 +67,7 @@ describe('UserService', () => { { provide: FiatService, useValue: fiatService }, { provide: TfaService, useValue: tfaService }, { provide: SiftService, useValue: siftService }, + { provide: SpecialCodeService, useValue: specialCodeService }, TestUtil.provideConfig(), ], }).compile();