From d812b1cc5f6bf2992c09abd564dea3c3e3368a5d Mon Sep 17 00:00:00 2001 From: fatemeh imanipour Date: Sat, 9 May 2026 16:17:00 +0330 Subject: [PATCH 1/3] Add interceptors dependency to all related modules --- accountant/accountant-app/pom.xml | 4 ++++ accountant/pom.xml | 5 +++++ auth-gateway/auth-gateway-app/pom.xml | 4 ++++ bc-gateway/bc-gateway-app/pom.xml | 8 ++++++++ device-management/device-management-app/pom.xml | 4 ++++ device-management/pom.xml | 5 +++++ docker-compose.yml | 4 ++-- matching-gateway/matching-gateway-app/pom.xml | 4 ++++ otp/otp-app/pom.xml | 4 ++++ profile/pom.xml | 5 +++++ profile/profile-app/pom.xml | 4 ++++ user-management/pom.xml | 5 +++++ 12 files changed, 54 insertions(+), 2 deletions(-) diff --git a/accountant/accountant-app/pom.xml b/accountant/accountant-app/pom.xml index 67649ff75..f56aee06a 100644 --- a/accountant/accountant-app/pom.xml +++ b/accountant/accountant-app/pom.xml @@ -51,6 +51,10 @@ co.nilin.opex.utility error-handler + + co.nilin.opex.utility + interceptors + org.springframework.cloud spring-cloud-starter-vault-config diff --git a/accountant/pom.xml b/accountant/pom.xml index 6eded39f6..edce57a88 100644 --- a/accountant/pom.xml +++ b/accountant/pom.xml @@ -82,6 +82,11 @@ error-handler ${error-hanlder.version} + + co.nilin.opex.utility + interceptors + ${interceptor.version} + org.springframework.cloud spring-cloud-dependencies diff --git a/auth-gateway/auth-gateway-app/pom.xml b/auth-gateway/auth-gateway-app/pom.xml index 0d31ffddd..0ff3a5ade 100644 --- a/auth-gateway/auth-gateway-app/pom.xml +++ b/auth-gateway/auth-gateway-app/pom.xml @@ -80,6 +80,10 @@ co.nilin.opex.utility error-handler + + co.nilin.opex.utility + interceptors + com.auth0 java-jwt diff --git a/bc-gateway/bc-gateway-app/pom.xml b/bc-gateway/bc-gateway-app/pom.xml index 2bdccb667..0046a178b 100644 --- a/bc-gateway/bc-gateway-app/pom.xml +++ b/bc-gateway/bc-gateway-app/pom.xml @@ -138,6 +138,14 @@ micrometer-registry-prometheus runtime + + co.nilin.opex.utility + interceptors + + + co.nilin.opex.utility + error-handler + diff --git a/device-management/device-management-app/pom.xml b/device-management/device-management-app/pom.xml index 82e41ff4e..ff9b70048 100644 --- a/device-management/device-management-app/pom.xml +++ b/device-management/device-management-app/pom.xml @@ -78,6 +78,10 @@ co.nilin.opex.utility error-handler + + co.nilin.opex.utility + interceptors + com.zaxxer HikariCP diff --git a/device-management/pom.xml b/device-management/pom.xml index d24596637..4800bfa7c 100644 --- a/device-management/pom.xml +++ b/device-management/pom.xml @@ -56,6 +56,11 @@ error-handler ${error-hanlder.version} + + co.nilin.opex.utility + interceptors + ${interceptor.version} + co.nilin.opex.device-management device-management-core diff --git a/docker-compose.yml b/docker-compose.yml index 63535e6e2..b9078536e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -546,8 +546,8 @@ services: - SMTP_USER=${SMTP_USER} - SMTP_PASS=${SMTP_PASS} - SMTP_FROM=${SMTP_FROM} - - SMTP_SOCKS_HOST=${SMTP_SOCKS_HOST} - - SMTP_SOCKS_PORT=${SMTP_SOCKS_PORT} + - SMTP_PROXY_HOST=${SMTP_PROXY_HOST} + - SMTP_PROXY_PORT=${SMTP_PROXY_PORT} - SMTP_PROXY_ENABLED=${SMTP_PROXY_ENABLED} - TOKEN_ISSUER_URL=${KC_ISSUER_URL} - OTP_CODE_RESPONSE_ENABLED=${OTP_CODE_RESPONSE_ENABLED} diff --git a/matching-gateway/matching-gateway-app/pom.xml b/matching-gateway/matching-gateway-app/pom.xml index 24e94ab58..86d029320 100644 --- a/matching-gateway/matching-gateway-app/pom.xml +++ b/matching-gateway/matching-gateway-app/pom.xml @@ -77,6 +77,10 @@ co.nilin.opex.utility error-handler + + co.nilin.opex.utility + interceptors + io.mockk mockk diff --git a/otp/otp-app/pom.xml b/otp/otp-app/pom.xml index 1d3d72124..49cb4f0c0 100644 --- a/otp/otp-app/pom.xml +++ b/otp/otp-app/pom.xml @@ -87,6 +87,10 @@ co.nilin.opex.utility error-handler + + co.nilin.opex.utility + interceptors + io.mockk mockk diff --git a/profile/pom.xml b/profile/pom.xml index 7ac615097..ffd3ab564 100644 --- a/profile/pom.xml +++ b/profile/pom.xml @@ -66,6 +66,11 @@ logging-handler ${logging-handler.version} + + co.nilin.opex.utility + error-handler + ${error-hanlder.version} + co.nilin.opex.utility interceptors diff --git a/profile/profile-app/pom.xml b/profile/profile-app/pom.xml index 685459a22..2b2412044 100644 --- a/profile/profile-app/pom.xml +++ b/profile/profile-app/pom.xml @@ -98,6 +98,10 @@ org.springframework.cloud spring-cloud-starter-vault-config + + co.nilin.opex.utility + error-handler + co.nilin.opex.utility interceptors diff --git a/user-management/pom.xml b/user-management/pom.xml index 0c0b0c5a4..fbb1146c0 100644 --- a/user-management/pom.xml +++ b/user-management/pom.xml @@ -49,6 +49,11 @@ error-handler ${error-hanlder.version} + + co.nilin.opex.utility + interceptors + ${interceptor.version} + co.nilin.opex.auth user-management-core From 31e78123f6e3779e276041d185ff05ae2688a038 Mon Sep 17 00:00:00 2001 From: Amir Rajabi Date: Sat, 25 Jul 2026 16:54:51 +0330 Subject: [PATCH 2/3] Add admin impersonated swap operations Implement admin endpoints for reserving and finalizing swaps on behalf of users. Admins can now specify custom rates or destination amounts when initiating transfers. Includes new AdminTransferReserveRequest DTO, AdminSwapController with reserve/finalize endpoints, and refactored TransferService logic with separate admin and user paths. Adds IMPERSONATED_PURCHASE_FINALIZED and IMPERSONATED_SWAP transaction categories to track admin-initiated transfers. --- .../core/inout/AdminTransferReserveRequest.kt | 12 ++ .../co/nilin/opex/api/core/spi/WalletProxy.kt | 8 + .../opex/controller/AdminSwapController.kt | 111 ++++++++++++ .../api/ports/proxy/impl/WalletProxyImpl.kt | 36 ++++ .../AdvancedTransferAdminController.kt | 42 ++++- .../app/dto/AdminTransferReserveRequest.kt | 12 ++ .../wallet/app/service/TransferService.kt | 167 +++++++++++++++--- .../wallet/core/model/TransferCategory.kt | 1 + .../core/model/UserTransactionCategory.kt | 1 + .../core/service/TransferManagerImpl.kt | 22 +++ 10 files changed, 380 insertions(+), 32 deletions(-) create mode 100644 api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/AdminTransferReserveRequest.kt create mode 100644 api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/AdminSwapController.kt create mode 100644 wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/dto/AdminTransferReserveRequest.kt diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/AdminTransferReserveRequest.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/AdminTransferReserveRequest.kt new file mode 100644 index 000000000..f4dd5b78f --- /dev/null +++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/AdminTransferReserveRequest.kt @@ -0,0 +1,12 @@ +package co.nilin.opex.api.core.inout + +import java.math.BigDecimal + +data class AdminTransferReserveRequest( + val sourceSymbol: String, + val sourceAmount: BigDecimal, + val destSymbol: String, + val destAmount: BigDecimal? = null, + val rate: BigDecimal? = null, + val receiverUuid: String, +) \ No newline at end of file diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/WalletProxy.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/WalletProxy.kt index d2c8e8956..8b0bd2e7c 100644 --- a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/WalletProxy.kt +++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/WalletProxy.kt @@ -259,4 +259,12 @@ interface WalletProxy { suspend fun deleteGateway(token: String, gatewayUuid: String, currencySymbol: String) suspend fun submitDepositWebhook(request: DepositWebhookRequest, signature: String): DepositWebhookResponse + + suspend fun reserveSwapByAdmin(token: String, request: AdminTransferReserveRequest): ReservedTransferResponse + suspend fun finalizeSwapByAdmin( + token: String, + reserveUuid: String, + description: String?, + transferRef: String? + ): TransferResult } \ No newline at end of file diff --git a/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/AdminSwapController.kt b/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/AdminSwapController.kt new file mode 100644 index 000000000..5fefbdba2 --- /dev/null +++ b/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/AdminSwapController.kt @@ -0,0 +1,111 @@ +package co.nilin.opex.api.ports.opex.controller + +import co.nilin.opex.api.core.inout.AdminTransferReserveRequest +import co.nilin.opex.api.core.inout.ReservedTransferResponse +import co.nilin.opex.api.core.inout.TransferResult +import co.nilin.opex.api.core.spi.WalletProxy +import co.nilin.opex.api.ports.opex.util.jwtAuthentication +import co.nilin.opex.api.ports.opex.util.tokenValue +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.Parameter +import io.swagger.v3.oas.annotations.media.Content +import io.swagger.v3.oas.annotations.media.Schema +import io.swagger.v3.oas.annotations.responses.ApiResponse +import io.swagger.v3.oas.annotations.security.SecurityRequirement +import io.swagger.v3.oas.annotations.tags.Tag +import org.springframework.security.core.annotation.CurrentSecurityContext +import org.springframework.security.core.context.SecurityContext +import org.springframework.web.bind.annotation.* + +@RestController +@RequestMapping("/opex/v1/admin/swap") +@Tag(name = "Admin Swap", description = "Admin impersonated swap reserve and finalize operations.") +class AdminSwapController( + val walletProxy: WalletProxy +) { + @PostMapping("/reserve") + @Operation( + summary = "Admin Reserve Swap", + description = """POST /opex/v1/admin/swap/reserve. +Security: Bearer admin-token required. Requires authenticated admin JWT. +Behavior: Admin can reserve swap on behalf of a user by specifying custom rate or amounts.""", + security = [SecurityRequirement(name = "bearerAuth")], + responses = [ + ApiResponse( + responseCode = "200", + description = "Successful response.", + content = [Content( + mediaType = "application/json", + schema = Schema(implementation = ReservedTransferResponse::class) + )] + ), + ApiResponse( + responseCode = "401", + description = "Unauthorized. Bearer token is missing, invalid, or expired.", + content = [Content()] + ), + ApiResponse( + responseCode = "403", + description = "Forbidden. User does not have admin permissions.", + content = [Content()] + ) + ] + ) + suspend fun reserve( + @RequestBody request: AdminTransferReserveRequest, + @Parameter(hidden = true) + @CurrentSecurityContext securityContext: SecurityContext + ): ReservedTransferResponse { + return walletProxy.reserveSwapByAdmin(securityContext.jwtAuthentication().tokenValue(), request) + } + + @PostMapping("/finalize/{reserveUuid}") + @Operation( + summary = "Admin Finalize Transfer", + description = """POST /opex/v1/admin/swap/finalize/{reserveUuid}. +Security: Bearer admin-token required. Requires authenticated admin JWT. +Behavior: Finalizes a reserved swap on behalf of the user.""", + security = [SecurityRequirement(name = "bearerAuth")], + responses = [ + ApiResponse( + responseCode = "200", + description = "Successful response.", + content = [Content( + mediaType = "application/json", + schema = Schema(implementation = TransferResult::class) + )] + ), + ApiResponse( + responseCode = "401", + description = "Unauthorized. Bearer token is missing, invalid, or expired.", + content = [Content()] + ), + ApiResponse( + responseCode = "403", + description = "Forbidden. User does not have admin permissions.", + content = [Content()] + ) + ] + ) + suspend fun finalizeTransfer( + @Parameter( + name = "reserveUuid", + description = "Swap reserve UUID returned by reserve endpoint.", + required = true + ) + @PathVariable reserveUuid: String, + @Parameter(name = "description", description = "Optional transfer description.", required = false) + @RequestParam description: String?, + @Parameter(name = "transferRef", description = "Optional external transfer reference.", required = false) + @RequestParam transferRef: String?, + @Parameter(hidden = true) + @CurrentSecurityContext securityContext: SecurityContext + ): TransferResult { + return walletProxy.finalizeSwapByAdmin( + securityContext.jwtAuthentication().tokenValue(), + reserveUuid, + description, + transferRef + ) + } +} \ No newline at end of file diff --git a/api/api-ports/api-proxy-rest/src/main/kotlin/co/nilin/opex/api/ports/proxy/impl/WalletProxyImpl.kt b/api/api-ports/api-proxy-rest/src/main/kotlin/co/nilin/opex/api/ports/proxy/impl/WalletProxyImpl.kt index a5b9aae5e..7e87050fb 100644 --- a/api/api-ports/api-proxy-rest/src/main/kotlin/co/nilin/opex/api/ports/proxy/impl/WalletProxyImpl.kt +++ b/api/api-ports/api-proxy-rest/src/main/kotlin/co/nilin/opex/api/ports/proxy/impl/WalletProxyImpl.kt @@ -1089,6 +1089,7 @@ class WalletProxyImpl(@Qualifier("generalWebClient") private val webClient: WebC } .awaitBodilessEntity() } + override suspend fun submitDepositWebhook( request: DepositWebhookRequest, signature: String @@ -1107,5 +1108,40 @@ class WalletProxyImpl(@Qualifier("generalWebClient") private val webClient: WebC .awaitSingle() } } + + override suspend fun reserveSwapByAdmin( + token: String, + request: AdminTransferReserveRequest + ): ReservedTransferResponse { + return webClient.post() + .uri("$baseUrl/admin/v1/transfer/reserve") + .accept(MediaType.APPLICATION_JSON) + .header(HttpHeaders.AUTHORIZATION, "Bearer $token") + .body(Mono.just(request)) + .retrieve() + .onStatus({ t -> t.isError }, { it.createException() }) + .bodyToMono() + .awaitFirstOrElse { throw OpexError.BadRequest.exception() } + } + + override suspend fun finalizeSwapByAdmin( + token: String, + reserveUuid: String, + description: String?, + transferRef: String? + ): TransferResult { + return webClient.post() + .uri("$baseUrl/admin/v1/transfer/${reserveUuid}") { + it.queryParam("description", description) + it.queryParam("transferRef", transferRef) + it.build() + } + .accept(MediaType.APPLICATION_JSON) + .header(HttpHeaders.AUTHORIZATION, "Bearer $token") + .retrieve() + .onStatus({ t -> t.isError }, { it.createException() }) + .bodyToMono() + .awaitFirstOrElse { throw OpexError.BadRequest.exception() } + } } diff --git a/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/controller/AdvancedTransferAdminController.kt b/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/controller/AdvancedTransferAdminController.kt index 703419083..50376d4db 100644 --- a/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/controller/AdvancedTransferAdminController.kt +++ b/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/controller/AdvancedTransferAdminController.kt @@ -1,18 +1,18 @@ package co.nilin.opex.wallet.app.controller +import co.nilin.opex.wallet.app.dto.AdminTransferReserveRequest +import co.nilin.opex.wallet.app.dto.ReservedTransferResponse import co.nilin.opex.wallet.app.dto.UserSwapTransactionRequest -import co.nilin.opex.wallet.app.dto.UserTransactionRequest +import co.nilin.opex.wallet.app.service.TransferService import co.nilin.opex.wallet.core.inout.AdminSwapResponse +import co.nilin.opex.wallet.core.inout.TransferResult +import co.nilin.opex.wallet.core.model.WalletType import co.nilin.opex.wallet.core.spi.ReservedTransferManager import io.swagger.annotations.ApiResponse import io.swagger.annotations.Example import io.swagger.annotations.ExampleProperty import org.springframework.beans.factory.annotation.Autowired -import org.springframework.security.core.annotation.CurrentSecurityContext -import org.springframework.security.core.context.SecurityContext -import org.springframework.web.bind.annotation.PostMapping -import org.springframework.web.bind.annotation.RequestBody -import org.springframework.web.bind.annotation.RestController +import org.springframework.web.bind.annotation.* import java.time.Instant import java.time.LocalDateTime import java.time.ZoneId @@ -23,6 +23,8 @@ class AdvancedTransferAdminController { @Autowired lateinit var reservedTransferManager: ReservedTransferManager + @Autowired + lateinit var transferService: TransferService @PostMapping("/admin/v1/swap/history") @ApiResponse( @@ -63,4 +65,32 @@ class AdvancedTransferAdminController { ) } } + + @PostMapping("/admin/v1/transfer/reserve") + suspend fun reserve(@RequestBody request: AdminTransferReserveRequest): ReservedTransferResponse { + return transferService.reserveTransferByAdmin( + request.sourceSymbol, + request.destSymbol, + request.receiverUuid, + WalletType.MAIN, + request.receiverUuid, + WalletType.MAIN, + request.sourceAmount, + request.destAmount, + request.rate + ) + } + + @PostMapping("/admin/v1/transfer/{reserveUuid}") + suspend fun finalizeTransfer( + @PathVariable reserveUuid: String, + @RequestParam description: String?, + @RequestParam transferRef: String?, + ): TransferResult { + return transferService.advanceTransferByAdmin( + reserveUuid, + description, + transferRef + ).transferResult + } } diff --git a/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/dto/AdminTransferReserveRequest.kt b/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/dto/AdminTransferReserveRequest.kt new file mode 100644 index 000000000..84f623215 --- /dev/null +++ b/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/dto/AdminTransferReserveRequest.kt @@ -0,0 +1,12 @@ +package co.nilin.opex.wallet.app.dto + +import java.math.BigDecimal + +data class AdminTransferReserveRequest( + val sourceSymbol: String, + val sourceAmount: BigDecimal, + val destSymbol: String, + val destAmount: BigDecimal? = null, + val rate: BigDecimal? = null, + val receiverUuid: String, +) \ No newline at end of file diff --git a/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/service/TransferService.kt b/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/service/TransferService.kt index 3749bbb7b..49087cf13 100644 --- a/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/service/TransferService.kt +++ b/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/service/TransferService.kt @@ -135,49 +135,161 @@ class TransferService( } } + suspend fun reserveTransferByAdmin( + sourceSymbol: String, + destSymbol: String, + senderUuid: String, + senderWalletType: WalletType, + receiverUuid: String, + receiverWalletType: WalletType, + sourceAmount: BigDecimal, + destAmount: BigDecimal? = null, + rate: BigDecimal? = null, + ): ReservedTransferResponse { + + if ((rate != null && destAmount != null) || (rate == null && destAmount == null)) { + throw OpexError.BadRequest.exception("Either 'rate' or 'destAmount' must be provided, but not both") + } + + if (rate != null && rate <= BigDecimal.ZERO) { + throw OpexError.BadRequest.exception("rate must be greater than zero") + } + + if (destAmount != null && destAmount <= BigDecimal.ZERO) { + throw OpexError.BadRequest.exception("destAmount must be greater than zero") + } + + val (finalRate, rawDestAmount) = if (rate != null) { + val calculatedDest = calculateDestAmount(sourceAmount, Rate(sourceSymbol, destSymbol, rate)) + Pair(rate, calculatedDest) + } else { + val calculatedRate = destAmount!!.divide(sourceAmount, 28, RoundingMode.HALF_UP) + Pair(calculatedRate, destAmount) + } + + validateInitialAmountAndPrecision(sourceAmount, sourceSymbol) + val scaledDestAmount = precisionService.calculatePrecision(rawDestAmount, destSymbol) + + validateMinimumAmount(sourceSymbol, sourceAmount, scaledDestAmount, destSymbol, finalRate) + validateMaximumAmount(sourceSymbol, sourceAmount, destSymbol, scaledDestAmount) + precisionService.validatePrecision(scaledDestAmount, destSymbol) + checkIfSystemHasEnoughBalance(destSymbol, receiverWalletType, scaledDestAmount) + val reserveNumber = UUID.randomUUID().toString() + val resp = reservedTransferManager.reserve( + ReservedTransfer( + reserveNumber = reserveNumber, + destSymbol = destSymbol, + sourceSymbol = sourceSymbol, + sourceAmount = sourceAmount, + senderUuid = senderUuid, + receiverUuid = receiverUuid, + senderWalletType = senderWalletType, + receiverWalletType = receiverWalletType, + reservedDestAmount = scaledDestAmount, + rate = finalRate + ) + ) + + return with(resp) { + ReservedTransferResponse( + reserveNumber, + sourceSymbol, + destSymbol, + receiverUuid, + sourceAmount, + reservedDestAmount, + reserveDate, + expDate, + status + ) + } + } + @Transactional suspend fun advanceTransfer( reserveNumber: String, description: String?, transferRef: String?, issuer: String? = null, - //todo need to review transferCategory: TransferCategory = TransferCategory.PURCHASE_FINALIZED, ): TransferResultDetailed { + return executeAdvanceTransfer( + reserveNumber = reserveNumber, + description = description, + transferRef = transferRef, + transferCategory = transferCategory, + issuer = issuer, + validateIssuer = true + ) + } + + @Transactional + suspend fun advanceTransferByAdmin( + reserveNumber: String, + description: String?, + transferRef: String?, + transferCategory: TransferCategory = TransferCategory.IMPERSONATED_PURCHASE_FINALIZED, + ): TransferResultDetailed { + return executeAdvanceTransfer( + reserveNumber = reserveNumber, + description = description, + transferRef = transferRef, + transferCategory = transferCategory, + issuer = null, + validateIssuer = false + ) + } + + private suspend fun executeAdvanceTransfer( + reserveNumber: String, + description: String?, + transferRef: String?, + transferCategory: TransferCategory, + issuer: String?, + validateIssuer: Boolean + ): TransferResultDetailed { + val reservations = reservedTransferManager.fetchValidReserve(reserveNumber) ?: throw OpexError.InvalidReserveNumber.exception() - if (!(issuer == null || reservations.senderUuid == issuer)) + + if (validateIssuer && !(issuer == null || reservations.senderUuid == issuer)) { throw OpexError.Forbidden.exception() + } + + val refPrefix = transferRef?.let { "$it-" } ?: "" + val withdrawRef = "${refPrefix}${reserveNumber}-withdraw" + val depositRef = "${refPrefix}${reserveNumber}-deposit" val senderTransfer = _transfer( - reservations.sourceSymbol, - reservations.senderWalletType, - reservations.senderUuid, - WalletType.MAIN, - walletOwnerManager.systemUuid, - reservations.sourceAmount, - description, - "$transferRef-$reserveNumber-withdraw", - transferCategory, - reservations.sourceSymbol, - reservations.sourceAmount + symbol = reservations.sourceSymbol, + senderWalletType = reservations.senderWalletType, + senderUuid = reservations.senderUuid, + receiverWalletType = WalletType.MAIN, + receiverUuid = walletOwnerManager.systemUuid, + amount = reservations.sourceAmount, + description = description, + transferRef = withdrawRef, + transferCategory = transferCategory, + destSymbol = reservations.destSymbol, + destAmount = reservations.reservedDestAmount ).transferResult val receiverTransfer = _transfer( - reservations.destSymbol, - WalletType.MAIN, - walletOwnerManager.systemUuid, - reservations.receiverWalletType, - reservations.receiverUuid, - reservations.reservedDestAmount, - description, - "$transferRef-$reserveNumber-deposit", - transferCategory, - reservations.destSymbol, - reservations.reservedDestAmount + symbol = reservations.destSymbol, + senderWalletType = WalletType.MAIN, + senderUuid = walletOwnerManager.systemUuid, + receiverWalletType = reservations.receiverWalletType, + receiverUuid = reservations.receiverUuid, + amount = reservations.reservedDestAmount, + description = description, + transferRef = depositRef, + transferCategory = transferCategory, + destSymbol = reservations.destSymbol, + destAmount = reservations.reservedDestAmount ).transferResult reservedTransferManager.commitReserve(reserveNumber) + return TransferResultDetailed( transferResult = TransferResult( senderTransfer.date, @@ -316,7 +428,10 @@ class TransferService( val minDestAmount = minPrecisionAmount(destPrecision) val minimumSource = - maxOf(minSourceAmount, minDestAmount.divide(rate, 10, RoundingMode.DOWN)).setScale(sourcePrecision, RoundingMode.DOWN) + maxOf(minSourceAmount, minDestAmount.divide(rate, 10, RoundingMode.DOWN)).setScale( + sourcePrecision, + RoundingMode.DOWN + ) val minimumDest = maxOf(minDestAmount, minSourceAmount.multiply(rate)).setScale(destPrecision, RoundingMode.DOWN) @@ -332,7 +447,7 @@ class TransferService( destAmount: BigDecimal, ) { suspend fun getMaxOrder(symbol: String): BigDecimal { - return currencyManager.fetchCurrencyMaxOrder(symbol)?: BigDecimal.ZERO + return currencyManager.fetchCurrencyMaxOrder(symbol) ?: BigDecimal.ZERO } if (sourceAmount > getMaxOrder(sourceSymbol) || destAmount > getMaxOrder(destSymbol)) { diff --git a/wallet/wallet-core/src/main/kotlin/co/nilin/opex/wallet/core/model/TransferCategory.kt b/wallet/wallet-core/src/main/kotlin/co/nilin/opex/wallet/core/model/TransferCategory.kt index 9cf82d1d6..c29c2fc16 100644 --- a/wallet/wallet-core/src/main/kotlin/co/nilin/opex/wallet/core/model/TransferCategory.kt +++ b/wallet/wallet-core/src/main/kotlin/co/nilin/opex/wallet/core/model/TransferCategory.kt @@ -10,6 +10,7 @@ enum class TransferCategory { WITHDRAW_REJECT, WITHDRAW_CANCEL, PURCHASE_FINALIZED, + IMPERSONATED_PURCHASE_FINALIZED, WITHDRAW_MANUALLY, ORDER_CREATE, ORDER_CANCEL, diff --git a/wallet/wallet-core/src/main/kotlin/co/nilin/opex/wallet/core/model/UserTransactionCategory.kt b/wallet/wallet-core/src/main/kotlin/co/nilin/opex/wallet/core/model/UserTransactionCategory.kt index 734c1b173..39b35b617 100644 --- a/wallet/wallet-core/src/main/kotlin/co/nilin/opex/wallet/core/model/UserTransactionCategory.kt +++ b/wallet/wallet-core/src/main/kotlin/co/nilin/opex/wallet/core/model/UserTransactionCategory.kt @@ -9,6 +9,7 @@ enum class UserTransactionCategory { WITHDRAW, FEE, SWAP, + IMPERSONATED_SWAP, REFERRAL_COMMISSION, REFERRAL_KYC_REWARD, REFERENT_COMMISSION, diff --git a/wallet/wallet-core/src/main/kotlin/co/nilin/opex/wallet/core/service/TransferManagerImpl.kt b/wallet/wallet-core/src/main/kotlin/co/nilin/opex/wallet/core/service/TransferManagerImpl.kt index 9a368972d..b03fb97ed 100644 --- a/wallet/wallet-core/src/main/kotlin/co/nilin/opex/wallet/core/service/TransferManagerImpl.kt +++ b/wallet/wallet-core/src/main/kotlin/co/nilin/opex/wallet/core/service/TransferManagerImpl.kt @@ -240,6 +240,28 @@ class TransferManagerImpl( ) userTransactionManager.save(dstTx) } + TransferCategory.IMPERSONATED_PURCHASE_FINALIZED -> { + val srcTx = UserTransaction( + command.sourceWallet.owner.id!!, + txId, + currency, + command.sourceWallet.balance.amount - amount, + -amount, + UserTransactionCategory.IMPERSONATED_SWAP, + command.description + ) + userTransactionManager.save(srcTx) + + val dstTx = UserTransaction( + command.destWallet.owner.id!!, + txId, + currency, + command.destWallet.balance.amount + amount, + amount, + UserTransactionCategory.IMPERSONATED_SWAP + ) + userTransactionManager.save(dstTx) + } TransferCategory.KYC_ACCEPTED_REWARD -> { val loserOwner = command.sourceWallet.owner.id!! From 19e8f2ab3e7a8d70a72fb0025d4229a98c603581 Mon Sep 17 00:00:00 2001 From: Amir Rajabi Date: Sat, 25 Jul 2026 17:16:16 +0330 Subject: [PATCH 3/3] Add impersonated transaction categories Add IMPERSONATED_PURCHASE_FINALIZED to TransferCategory and IMPERSONATED_SWAP to UserTransactionCategory enum to support tracking impersonated user transactions. --- .../main/kotlin/co/nilin/opex/api/core/inout/TransferCategory.kt | 1 + .../co/nilin/opex/api/core/inout/UserTransactionCategory.kt | 1 + 2 files changed, 2 insertions(+) diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/TransferCategory.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/TransferCategory.kt index ab728f008..838b2ba40 100644 --- a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/TransferCategory.kt +++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/TransferCategory.kt @@ -10,6 +10,7 @@ enum class TransferCategory { WITHDRAW_REJECT, WITHDRAW_CANCEL, PURCHASE_FINALIZED, + IMPERSONATED_PURCHASE_FINALIZED, WITHDRAW_MANUALLY, ORDER_CREATE, ORDER_CANCEL, diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/UserTransactionCategory.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/UserTransactionCategory.kt index 690a0c411..1e12ff483 100644 --- a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/UserTransactionCategory.kt +++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/UserTransactionCategory.kt @@ -9,6 +9,7 @@ enum class UserTransactionCategory { WITHDRAW, FEE, SWAP, + IMPERSONATED_SWAP, REFERRAL_COMMISSION, REFERRAL_KYC_REWARD, REFERENT_COMMISSION,