diff --git a/migration/1770600000000-AddAktionariatResponse.js b/migration/1770600000000-AddAktionariatResponse.js new file mode 100644 index 0000000000..30fc400c33 --- /dev/null +++ b/migration/1770600000000-AddAktionariatResponse.js @@ -0,0 +1,26 @@ +/** + * @typedef {import('typeorm').MigrationInterface} MigrationInterface + * @typedef {import('typeorm').QueryRunner} QueryRunner + */ + +/** + * @class + * @implements {MigrationInterface} + */ +module.exports = class AddAktionariatResponse1770600000000 { + name = 'AddAktionariatResponse1770600000000'; + + /** + * @param {QueryRunner} queryRunner + */ + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "dbo"."transaction_request" ADD "aktionariatResponse" nvarchar(MAX)`); + } + + /** + * @param {QueryRunner} queryRunner + */ + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "dbo"."transaction_request" DROP COLUMN "aktionariatResponse"`); + } +}; diff --git a/src/integration/blockchain/realunit/dto/realunit-broker.dto.ts b/src/integration/blockchain/realunit/dto/realunit-broker.dto.ts index 33ed336354..7591b9bb37 100644 --- a/src/integration/blockchain/realunit/dto/realunit-broker.dto.ts +++ b/src/integration/blockchain/realunit/dto/realunit-broker.dto.ts @@ -1,25 +1,25 @@ import { ApiProperty } from '@nestjs/swagger'; export class BrokerbotPriceDto { - @ApiProperty({ description: 'Current price per share in CHF (18 decimals formatted)' }) + @ApiProperty({ description: 'Current price per share in CHF' }) pricePerShare: string; - @ApiProperty({ description: 'Raw price per share in wei' }) - pricePerShareRaw: string; + @ApiProperty({ description: 'Available shares for purchase' }) + availableShares: number; } export class BrokerbotBuyPriceDto { @ApiProperty({ description: 'Number of shares' }) shares: number; - @ApiProperty({ description: 'Total cost in CHF (18 decimals formatted)' }) + @ApiProperty({ description: 'Total cost in CHF' }) totalPrice: string; - @ApiProperty({ description: 'Raw total cost in wei' }) - totalPriceRaw: string; - @ApiProperty({ description: 'Price per share in CHF' }) pricePerShare: string; + + @ApiProperty({ description: 'Available shares for purchase' }) + availableShares: number; } export class BrokerbotSharesDto { @@ -31,6 +31,9 @@ export class BrokerbotSharesDto { @ApiProperty({ description: 'Price per share in CHF' }) pricePerShare: string; + + @ApiProperty({ description: 'Available shares for purchase' }) + availableShares: number; } export class BrokerbotInfoDto { @@ -51,4 +54,7 @@ export class BrokerbotInfoDto { @ApiProperty({ description: 'Whether selling is enabled' }) sellingEnabled: boolean; + + @ApiProperty({ description: 'Available shares for purchase' }) + availableShares: number; } diff --git a/src/integration/blockchain/realunit/realunit-blockchain.service.ts b/src/integration/blockchain/realunit/realunit-blockchain.service.ts index bcf7c55c2b..e6fc15d707 100644 --- a/src/integration/blockchain/realunit/realunit-blockchain.service.ts +++ b/src/integration/blockchain/realunit/realunit-blockchain.service.ts @@ -1,10 +1,7 @@ -import { Injectable, OnModuleInit } from '@nestjs/common'; -import { ModuleRef } from '@nestjs/core'; -import { Contract } from 'ethers'; -import { Blockchain } from '../shared/enums/blockchain.enum'; -import { EvmClient } from '../shared/evm/evm-client'; -import { EvmUtil } from '../shared/evm/evm.util'; -import { BlockchainRegistryService } from '../shared/services/blockchain-registry.service'; +import { Injectable } from '@nestjs/common'; +import { GetConfig } from 'src/config/config'; +import { HttpService } from 'src/shared/services/http.service'; +import { AsyncCache, CacheItemResetPeriod } from 'src/shared/utils/async-cache'; import { BrokerbotBuyPriceDto, BrokerbotInfoDto, @@ -17,86 +14,105 @@ const BROKERBOT_ADDRESS = '0xCFF32C60B87296B8c0c12980De685bEd6Cb9dD6d'; const REALU_TOKEN_ADDRESS = '0x553C7f9C780316FC1D34b8e14ac2465Ab22a090B'; const ZCHF_ADDRESS = '0xb58e61c3098d85632df34eecfb899a1ed80921cb'; -// Contract ABIs -const BROKERBOT_ABI = [ - 'function getPrice() public view returns (uint256)', - 'function getBuyPrice(uint256 shares) public view returns (uint256)', - 'function getShares(uint256 money) public view returns (uint256)', - 'function settings() public view returns (uint256)', -]; +interface AktionariatPriceResponse { + priceInCHF: number; + priceInEUR: number; + availableShares: number; +} -@Injectable() -export class RealUnitBlockchainService implements OnModuleInit { - private registryService: BlockchainRegistryService; +interface PaymentInstructionsRequest { + currency: string; + address: string; + shares: number; + price: number; +} - constructor(private readonly moduleRef: ModuleRef) {} +interface PaymentInstructionsResponse { + [key: string]: unknown; +} - private getEvmClient(): EvmClient { - return this.registryService.getClient(Blockchain.ETHEREUM) as EvmClient; +@Injectable() +export class RealUnitBlockchainService { + private readonly priceCache = new AsyncCache(CacheItemResetPeriod.EVERY_30_SECONDS); + + constructor(private readonly http: HttpService) {} + + private async fetchPrice(): Promise { + return this.priceCache.get( + 'price', + async () => { + const { url, key } = GetConfig().blockchain.realunit.api; + return this.http.post(`${url}/realunit/directinvestment/getPrice`, null, { + headers: { 'x-api-key': key }, + }); + }, + undefined, + true, + ); } - private getBrokerbotContract(): Contract { - return new Contract(BROKERBOT_ADDRESS, BROKERBOT_ABI, this.getEvmClient().wallet); + async getRealUnitPrice(): Promise { + const { priceInCHF } = await this.fetchPrice(); + return priceInCHF; } - onModuleInit() { - this.registryService = this.moduleRef.get(BlockchainRegistryService, { strict: false }); + async getRealUnitPriceEur(): Promise { + const { priceInEUR } = await this.fetchPrice(); + return priceInEUR; } - async getRealUnitPrice(): Promise { - const price = await this.getBrokerbotContract().getPrice(); - return EvmUtil.fromWeiAmount(price); + async requestPaymentInstructions(request: PaymentInstructionsRequest): Promise { + const { url, key } = GetConfig().blockchain.realunit.api; + return this.http.post(`${url}/realunit/directinvestment/requestPaymentInstructions`, request, { + headers: { 'x-api-key': key }, + }); } // --- Brokerbot Methods --- async getBrokerbotPrice(): Promise { - const priceRaw = await this.getBrokerbotContract().getPrice(); + const { priceInCHF, availableShares } = await this.fetchPrice(); return { - pricePerShare: EvmUtil.fromWeiAmount(priceRaw).toString(), - pricePerShareRaw: priceRaw.toString(), + pricePerShare: priceInCHF.toString(), + availableShares, }; } async getBrokerbotBuyPrice(shares: number): Promise { - const contract = this.getBrokerbotContract(); - const [totalPriceRaw, pricePerShareRaw] = await Promise.all([contract.getBuyPrice(shares), contract.getPrice()]); + const { priceInCHF, availableShares } = await this.fetchPrice(); + const totalPrice = priceInCHF * shares; return { shares, - totalPrice: EvmUtil.fromWeiAmount(totalPriceRaw).toString(), - totalPriceRaw: totalPriceRaw.toString(), - pricePerShare: EvmUtil.fromWeiAmount(pricePerShareRaw).toString(), + totalPrice: totalPrice.toString(), + pricePerShare: priceInCHF.toString(), + availableShares, }; } async getBrokerbotShares(amountChf: string): Promise { - const contract = this.getBrokerbotContract(); - const amountWei = EvmUtil.toWeiAmount(parseFloat(amountChf)); - const [shares, pricePerShareRaw] = await Promise.all([contract.getShares(amountWei), contract.getPrice()]); + const { priceInCHF, availableShares } = await this.fetchPrice(); + const shares = Math.floor(parseFloat(amountChf) / priceInCHF); return { amount: amountChf, - shares: shares.toNumber(), - pricePerShare: EvmUtil.fromWeiAmount(pricePerShareRaw).toString(), + shares, + pricePerShare: priceInCHF.toString(), + availableShares, }; } async getBrokerbotInfo(): Promise { - const contract = this.getBrokerbotContract(); - const [priceRaw, settings] = await Promise.all([contract.getPrice(), contract.settings()]); - - // Settings bitmask: bit 0 = buying enabled, bit 1 = selling enabled - const buyingEnabled = (settings.toNumber() & 1) === 1; - const sellingEnabled = (settings.toNumber() & 2) === 2; + const { priceInCHF, availableShares } = await this.fetchPrice(); return { brokerbotAddress: BROKERBOT_ADDRESS, tokenAddress: REALU_TOKEN_ADDRESS, baseCurrencyAddress: ZCHF_ADDRESS, - pricePerShare: EvmUtil.fromWeiAmount(priceRaw).toString(), - buyingEnabled, - sellingEnabled, + pricePerShare: priceInCHF.toString(), + buyingEnabled: availableShares > 0, + sellingEnabled: true, + availableShares, }; } } diff --git a/src/shared/services/http.service.ts b/src/shared/services/http.service.ts index 766a02b1a1..96293cb7e2 100644 --- a/src/shared/services/http.service.ts +++ b/src/shared/services/http.service.ts @@ -40,6 +40,7 @@ const MOCK_RESPONSES: { pattern: RegExp; response: any }[] = [ }, { pattern: /login\.microsoftonline\.com/, response: { access_token: 'mock-token', expires_in: 3600 } }, { pattern: /api\.applicationinsights\.io/, response: { tables: [{ name: 'PrimaryResult', columns: [], rows: [] }] } }, + { pattern: /aktionariat\.com/, response: { priceInCHF: 1.57, priceInEUR: 1.71, availableShares: 65488 } }, ]; @Injectable() diff --git a/src/subdomains/core/aml/services/aml-helper.service.ts b/src/subdomains/core/aml/services/aml-helper.service.ts index 124b9e9a68..db4100b70c 100644 --- a/src/subdomains/core/aml/services/aml-helper.service.ts +++ b/src/subdomains/core/aml/services/aml-helper.service.ts @@ -270,7 +270,6 @@ export class AmlHelperService { errors.push(AmlError.ACCOUNT_IBAN_BLACKLISTED); const bank = banks.find((b) => b.iban === entity.bankTx.accountIban); - if (bank?.sctInst && !entity.userData.olkypayAllowed) errors.push(AmlError.INSTANT_NOT_ALLOWED); if (bank?.sctInst && !entity.outputAsset.instantBuyable) errors.push(AmlError.ASSET_NOT_INSTANT_BUYABLE); if (bank && !bank.amlEnabled) errors.push(AmlError.BANK_DEACTIVATED); } else if (entity.checkoutTx) { diff --git a/src/subdomains/supporting/payment/entities/transaction-request.entity.ts b/src/subdomains/supporting/payment/entities/transaction-request.entity.ts index 96c76dc794..3406861e15 100644 --- a/src/subdomains/supporting/payment/entities/transaction-request.entity.ts +++ b/src/subdomains/supporting/payment/entities/transaction-request.entity.ts @@ -95,6 +95,9 @@ export class TransactionRequest extends IEntity { @Column({ length: 'MAX', nullable: true }) siftResponse?: string; + @Column({ length: 'MAX', nullable: true }) + aktionariatResponse?: string; + @OneToOne(() => Transaction, (transaction) => transaction.request, { nullable: true }) transaction?: Transaction; diff --git a/src/subdomains/supporting/payment/services/transaction-request.service.ts b/src/subdomains/supporting/payment/services/transaction-request.service.ts index 02219fe062..00f42dc3de 100644 --- a/src/subdomains/supporting/payment/services/transaction-request.service.ts +++ b/src/subdomains/supporting/payment/services/transaction-request.service.ts @@ -326,8 +326,11 @@ export class TransactionRequestService { } } - async confirmTransactionRequest(txRequest: TransactionRequest): Promise { - await this.transactionRequestRepo.update(txRequest.id, { status: TransactionRequestStatus.WAITING_FOR_PAYMENT }); + async confirmTransactionRequest(txRequest: TransactionRequest, aktionariatResponse?: string): Promise { + await this.transactionRequestRepo.update(txRequest.id, { + status: TransactionRequestStatus.WAITING_FOR_PAYMENT, + ...(aktionariatResponse && { aktionariatResponse }), + }); } async getActiveDepositAddresses(created: Date, blockchain: Blockchain): Promise { @@ -349,6 +352,19 @@ export class TransactionRequestService { .then((transactionRequests) => transactionRequests.map((deposit) => deposit.address)); } + async getByAssetId(assetId: number, limit = 50, offset = 0): Promise { + return this.transactionRequestRepo.find({ + where: [ + { type: TransactionRequestType.BUY, targetId: assetId, isComplete: false }, + { type: TransactionRequestType.SELL, sourceId: assetId, isComplete: false }, + ], + order: { created: 'DESC' }, + take: limit, + skip: offset, + relations: { user: true }, + }); + } + // --- HELPER METHODS --- // private currentStatus(entity: TransactionRequest, expiryDate: Date): TransactionRequestStatus { diff --git a/src/subdomains/supporting/payment/services/transaction.service.ts b/src/subdomains/supporting/payment/services/transaction.service.ts index 3d7df91a47..3e0f3cd3c7 100644 --- a/src/subdomains/supporting/payment/services/transaction.service.ts +++ b/src/subdomains/supporting/payment/services/transaction.service.ts @@ -256,6 +256,33 @@ export class TransactionService { .getRawMany(); } + async getByAssetId(assetId: number, limit = 50, offset = 0): Promise { + return this.repo + .createQueryBuilder('transaction') + .select('transaction') + .leftJoinAndSelect('transaction.request', 'request') + .leftJoinAndSelect('transaction.user', 'user') + .leftJoinAndSelect('transaction.userData', 'userData') + .where('transaction.type IS NOT NULL') + .andWhere( + new Brackets((qb) => + qb + .where('request.type = :buyType AND request.targetId = :assetId', { + buyType: 'Buy', + assetId, + }) + .orWhere('request.type = :sellType AND request.sourceId = :assetId', { + sellType: 'Sell', + assetId, + }), + ), + ) + .orderBy('transaction.created', 'DESC') + .take(limit) + .skip(offset) + .getMany(); + } + async getTransactionByKey(key: string, value: any): Promise { return this.repo .createQueryBuilder('transaction') diff --git a/src/subdomains/supporting/pricing/services/integration/pricing-realunit.service.ts b/src/subdomains/supporting/pricing/services/integration/pricing-realunit.service.ts index 53c6d9b1af..c705e04ccb 100644 --- a/src/subdomains/supporting/pricing/services/integration/pricing-realunit.service.ts +++ b/src/subdomains/supporting/pricing/services/integration/pricing-realunit.service.ts @@ -9,6 +9,7 @@ import { PricingProvider } from './pricing-provider'; export class PricingRealUnitService extends PricingProvider implements OnModuleInit { private static readonly REALU = 'REALU'; private static readonly ZCHF = 'ZCHF'; + private static readonly EUR = 'EUR'; private static readonly ALLOWED_ASSETS = [PricingRealUnitService.REALU, PricingRealUnitService.ZCHF]; @@ -32,4 +33,17 @@ export class PricingRealUnitService extends PricingProvider implements OnModuleI return Price.create(from, to, Util.round(assetPrice, 8)); } + + async getDirectEurPrice(from: string, to: string): Promise { + const isRealuToEur = from === PricingRealUnitService.REALU && to === PricingRealUnitService.EUR; + const isEurToRealu = from === PricingRealUnitService.EUR && to === PricingRealUnitService.REALU; + + if (!isRealuToEur && !isEurToRealu) return undefined; + + const eurPrice = await this.realunitService.getRealUnitPriceEur(); + if (eurPrice == null) return undefined; + + const price = isRealuToEur ? 1 / eurPrice : eurPrice; + return Price.create(from, to, Util.round(price, 8)); + } } diff --git a/src/subdomains/supporting/pricing/services/pricing.service.ts b/src/subdomains/supporting/pricing/services/pricing.service.ts index 23e69d98a3..7f5e8cbbda 100644 --- a/src/subdomains/supporting/pricing/services/pricing.service.ts +++ b/src/subdomains/supporting/pricing/services/pricing.service.ts @@ -187,6 +187,10 @@ export class PricingService implements OnModuleInit { try { if (activesEqual(from, to)) return Price.create(from.name, to.name, 1); + // Direct EUR price for REALU from Aktionariat API + const directPrice = await this.realunitService.getDirectEurPrice(from.name, to.name).catch(() => undefined); + if (directPrice) return directPrice; + const shouldUpdate = validity !== PriceValidity.ANY; const [fromRules, toRules] = await Promise.all([ diff --git a/src/subdomains/supporting/realunit/controllers/realunit.controller.ts b/src/subdomains/supporting/realunit/controllers/realunit.controller.ts index 23327e13b6..9b4f82e2ac 100644 --- a/src/subdomains/supporting/realunit/controllers/realunit.controller.ts +++ b/src/subdomains/supporting/realunit/controllers/realunit.controller.ts @@ -34,6 +34,7 @@ import { } from 'src/integration/blockchain/realunit/dto/realunit-broker.dto'; import { Blockchain } from 'src/integration/blockchain/shared/enums/blockchain.enum'; import { GetJwt } from 'src/shared/auth/get-jwt.decorator'; +import { IpGuard } from 'src/shared/auth/ip.guard'; import { JwtPayload } from 'src/shared/auth/jwt-payload.interface'; import { RoleGuard } from 'src/shared/auth/role.guard'; import { UserActiveGuard } from 'src/shared/auth/user-active.guard'; @@ -45,6 +46,7 @@ import { BalancePdfService } from '../../balance/services/balance-pdf.service'; import { TxStatementType } from '../../payment/dto/transaction-helper/tx-statement-details.dto'; import { SwissQRService } from '../../payment/services/swiss-qr.service'; import { TransactionHelper } from '../../payment/services/transaction-helper'; +import { RealUnitAdminQueryDto, RealUnitQuoteDto, RealUnitTransactionDto } from '../dto/realunit-admin.dto'; import { RealUnitBalancePdfDto, RealUnitMultiReceiptPdfDto, @@ -284,6 +286,14 @@ export class RealUnitController { return this.realunitService.getPaymentInfo(user, dto); } + @Put('buy/:id/confirm') + @ApiBearerAuth() + @UseGuards(AuthGuard(), RoleGuard(UserRole.USER), IpGuard) + @ApiOkResponse() + async confirmBuy(@GetJwt() jwt: JwtPayload, @Param('id') id: string): Promise { + await this.realunitService.confirmBuy(jwt.user, +id); + } + // --- Sell Payment Info Endpoints --- @Put('sell') @@ -407,6 +417,26 @@ export class RealUnitController { // --- Admin Endpoints --- + @Get('admin/quotes') + @ApiBearerAuth() + @ApiExcludeEndpoint() + @ApiOperation({ summary: 'Get RealUnit quotes' }) + @ApiOkResponse({ type: [RealUnitQuoteDto], description: 'List of open RealUnit requests (quotes)' }) + @UseGuards(AuthGuard(), RoleGuard(UserRole.ADMIN), UserActiveGuard()) + async getAdminQuotes(@Query() { limit, offset }: RealUnitAdminQueryDto): Promise { + return this.realunitService.getAdminQuotes(limit, offset); + } + + @Get('admin/transactions') + @ApiBearerAuth() + @ApiExcludeEndpoint() + @ApiOperation({ summary: 'Get RealUnit transactions' }) + @ApiOkResponse({ type: [RealUnitTransactionDto], description: 'List of completed RealUnit transactions' }) + @UseGuards(AuthGuard(), RoleGuard(UserRole.ADMIN), UserActiveGuard()) + async getAdminTransactions(@Query() { limit, offset }: RealUnitAdminQueryDto): Promise { + return this.realunitService.getAdminTransactions(limit, offset); + } + @Put('admin/registration/:kycStepId/forward') @ApiBearerAuth() @ApiExcludeEndpoint() diff --git a/src/subdomains/supporting/realunit/dto/realunit-admin.dto.ts b/src/subdomains/supporting/realunit/dto/realunit-admin.dto.ts new file mode 100644 index 0000000000..b9bc9dd1d7 --- /dev/null +++ b/src/subdomains/supporting/realunit/dto/realunit-admin.dto.ts @@ -0,0 +1,73 @@ +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; +import { IsNumber, IsOptional, Min } from 'class-validator'; +import { TransactionRequestStatus, TransactionRequestType } from '../../payment/entities/transaction-request.entity'; +import { TransactionTypeInternal } from '../../payment/entities/transaction.entity'; + +export class RealUnitAdminQueryDto { + @ApiPropertyOptional({ description: 'Number of items to return' }) + @IsOptional() + @IsNumber() + @Min(1) + @Type(() => Number) + limit?: number; + + @ApiPropertyOptional({ description: 'Number of items to skip' }) + @IsOptional() + @IsNumber() + @Min(0) + @Type(() => Number) + offset?: number; +} + +export class RealUnitQuoteDto { + @ApiProperty({ description: 'Quote ID' }) + id: number; + + @ApiProperty({ description: 'Quote UID' }) + uid: string; + + @ApiProperty({ description: 'Quote type', enum: TransactionRequestType }) + type: TransactionRequestType; + + @ApiProperty({ description: 'Quote status', enum: TransactionRequestStatus }) + status: TransactionRequestStatus; + + @ApiProperty({ description: 'Quote amount' }) + amount: number; + + @ApiProperty({ description: 'Estimated amount' }) + estimatedAmount: number; + + @ApiProperty({ description: 'Creation date' }) + created: Date; + + @ApiPropertyOptional({ description: 'User address' }) + userAddress?: string; +} + +export class RealUnitTransactionDto { + @ApiProperty({ description: 'Transaction ID' }) + id: number; + + @ApiProperty({ description: 'Transaction UID' }) + uid: string; + + @ApiProperty({ description: 'Transaction type', enum: TransactionTypeInternal }) + type: TransactionTypeInternal; + + @ApiProperty({ description: 'Amount in CHF' }) + amountInChf: number; + + @ApiProperty({ description: 'Assets involved' }) + assets: string; + + @ApiProperty({ description: 'Creation date' }) + created: Date; + + @ApiPropertyOptional({ description: 'Output date' }) + outputDate?: Date; + + @ApiPropertyOptional({ description: 'User address' }) + userAddress?: string; +} diff --git a/src/subdomains/supporting/realunit/realunit.service.ts b/src/subdomains/supporting/realunit/realunit.service.ts index babdcd6a5b..2054f35c5c 100644 --- a/src/subdomains/supporting/realunit/realunit.service.ts +++ b/src/subdomains/supporting/realunit/realunit.service.ts @@ -42,7 +42,9 @@ import { UserDataService } from 'src/subdomains/generic/user/models/user-data/us import { User } from 'src/subdomains/generic/user/models/user/user.entity'; import { UserService } from 'src/subdomains/generic/user/models/user/user.service'; import { FiatPaymentMethod } from 'src/subdomains/supporting/payment/dto/payment-method.enum'; +import { TransactionRequestStatus } from 'src/subdomains/supporting/payment/entities/transaction-request.entity'; import { TransactionRequestService } from 'src/subdomains/supporting/payment/services/transaction-request.service'; +import { TransactionService } from 'src/subdomains/supporting/payment/services/transaction.service'; import { transliterate } from 'transliteration'; import { AssetPricesService } from '../pricing/services/asset-prices.service'; import { PriceCurrency, PriceValidity, PricingService } from '../pricing/services/pricing.service'; @@ -52,6 +54,7 @@ import { HoldersClientResponse, TokenInfoClientResponse, } from './dto/client.dto'; +import { RealUnitQuoteDto, RealUnitTransactionDto } from './dto/realunit-admin.dto'; import { RealUnitDtoMapper } from './dto/realunit-dto.mapper'; import { AktionariatRegistrationDto, @@ -106,6 +109,7 @@ export class RealUnitService { private readonly sellService: SellService, private readonly eip7702DelegationService: Eip7702DelegationService, private readonly transactionRequestService: TransactionRequestService, + private readonly transactionService: TransactionService, private readonly accountMergeService: AccountMergeService, ) { this.ponderUrl = GetConfig().blockchain.realunit.graphUrl; @@ -288,6 +292,27 @@ export class RealUnitService { return response; } + async confirmBuy(userId: number, requestId: number): Promise { + const request = await this.transactionRequestService.getOrThrow(requestId, userId); + if (!request.isValid) throw new BadRequestException('Transaction request is not valid'); + if ([TransactionRequestStatus.COMPLETED, TransactionRequestStatus.WAITING_FOR_PAYMENT].includes(request.status)) + throw new ConflictException('Transaction request is already confirmed'); + if (Util.daysDiff(request.created) >= Config.txRequestWaitingExpiryDays) + throw new BadRequestException('Transaction request is expired'); + + // Aktionariat API aufrufen + const fiat = await this.fiatService.getFiat(request.sourceId); + const aktionariatResponse = await this.blockchainService.requestPaymentInstructions({ + currency: fiat.name, + address: request.user.address, + shares: Math.floor(request.estimatedAmount), + price: Math.round(request.exchangeRate * 100), + }); + + // Status + Response speichern + await this.transactionRequestService.confirmTransactionRequest(request, JSON.stringify(aktionariatResponse)); + } + // --- Registration Methods --- // returns true if registration needs manual review, false if completed @@ -769,6 +794,40 @@ export class RealUnitService { return response; } + // --- Admin Methods --- + + async getAdminQuotes(limit = 50, offset = 0): Promise { + const realuAsset = await this.getRealuAsset(); + const requests = await this.transactionRequestService.getByAssetId(realuAsset.id, limit, offset); + + return requests.map((r) => ({ + id: r.id, + uid: r.uid, + type: r.type, + status: r.status, + amount: r.amount, + estimatedAmount: r.estimatedAmount, + created: r.created, + userAddress: r.user?.address, + })); + } + + async getAdminTransactions(limit = 50, offset = 0): Promise { + const realuAsset = await this.getRealuAsset(); + const transactions = await this.transactionService.getByAssetId(realuAsset.id, limit, offset); + + return transactions.map((t) => ({ + id: t.id, + uid: t.uid, + type: t.type, + amountInChf: t.amountInChf, + assets: t.assets, + created: t.created, + outputDate: t.outputDate, + userAddress: t.user?.address, + })); + } + async confirmSell(userId: number, requestId: number, dto: RealUnitSellConfirmDto): Promise<{ txHash: string }> { // 1. Get and validate TransactionRequest (getOrThrow validates ownership and existence) const request = await this.transactionRequestService.getOrThrow(requestId, userId);