diff --git a/EDGE_REQUIREMENTS.md b/EDGE_REQUIREMENTS.md new file mode 100644 index 0000000000..dda22aa497 --- /dev/null +++ b/EDGE_REQUIREMENTS.md @@ -0,0 +1,214 @@ +# Edge Exchange Provider API Requirements - Tracking + +## Status-Legende +- [ ] Nicht begonnen +- [~] In Arbeit +- [x] Erledigt + +--- + +## Allgemeine Anforderungen (Swap + On/Off Ramp) + +### 1. Chain and Token Identification `[~]` ~70% +**Anforderung:** Quotes/Orders via `chainNetwork` + `tokenId` (Contract Address). Für EVM-Chains zusätzlich `chainNetwork: "evmGeneric"` + `chainId`. + +**Ist-Zustand:** +- Asset-Entity hat `chainId` (Contract Address) und `blockchain` Enum +- EVM Chain-Config mit numerischen chainIds vorhanden (ETH=1, BSC=56, etc.) +- `evmChainId` Feld in `AssetInDto` (numerische EVM Chain ID) +- `EvmUtil.getBlockchain()` Reverse-Lookup (chainId → Blockchain) +- `resolveAsset()` unterstützt `evmChainId` + `chainId` Kombination + +**Offen:** +- [ ] Support für `chainNetwork` als String-Identifier (Edge-Format) +- [ ] Mapping zwischen Edge chain identifiers und DFX Blockchain-Enum + +--- + +### 2. Order Identification and Status Page `[~]` ~70% +**Anforderung:** Unique `orderId` pro Order, abfragbar via unauthenticated API. Status-Page URL: `https://status.provider.com/orderStatus/{orderId}` + +**Ist-Zustand:** +- TransactionRequest hat `uid` als unique identifier +- `GET /transaction/single?uid=...` existiert (unauthenticated) + +**Offen:** +- [ ] Konsistentes `orderId` Feld in allen Responses +- [ ] Status-Page URL bereitstellen (Frontend oder Redirect) +- [ ] Path-Parameter statt Query-Parameter: `GET /api/status/{orderId}` + +--- + +### 3. Error Handling `[~]` ~80% +**Anforderung:** Alle möglichen Fehler gleichzeitig zurückgeben. Hardened error codes: `RegionRestricted`, `AssetUnsupported`, `OverLimitError` (mit min/max in Source UND Destination Asset). + +**Ist-Zustand:** +- QuoteError Enum: `AmountTooLow`, `AmountTooHigh`, `KycRequired`, `LimitExceeded`, `NationalityNotAllowed`, `PaymentMethodNotAllowed`, `RegionRestricted`, `AssetUnsupported` +- `minVolume`/`maxVolume` + `minVolumeTarget`/`maxVolumeTarget` in Quote-Response +- `errors` Array in allen Quote-DTOs (Buy/Sell/Swap) mit `StructuredErrorDto` +- `QuoteErrorUtil.mapToStructuredErrors()` mappt bestehende Errors auf Edge-Format +- Error-Mapping: `NationalityNotAllowed` → `RegionRestricted`, Amount-Errors → `OverLimitError`/`UnderLimitError` mit `sourceAmountLimit`/`destinationAmountLimit` + +**Offen:** +- [ ] Alle Fehler gleichzeitig als Array zurückgeben (aktuell: einzelner Error → Array mit einem Element) + +--- + +### 4. Quoting Requirements `[x]` 100% +**Anforderung:** Bi-directional quoting (Source ODER Destination Amount). + +**Ist-Zustand:** +- XOR-Validation in `GetSwapQuoteDto`: `amount` oder `targetAmount` +- Gleiche Logik in Sell-Quotes +- **Vollständig erfüllt.** + +--- + +### 5. Transaction Status API `[x]` 100% +**Anforderung:** `GET /api/status/{orderId}` mit Status: `pending | processing | infoNeeded | expired | refunded | completed` + +**Ist-Zustand:** +- `GET /transaction/single` existiert (unauthenticated) +- TransactionState hat 14 verschiedene States +- `GET /transaction/status/:orderId` Endpoint (unauthenticated) +- `ProviderTransactionStatus` Enum mit Edge-Format +- `mapToProviderStatus()` mappt alle 14 States auf 6 Edge-Status-Werte +- Lookup via `uid` und `orderUid` + +**Erledigt.** + +--- + +### 6. Reporting API `[~]` ~40% +**Anforderung:** Authentifizierte API für alle Transaktionen. Pagination (start date, end date, count). Felder: orderId, status, dates, source/dest network/token/amount, payin/payout addresses, txids, EVM chainIds. + +**Ist-Zustand:** +- `GET /history/:exportType` mit API-Key Auth +- `GET /transaction/detail` mit Bearer Token +- Date-Filter vorhanden + +**Offen:** +- [ ] Explizite Pagination (startDate, endDate, limit/offset) +- [ ] Response-Format mit allen Edge-Pflichtfeldern +- [ ] `orderId` konsistent (statt `id`/`uid`) +- [ ] EVM `chainId` im Response +- [ ] Source/Dest Network + TokenId + Amount + Addresses + Txids + +--- + +### 7. Account Activation `[ ]` 0% +**Anforderung:** Bei XRP/HBAR: unactivated Addresses erkennen und Aktivierungs-Transaktion senden. + +**Ist-Zustand:** +- XRP und HBAR sind nicht in der Blockchain-Enum + +**Offen:** +- [ ] Prüfen ob XRP/HBAR überhaupt unterstützt werden sollen +- [ ] Falls ja: Activation-Logik implementieren + +--- + +### 8. Affiliate Revenue Withdrawal `[ ]` ~10% +**Anforderung:** Auto-Withdraw innerhalb 24h nach Monatsende. In BTC/ETH/USDC an feste, verifizierte Adresse. + +**Ist-Zustand:** +- Referral/RefReward System vorhanden + +**Offen:** +- [ ] Klären ob DFX Affiliate-Programm mit Edge hat +- [ ] Auto-Withdrawal Mechanismus (Cron-Job) +- [ ] Fixed address Verwaltung mit 2FA/Email-Verification bei Änderung + +--- + +## Zusätzliche Anforderungen für Fiat On/Off Ramp + +### 9. User Authentication `[~]` ~60% +**Anforderung:** Auth via cryptographic random `authKey`. Auto-Account-Erstellung wenn authKey nicht existiert. KYC-Info via API. + +**Ist-Zustand:** +- Wallet-Signature basierte Auth (`POST /auth`) +- Auto-Signup bei `POST /auth/signUp` + +**Offen:** +- [ ] `authKey`-basierte Authentifizierung (random key statt wallet signature) +- [ ] Auto-Create Account bei unbekanntem authKey +- [ ] authKey in Quote/Order-Endpoints akzeptieren + +--- + +### 10. Regional and Fiat Currency Support `[~]` ~40% +**Anforderung:** Quote-Request mit Region (Country/Province) und Fiat-Currency. Proper Errors bei unsupported. + +**Ist-Zustand:** +- IP-basierte Country-Detection +- Fiat-Currency Support in Quotes + +**Offen:** +- [ ] Expliziter `region`/`country` Parameter in Quote-Requests +- [ ] Expliziter `fiatCurrency` Parameter +- [ ] Spezifische Errors: `RegionNotSupported`, `CurrencyNotSupported` + +--- + +### 11. KYC Information `[~]` ~60% +**Anforderung:** KYC-Daten (Name, Address, Phone, Email) via API submitten (kein Widget). + +**Ist-Zustand:** +- UserData-Update mit firstname, surname, street, phone, mail etc. +- KYC-Controller vorhanden + +**Offen:** +- [ ] Dedizierter KYC-Submission-Endpoint für Edge +- [ ] Sicherstellen dass alle Felder via API setzbar sind (ohne Widget) + +--- + +### 12. Bank Information `[~]` ~40% +**Anforderung:** Bank-Info (Account Number, IBAN, Routing Number) via API submitten. + +**Ist-Zustand:** +- BankData-Controller existiert (Admin-only) +- CreateBankDataDto: IBAN, BIC, Name + +**Offen:** +- [ ] User-zugängliche API (nicht nur Admin) +- [ ] `accountNumber` und `routingNumber` Felder (für US-Markt) + +--- + +### 13. Verification `[ ]` ~10% +**Anforderung:** KYC-Verification-Codes (Phone/Email) über API senden. Edge informieren wenn KYC-Info fehlt/veraltet. + +**Ist-Zustand:** +- KYC via externen Provider (SumSub) + +**Offen:** +- [ ] Phone-Verification-Code senden via API +- [ ] Email-Verification-Code senden via API +- [ ] Verification-Code validieren via API +- [ ] "KYC info missing/outdated" Status in API-Response + +--- + +### 14. Widgets `[~]` ~30% +**Anforderung:** Widgets müssen Return-URIs unterstützen, damit Edge Webviews schließen kann. + +**Ist-Zustand:** +- Redirect-Logik in Auth vorhanden + +**Offen:** +- [ ] Return-URI Parameter für alle Widget-Flows +- [ ] Callback nach Widget-Completion + +--- + +### 15. Off-Ramp Flow `[~]` ~50% +**Anforderung:** No-Widget Off-Ramp: Crypto-Address + Expiration-Time bereitstellen. + +**Ist-Zustand:** +- Sell-Flow mit Deposit-Address vorhanden + +**Offen:** +- [ ] Expiration-Time für Deposit-Addresses +- [ ] Sicherstellen dass Flow komplett ohne Widget funktioniert diff --git a/src/integration/blockchain/shared/evm/evm.util.ts b/src/integration/blockchain/shared/evm/evm.util.ts index f5fe4cc4ea..fc3450a9d8 100644 --- a/src/integration/blockchain/shared/evm/evm.util.ts +++ b/src/integration/blockchain/shared/evm/evm.util.ts @@ -57,6 +57,13 @@ export class EvmUtil { return this.blockchainToChainIdMap.get(blockchain); } + static getBlockchain(chainId: number): Blockchain | undefined { + for (const [blockchain, id] of this.blockchainToChainIdMap.entries()) { + if (id === chainId) return blockchain; + } + return undefined; + } + static createWallet({ seed, index }: WalletAccount, provider?: ethers.providers.JsonRpcProvider): ethers.Wallet { const wallet = ethers.Wallet.fromMnemonic(seed, this.getPathFor(index)); return provider ? wallet.connect(provider) : wallet; diff --git a/src/shared/models/asset/dto/asset.dto.ts b/src/shared/models/asset/dto/asset.dto.ts index aa936e0ab4..d9d195cdff 100644 --- a/src/shared/models/asset/dto/asset.dto.ts +++ b/src/shared/models/asset/dto/asset.dto.ts @@ -1,5 +1,5 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { IsEnum, IsInt, IsNotEmpty, IsString, ValidateIf } from 'class-validator'; +import { IsEnum, IsInt, IsNotEmpty, IsOptional, IsString, ValidateIf } from 'class-validator'; import { Blockchain } from 'src/integration/blockchain/shared/enums/blockchain.enum'; import { AssetCategory, AssetType } from '../asset.entity'; @@ -77,7 +77,7 @@ export class AssetDto { export class AssetInDto { @ApiPropertyOptional() @IsNotEmpty() - @ValidateIf((a: AssetInDto) => Boolean(a.id || !(a.chainId || a.blockchain))) + @ValidateIf((a: AssetInDto) => Boolean(a.id || !(a.chainId || a.blockchain || a.evmChainId))) @IsInt() id?: number; @@ -89,9 +89,14 @@ export class AssetInDto { @ApiPropertyOptional() @IsNotEmpty() - @ValidateIf((a: AssetInDto) => Boolean(a.blockchain || !a.id)) + @ValidateIf((a: AssetInDto) => Boolean(a.blockchain || (!a.id && !a.evmChainId))) @IsEnum(Blockchain) blockchain?: Blockchain; + + @ApiPropertyOptional({ description: 'Numeric EVM chain ID (e.g. 1, 56, 137)' }) + @IsOptional() + @IsInt() + evmChainId?: number; } export class AssetLimitsDto { diff --git a/src/shared/services/payment-info.service.ts b/src/shared/services/payment-info.service.ts index cd7f3e57c8..1bfdde4490 100644 --- a/src/shared/services/payment-info.service.ts +++ b/src/shared/services/payment-info.service.ts @@ -1,4 +1,5 @@ import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common'; +import { EvmUtil } from 'src/integration/blockchain/shared/evm/evm.util'; import { Config, Environment } from 'src/config/config'; import { AmlRule } from 'src/subdomains/core/aml/enums/aml-rule.enum'; import { CreateBuyDto } from 'src/subdomains/core/buy-crypto/routes/buy/dto/create-buy.dto'; @@ -129,8 +130,15 @@ export class PaymentInfoService { } async resolveAsset(asset: Asset): Promise { - return asset.id - ? this.assetService.getAssetById(asset.id) - : this.assetService.getAssetByChainId(asset.blockchain, asset.chainId); + if (asset.id) return this.assetService.getAssetById(asset.id); + + let blockchain = asset.blockchain; + const evmChainId = (asset as any).evmChainId as number | undefined; + if (evmChainId && !blockchain) { + blockchain = EvmUtil.getBlockchain(evmChainId); + if (!blockchain) throw new BadRequestException(`Unsupported EVM chain ID: ${evmChainId}`); + } + + return this.assetService.getAssetByChainId(blockchain, asset.chainId); } } 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 52bb482272..dea5892aaa 100644 --- a/src/subdomains/core/buy-crypto/routes/buy/buy.controller.ts +++ b/src/subdomains/core/buy-crypto/routes/buy/buy.controller.ts @@ -31,6 +31,7 @@ import { VirtualIbanDto } from 'src/subdomains/supporting/bank/virtual-iban/dto/ import { VirtualIbanMapper } from 'src/subdomains/supporting/bank/virtual-iban/dto/virtual-iban.mapper'; import { VirtualIbanService } from 'src/subdomains/supporting/bank/virtual-iban/virtual-iban.service'; import { CryptoPaymentMethod, FiatPaymentMethod } from 'src/subdomains/supporting/payment/dto/payment-method.enum'; +import { QuoteErrorUtil } from 'src/subdomains/supporting/payment/dto/transaction-helper/quote-error.util'; import { TransactionRequestStatus } from 'src/subdomains/supporting/payment/entities/transaction-request.entity'; import { SwissQRService } from 'src/subdomains/supporting/payment/services/swiss-qr.service'; import { TransactionHelper } from 'src/subdomains/supporting/payment/services/transaction-helper'; @@ -136,6 +137,7 @@ export class BuyController { priceSteps, isValid, error, + errors: QuoteErrorUtil.mapToStructuredErrors(error, minVolume, minVolumeTarget, maxVolume, maxVolumeTarget), }; } diff --git a/src/subdomains/core/buy-crypto/routes/buy/dto/buy-quote.dto.ts b/src/subdomains/core/buy-crypto/routes/buy/dto/buy-quote.dto.ts index ed3f39fb24..eb4e789794 100644 --- a/src/subdomains/core/buy-crypto/routes/buy/dto/buy-quote.dto.ts +++ b/src/subdomains/core/buy-crypto/routes/buy/dto/buy-quote.dto.ts @@ -1,6 +1,7 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { FeeDto } from 'src/subdomains/supporting/payment/dto/fee.dto'; import { QuoteError } from 'src/subdomains/supporting/payment/dto/transaction-helper/quote-error.enum'; +import { StructuredErrorDto } from 'src/subdomains/supporting/payment/dto/transaction-helper/structured-error.dto'; import { PriceStep } from 'src/subdomains/supporting/pricing/domain/entities/price'; export class BuyQuoteDto { @@ -45,4 +46,7 @@ export class BuyQuoteDto { @ApiPropertyOptional({ enum: QuoteError, description: 'Error message in case isValid is false' }) error?: QuoteError; + + @ApiPropertyOptional({ type: [StructuredErrorDto], description: 'Structured errors array' }) + errors?: StructuredErrorDto[]; } diff --git a/src/subdomains/core/buy-crypto/routes/swap/dto/swap-quote.dto.ts b/src/subdomains/core/buy-crypto/routes/swap/dto/swap-quote.dto.ts index 77729eb240..05dbc8a30c 100644 --- a/src/subdomains/core/buy-crypto/routes/swap/dto/swap-quote.dto.ts +++ b/src/subdomains/core/buy-crypto/routes/swap/dto/swap-quote.dto.ts @@ -1,6 +1,7 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { FeeDto } from 'src/subdomains/supporting/payment/dto/fee.dto'; import { QuoteError } from 'src/subdomains/supporting/payment/dto/transaction-helper/quote-error.enum'; +import { StructuredErrorDto } from 'src/subdomains/supporting/payment/dto/transaction-helper/structured-error.dto'; import { PriceStep } from 'src/subdomains/supporting/pricing/domain/entities/price'; export class SwapQuoteDto { @@ -42,4 +43,7 @@ export class SwapQuoteDto { @ApiPropertyOptional({ enum: QuoteError, description: 'Error message in case isValid is false' }) error?: QuoteError; + + @ApiPropertyOptional({ type: [StructuredErrorDto], description: 'Structured errors array' }) + errors?: StructuredErrorDto[]; } 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 c9efedb5a1..3cf66d5a4b 100644 --- a/src/subdomains/core/buy-crypto/routes/swap/swap.controller.ts +++ b/src/subdomains/core/buy-crypto/routes/swap/swap.controller.ts @@ -33,6 +33,7 @@ import { UnsignedTxDto } from 'src/subdomains/core/sell-crypto/route/dto/unsigne import { UserService } from 'src/subdomains/generic/user/models/user/user.service'; import { DepositDtoMapper } from 'src/subdomains/supporting/address-pool/deposit/dto/deposit-dto.mapper'; import { CryptoPaymentMethod } from 'src/subdomains/supporting/payment/dto/payment-method.enum'; +import { QuoteErrorUtil } from 'src/subdomains/supporting/payment/dto/transaction-helper/quote-error.util'; import { TransactionDto } from 'src/subdomains/supporting/payment/dto/transaction.dto'; import { TransactionHelper } from 'src/subdomains/supporting/payment/services/transaction-helper'; import { TransactionRequestService } from 'src/subdomains/supporting/payment/services/transaction-request.service'; @@ -137,6 +138,7 @@ export class SwapController { priceSteps, isValid, error, + errors: QuoteErrorUtil.mapToStructuredErrors(error, minVolume, minVolumeTarget, maxVolume, maxVolumeTarget), }; } diff --git a/src/subdomains/core/history/controllers/transaction.controller.ts b/src/subdomains/core/history/controllers/transaction.controller.ts index 756524710a..469347630f 100644 --- a/src/subdomains/core/history/controllers/transaction.controller.ts +++ b/src/subdomains/core/history/controllers/transaction.controller.ts @@ -56,6 +56,7 @@ import { TransactionHelper } from 'src/subdomains/supporting/payment/services/tr import { TransactionRequestService } from 'src/subdomains/supporting/payment/services/transaction-request.service'; import { TransactionService } from 'src/subdomains/supporting/payment/services/transaction.service'; import { FindOptionsRelations } from 'typeorm'; +import { TransactionStatusDto, mapToProviderStatus } from '../../../supporting/payment/dto/transaction-status.dto'; import { TransactionDetailDto, TransactionDto, @@ -147,6 +148,20 @@ export class TransactionController { return dto; } + @Get('status/:orderId') + @ApiOkResponse({ type: TransactionStatusDto }) + async getTransactionStatus(@Param('orderId') orderId: string): Promise { + const tx = await this.getTransaction({ uid: orderId, orderUid: orderId }); + + const dto = await this.getTransactionDto(tx); + if (!dto) throw new NotFoundException('Transaction not found'); + + return { + orderId, + status: mapToProviderStatus(dto.state), + }; + } + @Put('csv') @ApiOkResponse() @ApiOperation({ description: 'Initiate CSV history export' }) diff --git a/src/subdomains/core/sell-crypto/route/dto/sell-quote.dto.ts b/src/subdomains/core/sell-crypto/route/dto/sell-quote.dto.ts index f60e6603e3..2ed6351316 100644 --- a/src/subdomains/core/sell-crypto/route/dto/sell-quote.dto.ts +++ b/src/subdomains/core/sell-crypto/route/dto/sell-quote.dto.ts @@ -1,6 +1,7 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { FeeDto } from 'src/subdomains/supporting/payment/dto/fee.dto'; import { QuoteError } from 'src/subdomains/supporting/payment/dto/transaction-helper/quote-error.enum'; +import { StructuredErrorDto } from 'src/subdomains/supporting/payment/dto/transaction-helper/structured-error.dto'; import { PriceStep } from 'src/subdomains/supporting/pricing/domain/entities/price'; export class SellQuoteDto { @@ -45,4 +46,7 @@ export class SellQuoteDto { @ApiPropertyOptional({ enum: QuoteError, description: 'Error message in case isValid is false' }) error?: QuoteError; + + @ApiPropertyOptional({ type: [StructuredErrorDto], description: 'Structured errors array' }) + errors?: StructuredErrorDto[]; } diff --git a/src/subdomains/core/sell-crypto/route/sell.controller.ts b/src/subdomains/core/sell-crypto/route/sell.controller.ts index 19e7f5ef15..301353beb3 100644 --- a/src/subdomains/core/sell-crypto/route/sell.controller.ts +++ b/src/subdomains/core/sell-crypto/route/sell.controller.ts @@ -28,6 +28,7 @@ import { Util } from 'src/shared/utils/util'; import { UserService } from 'src/subdomains/generic/user/models/user/user.service'; import { DepositDtoMapper } from 'src/subdomains/supporting/address-pool/deposit/dto/deposit-dto.mapper'; import { CryptoPaymentMethod, FiatPaymentMethod } from 'src/subdomains/supporting/payment/dto/payment-method.enum'; +import { QuoteErrorUtil } from 'src/subdomains/supporting/payment/dto/transaction-helper/quote-error.util'; import { TransactionDto } from 'src/subdomains/supporting/payment/dto/transaction.dto'; import { TransactionHelper } from 'src/subdomains/supporting/payment/services/transaction-helper'; import { TransactionRequestService } from 'src/subdomains/supporting/payment/services/transaction-request.service'; @@ -139,6 +140,7 @@ export class SellController { priceSteps, isValid, error, + errors: QuoteErrorUtil.mapToStructuredErrors(error, minVolume, minVolumeTarget, maxVolume, maxVolumeTarget), }; } diff --git a/src/subdomains/supporting/payment/dto/transaction-helper/quote-error.enum.ts b/src/subdomains/supporting/payment/dto/transaction-helper/quote-error.enum.ts index f947c87230..56e4e9499a 100644 --- a/src/subdomains/supporting/payment/dto/transaction-helper/quote-error.enum.ts +++ b/src/subdomains/supporting/payment/dto/transaction-helper/quote-error.enum.ts @@ -13,4 +13,6 @@ export enum QuoteError { IBAN_CURRENCY_MISMATCH = 'IbanCurrencyMismatch', RECOMMENDATION_REQUIRED = 'RecommendationRequired', EMAIL_REQUIRED = 'EmailRequired', + REGION_RESTRICTED = 'RegionRestricted', + ASSET_UNSUPPORTED = 'AssetUnsupported', } diff --git a/src/subdomains/supporting/payment/dto/transaction-helper/quote-error.util.ts b/src/subdomains/supporting/payment/dto/transaction-helper/quote-error.util.ts new file mode 100644 index 0000000000..31e352a569 --- /dev/null +++ b/src/subdomains/supporting/payment/dto/transaction-helper/quote-error.util.ts @@ -0,0 +1,29 @@ +import { QuoteError } from './quote-error.enum'; +import { StructuredErrorDto } from './structured-error.dto'; + +export class QuoteErrorUtil { + static mapToStructuredErrors( + error: QuoteError | undefined, + minVolume?: number, + minVolumeTarget?: number, + maxVolume?: number, + maxVolumeTarget?: number, + ): StructuredErrorDto[] { + if (!error) return []; + + switch (error) { + case QuoteError.NATIONALITY_NOT_ALLOWED: + return [{ error: QuoteError.REGION_RESTRICTED }]; + + case QuoteError.AMOUNT_TOO_LOW: + return [{ error: 'UnderLimitError', sourceAmountLimit: minVolume, destinationAmountLimit: minVolumeTarget }]; + + case QuoteError.AMOUNT_TOO_HIGH: + case QuoteError.LIMIT_EXCEEDED: + return [{ error: 'OverLimitError', sourceAmountLimit: maxVolume, destinationAmountLimit: maxVolumeTarget }]; + + default: + return [{ error }]; + } + } +} diff --git a/src/subdomains/supporting/payment/dto/transaction-helper/structured-error.dto.ts b/src/subdomains/supporting/payment/dto/transaction-helper/structured-error.dto.ts new file mode 100644 index 0000000000..2cda75fa26 --- /dev/null +++ b/src/subdomains/supporting/payment/dto/transaction-helper/structured-error.dto.ts @@ -0,0 +1,12 @@ +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; + +export class StructuredErrorDto { + @ApiProperty({ description: 'Error code' }) + error: string; + + @ApiPropertyOptional({ description: 'Source amount limit' }) + sourceAmountLimit?: number; + + @ApiPropertyOptional({ description: 'Destination amount limit' }) + destinationAmountLimit?: number; +} diff --git a/src/subdomains/supporting/payment/dto/transaction-status.dto.ts b/src/subdomains/supporting/payment/dto/transaction-status.dto.ts new file mode 100644 index 0000000000..03c74ec612 --- /dev/null +++ b/src/subdomains/supporting/payment/dto/transaction-status.dto.ts @@ -0,0 +1,41 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { TransactionState } from './transaction.dto'; + +export enum ProviderTransactionStatus { + PENDING = 'pending', + PROCESSING = 'processing', + INFO_NEEDED = 'infoNeeded', + EXPIRED = 'expired', + REFUNDED = 'refunded', + COMPLETED = 'completed', +} + +export class TransactionStatusDto { + @ApiProperty({ description: 'Order ID' }) + orderId: string; + + @ApiProperty({ enum: ProviderTransactionStatus, description: 'Simplified transaction status' }) + status: ProviderTransactionStatus; +} + +const TransactionStateToProviderStatus: Record = { + [TransactionState.CREATED]: ProviderTransactionStatus.PENDING, + [TransactionState.WAITING_FOR_PAYMENT]: ProviderTransactionStatus.PENDING, + [TransactionState.PROCESSING]: ProviderTransactionStatus.PROCESSING, + [TransactionState.LIQUIDITY_PENDING]: ProviderTransactionStatus.PROCESSING, + [TransactionState.CHECK_PENDING]: ProviderTransactionStatus.PROCESSING, + [TransactionState.PAYOUT_IN_PROGRESS]: ProviderTransactionStatus.PROCESSING, + [TransactionState.UNASSIGNED]: ProviderTransactionStatus.PROCESSING, + [TransactionState.KYC_REQUIRED]: ProviderTransactionStatus.INFO_NEEDED, + [TransactionState.LIMIT_EXCEEDED]: ProviderTransactionStatus.INFO_NEEDED, + [TransactionState.FAILED]: ProviderTransactionStatus.EXPIRED, + [TransactionState.FEE_TOO_HIGH]: ProviderTransactionStatus.EXPIRED, + [TransactionState.PRICE_UNDETERMINABLE]: ProviderTransactionStatus.EXPIRED, + [TransactionState.RETURN_PENDING]: ProviderTransactionStatus.REFUNDED, + [TransactionState.RETURNED]: ProviderTransactionStatus.REFUNDED, + [TransactionState.COMPLETED]: ProviderTransactionStatus.COMPLETED, +}; + +export function mapToProviderStatus(state: TransactionState): ProviderTransactionStatus { + return TransactionStateToProviderStatus[state] ?? ProviderTransactionStatus.PROCESSING; +}