diff --git a/migration/1773675740273-AddUserDataPhoneCallAccepted.js b/migration/1773675740273-AddUserDataPhoneCallAccepted.js new file mode 100644 index 0000000000..74de829295 --- /dev/null +++ b/migration/1773675740273-AddUserDataPhoneCallAccepted.js @@ -0,0 +1,26 @@ +/** + * @typedef {import('typeorm').MigrationInterface} MigrationInterface + * @typedef {import('typeorm').QueryRunner} QueryRunner + */ + +/** + * @class + * @implements {MigrationInterface} + */ +module.exports = class AddUserDataPhoneCallAccepted1773675740273 { + name = 'AddUserDataPhoneCallAccepted1773675740273' + + /** + * @param {QueryRunner} queryRunner + */ + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "user_data" ADD "phoneCallAccepted" bit`); + } + + /** + * @param {QueryRunner} queryRunner + */ + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "user_data" DROP COLUMN "phoneCallAccepted"`); + } +} diff --git a/migration/1773700400000-AddEurBankPercentFee.js b/migration/1773700400000-AddEurBankPercentFee.js new file mode 100644 index 0000000000..472520e5c4 --- /dev/null +++ b/migration/1773700400000-AddEurBankPercentFee.js @@ -0,0 +1,16 @@ +module.exports = class AddEurBankPercentFee1773700400000 { + name = 'AddEurBankPercentFee1773700400000'; + + async up(queryRunner) { + await queryRunner.query(` + INSERT INTO "dbo"."fee" ("label", "type", "rate", "fixed", "blockchainFactor", "payoutRefBonus", "active", "fiats") + VALUES ('Bank Fee EUR 0.5%', 'Bank', 0.005, 0, 1, 1, 1, '2') + `); + } + + async down(queryRunner) { + await queryRunner.query(` + DELETE FROM "dbo"."fee" WHERE "label" = 'Bank Fee EUR 0.5%' AND "type" = 'Bank' + `); + } +}; diff --git a/src/config/config.ts b/src/config/config.ts index 39f46326c0..3855dc390e 100644 --- a/src/config/config.ts +++ b/src/config/config.ts @@ -990,7 +990,7 @@ export class Configuration { }), }, internetComputer: { - internetComputerHost: 'https://ic0.app', + internetComputerHost: 'https://icp-api.io', internetComputerRosettaApiUrl: process.env.ICP_ROSETTA_API_URL ?? 'https://rosetta-api.internetcomputer.org', internetComputerWalletSeed: process.env.ICP_WALLET_SEED, internetComputerLedgerCanisterId: 'ryjl3-tyaaa-aaaaa-aaaba-cai', 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 3c4e3b4c48..f7267d8a6a 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 @@ -667,7 +667,7 @@ export class BuyCryptoService { } async resetAmlCheck(id: number): Promise { - const entity = await this.buyCryptoRepo.findOneBy({ id }); + const entity = await this.buyCryptoRepo.findOne({ where: { id }, relations: { chargebackOutput: true } }); if (!entity) throw new NotFoundException('BuyCrypto not found'); if (entity.isComplete || entity.batch || entity.chargebackOutput?.isComplete) throw new BadRequestException('BuyCrypto is already complete or payout initiated'); diff --git a/src/subdomains/core/history/dto/transaction-refund.dto.ts b/src/subdomains/core/history/dto/transaction-refund.dto.ts index 341d013484..04554bd05c 100644 --- a/src/subdomains/core/history/dto/transaction-refund.dto.ts +++ b/src/subdomains/core/history/dto/transaction-refund.dto.ts @@ -36,12 +36,12 @@ export class CreditorDataDto { } export class TransactionRefundDto { - @ApiProperty({ description: 'Refund address or refund IBAN' }) - @IsNotEmpty() + @ApiPropertyOptional({ description: 'Refund address or refund IBAN' }) + @IsOptional() @IsString() @Transform(Util.trimAll) @Transform(Util.sanitize) - refundTarget: string; + refundTarget?: string; @ApiPropertyOptional({ description: 'Creditor data (required for bank refunds)' }) @IsOptional() diff --git a/src/subdomains/core/history/mappers/transaction-dto.mapper.ts b/src/subdomains/core/history/mappers/transaction-dto.mapper.ts index f68aa527f6..b7d154eae3 100644 --- a/src/subdomains/core/history/mappers/transaction-dto.mapper.ts +++ b/src/subdomains/core/history/mappers/transaction-dto.mapper.ts @@ -370,6 +370,8 @@ export class TransactionDtoMapper { entity.bankFeeAmount != null ? Util.roundReadable(entity.bankFeeAmount * referencePrice, feeAmountType(entity.inputAssetEntity)) : null, + bankFixed: null, + bankPercent: null, fixed: entity.absoluteFeeAmount != null ? Util.roundReadable(entity.absoluteFeeAmount * referencePrice, feeAmountType(entity.inputAssetEntity)) 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 ccc329943a..02fa3f1225 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 @@ -254,6 +254,9 @@ export class UserData extends IEntity { @Column({ length: 256, nullable: true }) phoneCallStatus: PhoneCallStatus; + @Column({ nullable: true }) + phoneCallAccepted?: boolean; + @Column({ type: 'datetime2', nullable: true }) tradeApprovalDate?: Date; @@ -507,11 +510,11 @@ export class UserData extends IEntity { language: dto.language ?? this.language, currency: dto.currency ?? this.currency, phoneCallTimes: dto.preferredPhoneTimes ? dto.preferredPhoneTimes.join(';') : undefined, - phoneCallStatus: dto.acceptCall - ? PhoneCallStatus.REPEAT - : dto.acceptCall === false + phoneCallStatus: + dto.acceptCall === false && (!this.phoneCallStatus || PhoneCallStatus.UNAVAILABLE === this.phoneCallStatus) ? PhoneCallStatus.USER_REJECTED : undefined, + phoneCallAccepted: dto.acceptCall, }; Object.assign(this, update); diff --git a/src/subdomains/generic/user/models/user-data/user-data.enum.ts b/src/subdomains/generic/user/models/user-data/user-data.enum.ts index a1474b1105..dc6643891c 100644 --- a/src/subdomains/generic/user/models/user-data/user-data.enum.ts +++ b/src/subdomains/generic/user/models/user-data/user-data.enum.ts @@ -33,6 +33,7 @@ export enum PhoneCallStatus { FAILED = 'Failed', COMPLETED = 'Completed', SUSPICIOUS = 'Suspicious', + MANUAL_CHECK = 'ManualCheck', } export enum KycLevel { diff --git a/src/subdomains/generic/user/models/user/dto/user-dto.mapper.ts b/src/subdomains/generic/user/models/user/dto/user-dto.mapper.ts index 02c31a8f22..f8a808a336 100644 --- a/src/subdomains/generic/user/models/user/dto/user-dto.mapper.ts +++ b/src/subdomains/generic/user/models/user/dto/user-dto.mapper.ts @@ -10,8 +10,7 @@ import { Util } from 'src/shared/utils/util'; import { UserData } from '../../user-data/user-data.entity'; import { User } from '../user.entity'; import { UserProfileDto } from './user-profile.dto'; -import { ReferralDto, UserAddressDto, UserV2Dto, VolumesDto } from './user-v2.dto'; -import { PhoneCallStatusMapper } from './user.dto'; +import { PhoneCallStatusMapper, ReferralDto, UserAddressDto, UserV2Dto, VolumesDto } from './user-v2.dto'; export class UserDtoMapper { static mapUser(userData: UserData, activeUserId?: number): UserV2Dto { @@ -29,6 +28,7 @@ export class UserDtoMapper { hash: userData.kycHash, level: userData.kycLevelDisplay, dataComplete: userData.isDataComplete, + phoneCallAccepted: userData.phoneCallAccepted, phoneCallStatus: userData.phoneCallStatus ? PhoneCallStatusMapper[userData.phoneCallStatus] : undefined, preferredPhoneTimes: userData.phoneCallTimesObject, }, diff --git a/src/subdomains/generic/user/models/user/dto/user-v2.dto.ts b/src/subdomains/generic/user/models/user/dto/user-v2.dto.ts index a59c9e527b..ec680e9dcc 100644 --- a/src/subdomains/generic/user/models/user/dto/user-v2.dto.ts +++ b/src/subdomains/generic/user/models/user/dto/user-v2.dto.ts @@ -9,9 +9,27 @@ import { FiatDto } from 'src/shared/models/fiat/dto/fiat.dto'; import { LanguageDto } from 'src/shared/models/language/dto/language.dto'; import { HistoryFilterKey } from 'src/subdomains/core/history/dto/history-filter.dto'; import { AccountType } from '../../user-data/account-type.enum'; -import { KycLevel, PhoneCallPreferredTime } from '../../user-data/user-data.enum'; +import { KycLevel, PhoneCallPreferredTime, PhoneCallStatus } from '../../user-data/user-data.enum'; import { RefPayoutFrequency } from '../user.enum'; -import { TradingLimit, UserPhoneCallStatus, VolumeInformation } from './user.dto'; +import { TradingLimit, VolumeInformation } from './user.dto'; + +export enum UserPhoneCallStatus { + UNAVAILABLE = 'Unavailable', + COMPLETED = 'Completed', + FAILED = 'Failed', +} + +export const PhoneCallStatusMapper: { + [key in PhoneCallStatus]: UserPhoneCallStatus; +} = { + [PhoneCallStatus.REPEAT]: undefined, + [PhoneCallStatus.USER_REJECTED]: undefined, + [PhoneCallStatus.MANUAL_CHECK]: undefined, + [PhoneCallStatus.UNAVAILABLE]: UserPhoneCallStatus.UNAVAILABLE, + [PhoneCallStatus.FAILED]: UserPhoneCallStatus.FAILED, + [PhoneCallStatus.COMPLETED]: UserPhoneCallStatus.COMPLETED, + [PhoneCallStatus.SUSPICIOUS]: UserPhoneCallStatus.FAILED, +}; export class VolumesDto { @ApiProperty({ type: VolumeInformation, description: 'Total buy volume in CHF' }) @@ -108,7 +126,10 @@ export class UserKycDto { @ApiProperty({ enum: PhoneCallPreferredTime, isArray: true }) preferredPhoneTimes: PhoneCallPreferredTime[]; - @ApiProperty({ enum: UserPhoneCallStatus }) + @ApiPropertyOptional() + phoneCallAccepted: boolean; + + @ApiPropertyOptional({ enum: UserPhoneCallStatus }) phoneCallStatus: UserPhoneCallStatus; } diff --git a/src/subdomains/generic/user/models/user/dto/user.dto.ts b/src/subdomains/generic/user/models/user/dto/user.dto.ts index 6ff3686308..d73b86e376 100644 --- a/src/subdomains/generic/user/models/user/dto/user.dto.ts +++ b/src/subdomains/generic/user/models/user/dto/user.dto.ts @@ -3,29 +3,10 @@ import { Fiat } from 'src/shared/models/fiat/fiat.entity'; import { LanguageDto } from 'src/shared/models/language/dto/language.dto'; import { HistoryFilterKey } from 'src/subdomains/core/history/dto/history-filter.dto'; import { AccountType } from '../../user-data/account-type.enum'; -import { KycLevel, KycState, KycStatus, LimitPeriod, PhoneCallStatus } from '../../user-data/user-data.enum'; +import { KycLevel, KycState, KycStatus, LimitPeriod } from '../../user-data/user-data.enum'; import { UserStatus } from '../user.enum'; import { LinkedUserOutDto } from './linked-user.dto'; -export enum UserPhoneCallStatus { - ACCEPTED = 'Accepted', - REJECTED = 'Rejected', - UNAVAILABLE = 'Unavailable', - COMPLETED = 'Completed', - FAILED = 'Failed', -} - -export const PhoneCallStatusMapper: { - [key in PhoneCallStatus]: UserPhoneCallStatus; -} = { - [PhoneCallStatus.REPEAT]: UserPhoneCallStatus.ACCEPTED, - [PhoneCallStatus.USER_REJECTED]: UserPhoneCallStatus.REJECTED, - [PhoneCallStatus.UNAVAILABLE]: UserPhoneCallStatus.UNAVAILABLE, - [PhoneCallStatus.FAILED]: UserPhoneCallStatus.FAILED, - [PhoneCallStatus.COMPLETED]: UserPhoneCallStatus.COMPLETED, - [PhoneCallStatus.SUSPICIOUS]: UserPhoneCallStatus.FAILED, -}; - export class VolumeInformation { @ApiProperty() total: number; diff --git a/src/subdomains/supporting/payment/dto/fee.dto.ts b/src/subdomains/supporting/payment/dto/fee.dto.ts index d39f676cd9..3fbc34bd21 100644 --- a/src/subdomains/supporting/payment/dto/fee.dto.ts +++ b/src/subdomains/supporting/payment/dto/fee.dto.ts @@ -22,6 +22,12 @@ export class FeeDto extends BaseFeeDto { @ApiProperty({ description: 'Bank fee amount' }) bank: number; // final bank fee addition + @ApiPropertyOptional({ description: 'Bank fixed fee amount' }) + bankFixed?: number; + + @ApiPropertyOptional({ description: 'Bank percent fee amount' }) + bankPercent?: number; + @ApiProperty({ description: 'Total fee amount (DFX + bank + network fee)' }) total: number; diff --git a/src/subdomains/supporting/payment/services/transaction-helper.ts b/src/subdomains/supporting/payment/services/transaction-helper.ts index eb9d0b74ad..6280a43a73 100644 --- a/src/subdomains/supporting/payment/services/transaction-helper.ts +++ b/src/subdomains/supporting/payment/services/transaction-helper.ts @@ -232,7 +232,7 @@ export class TransactionHelper implements OnModuleInit { const sourceSpecs = await this.getSourceSpecs(fromReference, specs, PriceValidity.VALID_ONLY); - const { dfx, bank, total } = this.calculateTotalFee( + const { dfx, bank, bankFixed, bankPercent, total } = this.calculateTotalFee( inputReferenceAmount, fee.rate, fee.bankRate, @@ -246,6 +246,8 @@ export class TransactionHelper implements OnModuleInit { total, dfx, bank, + bankFixed, + bankPercent, }; } @@ -823,6 +825,8 @@ export class TransactionHelper implements OnModuleInit { dfx: this.convertFee(sourceFees.dfx, price, to), total: this.convertFee(sourceFees.total, price, to), bank: this.convertFee(sourceFees.bank, price, to), + bankFixed: this.convertFee(sourceFees.bankFixed, price, to), + bankPercent: this.convertFee(sourceFees.bankPercent, price, to), }; return { @@ -915,14 +919,17 @@ export class TransactionHelper implements OnModuleInit { bankRate: number, { fee: { fixed, min, network, networkStart, bankFixed } }: TxSpec, roundingActive: Active, - ): { dfx: number; bank: number; total: number } { - const bank = amount * bankRate + bankFixed; + ): { dfx: number; bank: number; bankFixed: number; bankPercent: number; total: number } { + const bankPercentAmount = amount * bankRate; + const bank = bankPercentAmount + bankFixed; const dfx = Math.max(amount * rate + fixed, min); const total = dfx + bank + network + (networkStart ?? 0); return { dfx: Util.roundReadable(dfx, feeAmountType(roundingActive)), bank: Util.roundReadable(bank, feeAmountType(roundingActive)), + bankFixed: Util.roundReadable(bankFixed, feeAmountType(roundingActive)), + bankPercent: Util.roundReadable(bankPercentAmount, feeAmountType(roundingActive)), total: Util.roundReadable(total, feeAmountType(roundingActive)), }; }