diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/DepositWebhookHeaders.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/DepositWebhookHeaders.kt new file mode 100644 index 000000000..90d628b97 --- /dev/null +++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/DepositWebhookHeaders.kt @@ -0,0 +1,5 @@ +package co.nilin.opex.api.core.inout + +object DepositWebhookHeaders { + const val SIGNATURE = "X-Signature" +} \ No newline at end of file diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/DepositWebhookRequest.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/DepositWebhookRequest.kt new file mode 100644 index 000000000..86d5b07a9 --- /dev/null +++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/DepositWebhookRequest.kt @@ -0,0 +1,12 @@ +package co.nilin.opex.api.core.inout + +import java.math.BigDecimal + +data class DepositWebhookRequest( + val referenceNumber: String, + val depositNumber: String, + val symbol: String, + val amount: BigDecimal, + val externalIdentifier: String, + val date: Long +) diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/DepositWebhookResponse.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/DepositWebhookResponse.kt new file mode 100644 index 000000000..659cc3d3f --- /dev/null +++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/DepositWebhookResponse.kt @@ -0,0 +1,9 @@ +package co.nilin.opex.api.core.inout + +data class DepositWebhookResponse( + val referenceNumber: String? = null, + val status: String, + val duplicate: Boolean = false, + val message: String? = null +) + 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 402f8bd21..f90018a32 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 @@ -261,5 +261,5 @@ interface WalletProxy { suspend fun deleteGateway(token: String, gatewayUuid: String, currencySymbol: String) - + suspend fun submitDepositWebhook(request: DepositWebhookRequest, signature: String): DepositWebhookResponse } \ No newline at end of file diff --git a/api/api-ports/api-binance-rest/src/main/kotlin/co/nilin/opex/api/ports/binance/config/SecurityConfig.kt b/api/api-ports/api-binance-rest/src/main/kotlin/co/nilin/opex/api/ports/binance/config/SecurityConfig.kt index f9bd55dc6..9769b02f0 100644 --- a/api/api-ports/api-binance-rest/src/main/kotlin/co/nilin/opex/api/ports/binance/config/SecurityConfig.kt +++ b/api/api-ports/api-binance-rest/src/main/kotlin/co/nilin/opex/api/ports/binance/config/SecurityConfig.kt @@ -49,6 +49,7 @@ class SecurityConfig( .pathMatchers(HttpMethod.DELETE, "/v3/order").hasAuthority("PERM_order:write") // Opex endpoints + .pathMatchers("/v1/deposit/webhook").permitAll() .pathMatchers("/opex/v1/admin/transactions/**").hasAnyAuthority("ROLE_monitoring", "ROLE_admin") .pathMatchers("/opex/v1/storage/**").permitAll() .pathMatchers("/opex/v1/web/config/**").permitAll() diff --git a/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/DepositWebhookController.kt b/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/DepositWebhookController.kt new file mode 100644 index 000000000..12180c572 --- /dev/null +++ b/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/DepositWebhookController.kt @@ -0,0 +1,27 @@ +package co.nilin.opex.api.ports.opex.controller + +import co.nilin.opex.api.core.inout.DepositWebhookRequest +import co.nilin.opex.api.core.inout.DepositWebhookResponse +import co.nilin.opex.api.core.inout.DepositWebhookHeaders +import co.nilin.opex.api.core.spi.WalletProxy +import org.springframework.web.bind.annotation.* + +@RestController +@RequestMapping("/v1/deposit") +class DepositWebhookController( + private val walletProxy: WalletProxy +) { + @PostMapping( + "/webhook" + ) + suspend fun submit( + @RequestHeader(DepositWebhookHeaders.SIGNATURE) sign: String, @RequestBody body: DepositWebhookRequest + ): DepositWebhookResponse { + return walletProxy.submitDepositWebhook( + body, + sign + ) + } +} + + 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 25d7b969f..775a54f12 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 @@ -1110,5 +1110,23 @@ class WalletProxyImpl(@Qualifier("generalWebClient") private val webClient: WebC } .awaitBodilessEntity() } + override suspend fun submitDepositWebhook( + request: DepositWebhookRequest, + signature: String + ): DepositWebhookResponse { + logger.info("proxying deposit webhook to wallet") + + return withContext(ProxyDispatchers.wallet) { + webClient.post() + .uri("$baseUrl/v1/deposit/webhook") + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON) + .header(DepositWebhookHeaders.SIGNATURE, signature) + .bodyValue(request) + .retrieve() + .bodyToMono() + .awaitSingle() + } + } } diff --git a/bc-gateway/bc-gateway-app/src/main/kotlin/co/nilin/opex/bcgateway/app/config/AppConfig.kt b/bc-gateway/bc-gateway-app/src/main/kotlin/co/nilin/opex/bcgateway/app/config/AppConfig.kt index 7c2b0e6fb..6c326f929 100644 --- a/bc-gateway/bc-gateway-app/src/main/kotlin/co/nilin/opex/bcgateway/app/config/AppConfig.kt +++ b/bc-gateway/bc-gateway-app/src/main/kotlin/co/nilin/opex/bcgateway/app/config/AppConfig.kt @@ -48,20 +48,5 @@ class AppConfig(private val resourceLoader: ResourceLoader) { adminKafkaEventListener.addEventListener(adminEventListener) } - @Bean("webhookPublicKey") - fun webhookPublicKey(): PublicKey { - val publicKeyString = resourceLoader.getResource("classpath:scanner-public.pem").inputStream - .readAllBytes() - .toString(Charsets.UTF_8) - .replace("-----BEGIN PUBLIC KEY-----", "") - .replace("-----END PUBLIC KEY-----", "") - .replace("\n", "") - .replace("\r", "") - - val keyBytes = Base64.getDecoder().decode(publicKeyString) - val keySpec = X509EncodedKeySpec(keyBytes) - val keyFactory = KeyFactory.getInstance("RSA") - return keyFactory.generatePublic(keySpec) - } } diff --git a/bc-gateway/bc-gateway-app/src/main/kotlin/co/nilin/opex/bcgateway/app/controller/ScannerController.kt b/bc-gateway/bc-gateway-app/src/main/kotlin/co/nilin/opex/bcgateway/app/controller/ScannerController.kt index fd89f0b83..7c79f00cf 100644 --- a/bc-gateway/bc-gateway-app/src/main/kotlin/co/nilin/opex/bcgateway/app/controller/ScannerController.kt +++ b/bc-gateway/bc-gateway-app/src/main/kotlin/co/nilin/opex/bcgateway/app/controller/ScannerController.kt @@ -3,20 +3,12 @@ package co.nilin.opex.bcgateway.app.controller import co.nilin.opex.bcgateway.core.api.WalletSyncService import co.nilin.opex.bcgateway.core.model.Transfer import co.nilin.opex.bcgateway.core.model.Wallet -import co.nilin.opex.common.OpexError -import co.nilin.opex.utility.error.data.OpexException +import co.nilin.opex.common.utils.SignVerifier import com.fasterxml.jackson.databind.ObjectMapper import org.slf4j.LoggerFactory import org.springframework.core.io.ResourceLoader -import org.springframework.web.bind.annotation.PostMapping -import org.springframework.web.bind.annotation.RequestBody -import org.springframework.web.bind.annotation.RequestHeader -import org.springframework.web.bind.annotation.RequestMapping -import org.springframework.web.bind.annotation.RestController +import org.springframework.web.bind.annotation.* import java.math.BigDecimal -import java.security.PublicKey -import java.security.Signature -import java.util.* data class WebhookBody( val txId: String, @@ -33,7 +25,7 @@ data class WebhookBody( @RestController @RequestMapping("/scanner") class ScannerController( - private val publicKey: PublicKey, + private val resourceLoader: ResourceLoader, private val mapper: ObjectMapper, private val service: WalletSyncService ) { @@ -42,30 +34,13 @@ class ScannerController( @PostMapping("/webhook") suspend fun webhook(@RequestHeader("X-Signature") sign: String, @RequestBody body: WebhookBody) { - verifySignature(sign, body) + val publicKeyRawStr = resourceLoader.getResource("classpath:scanner-public.pem").inputStream + .readAllBytes() + .toString(Charsets.UTF_8) + SignVerifier().verify("SHA256withRSA", publicKeyRawStr, mapper.writeValueAsString(body), sign) logger.info("Webhook received for address ${body.address}, amount ${body.amount}") service.sendTransfer(with(body) { Transfer(txId, Wallet(address, memo), isToken, amount, chain, tokenAddress) }) } - private fun verifySignature(sign: String, request: WebhookBody) { - try { - logger.info("Verifying signature for address ${request.address}") - val reqStr = mapper.writeValueAsString(request) - val decodedSign = Base64.getDecoder().decode(sign) - val verifier = Signature.getInstance("SHA256withRSA").apply { - initVerify(publicKey) - update(reqStr.toByteArray()) - } - if (!verifier.verify(decodedSign)) { - logger.warn("Signature is not valid!") - throw OpexError.Forbidden.exception() - } - } catch (e: OpexException) { - throw e - } catch (e: Exception) { - logger.error("Unable to verify signature", e) - throw OpexError.InternalServerError.exception() - } - } } \ No newline at end of file diff --git a/common/src/main/kotlin/co/nilin/opex/common/utils/SignVerifier.kt b/common/src/main/kotlin/co/nilin/opex/common/utils/SignVerifier.kt new file mode 100644 index 000000000..288514a47 --- /dev/null +++ b/common/src/main/kotlin/co/nilin/opex/common/utils/SignVerifier.kt @@ -0,0 +1,58 @@ +package co.nilin.opex.common.utils + + +import org.springframework.http.HttpStatus +import org.springframework.web.server.ResponseStatusException +import java.security.KeyFactory +import java.security.Signature +import java.security.spec.X509EncodedKeySpec +import java.util.Base64 + +class SignVerifier{ + + fun verify(signatureAlgorithm: String , publicKeyPem: String, payload: String, signatureBase64: String) { + val signatureBytes = try { + Base64.getDecoder().decode(signatureBase64) + } catch (ex: Exception) { + throw ResponseStatusException(HttpStatus.UNAUTHORIZED, "Invalid fiat scanner signature encoding") + } + + val publicKey = try { + val normalizedPem = publicKeyPem + .replace("-----BEGIN PUBLIC KEY-----", "") + .replace("-----END PUBLIC KEY-----", "") + .replace("\\s".toRegex(), "") + + val keyBytes = Base64.getDecoder().decode(normalizedPem) + val keySpec = X509EncodedKeySpec(keyBytes) + + KeyFactory + .getInstance(resolveKeyFactoryAlgorithm(signatureAlgorithm)) + .generatePublic(keySpec) + } catch (ex: Exception) { + throw ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Invalid fiat scanner public key") + } + + val verified = try { + Signature.getInstance(signatureAlgorithm).apply { + initVerify(publicKey) + update(payload.toByteArray(Charsets.UTF_8)) + }.verify(signatureBytes) + } catch (ex: Exception) { + throw ResponseStatusException(HttpStatus.UNAUTHORIZED, "Fiat scanner signature verification failed") + } + + if (!verified) { + throw ResponseStatusException(HttpStatus.UNAUTHORIZED, "Invalid fiat scanner signature") + } + } + + private fun resolveKeyFactoryAlgorithm(algorithm: String): String { + return when { + algorithm.equals("Ed25519", ignoreCase = true) -> "Ed25519" + algorithm.contains("RSA", ignoreCase = true) -> "RSA" + algorithm.contains("ECDSA", ignoreCase = true) -> "EC" + else -> "RSA" + } + } +} \ No newline at end of file diff --git a/device-management/pom.xml b/device-management/pom.xml index 4800bfa7c..fc80e43f7 100644 --- a/device-management/pom.xml +++ b/device-management/pom.xml @@ -22,7 +22,7 @@ 2.1.0 3.4.2 2024.0.0 - 1.2.19 + 1.2.25 diff --git a/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/service/ProfileManagement.kt b/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/service/ProfileManagement.kt index 74df80086..daaa1653c 100644 --- a/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/service/ProfileManagement.kt +++ b/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/service/ProfileManagement.kt @@ -161,7 +161,8 @@ class ProfileManagement( ProfileUpdatedEvent( userId = userId, firstName = request.firstName, - lastName = request.lastName + lastName = request.lastName, + identifier = request.identifier ) ) diff --git a/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/event/ProfileUpdatedEvent.kt b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/event/ProfileUpdatedEvent.kt index 93ad3220d..ad7e6394f 100644 --- a/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/event/ProfileUpdatedEvent.kt +++ b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/event/ProfileUpdatedEvent.kt @@ -5,5 +5,6 @@ data class ProfileUpdatedEvent( var firstName: String? = null, var lastName: String? = null, var email: String? = null, - var mobile: String? = null + var mobile: String? = null, + var identifier: String? = null ) diff --git a/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/config/SecurityConfig.kt b/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/config/SecurityConfig.kt index ad6105aea..e10790d8d 100644 --- a/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/config/SecurityConfig.kt +++ b/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/config/SecurityConfig.kt @@ -31,6 +31,7 @@ class SecurityConfig(private val webClient: WebClient) { fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain? { http.csrf().disable() .authorizeExchange() + .pathMatchers("/v1/deposit/webhook").permitAll() .pathMatchers("/balanceOf/**").authenticated() .pathMatchers("/owner/**").authenticated() .pathMatchers("/withdraw").authenticated() diff --git a/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/controller/DepositController.kt b/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/controller/DepositController.kt index 0536ac9b8..be0a67b19 100644 --- a/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/controller/DepositController.kt +++ b/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/controller/DepositController.kt @@ -1,17 +1,18 @@ package co.nilin.opex.wallet.app.controller +import co.nilin.opex.common.utils.SignVerifier import co.nilin.opex.wallet.app.dto.DepositHistoryRequest import co.nilin.opex.wallet.app.service.DepositService import co.nilin.opex.wallet.app.utils.asLocalDateTime -import co.nilin.opex.wallet.core.inout.DepositResponse -import co.nilin.opex.wallet.core.inout.TransactionSummary -import co.nilin.opex.wallet.core.inout.TransferResult +import co.nilin.opex.wallet.core.inout.* import co.nilin.opex.wallet.core.model.DepositType import co.nilin.opex.wallet.core.model.WalletType +import com.fasterxml.jackson.databind.ObjectMapper import io.swagger.annotations.ApiResponse import io.swagger.annotations.Example import io.swagger.annotations.ExampleProperty +import org.springframework.core.io.ResourceLoader import org.springframework.security.core.annotation.CurrentSecurityContext import org.springframework.security.core.context.SecurityContext import org.springframework.web.bind.annotation.* @@ -24,6 +25,8 @@ import java.time.ZoneId @RequestMapping class DepositController( private val depositService: DepositService, + private val resourceLoader: ResourceLoader, + private val mapper: ObjectMapper, ) { @PostMapping("/v1/deposit/history") @@ -114,6 +117,22 @@ class DepositController( limit, ) } + + @PostMapping("/v1/deposit/webhook") + suspend fun submit( + @RequestBody request: DepositWebhookRequest, + @RequestHeader(DepositWebhookHeaders.SIGNATURE) signature: String + ): DepositWebhookResponse { + val publicKeyRawStr = resourceLoader.getResource("classpath:scanner-public.pem").inputStream + .readAllBytes() + .toString(Charsets.UTF_8) + SignVerifier().verify("SHA512withRSA", publicKeyRawStr, mapper.writeValueAsString(request), signature) + return depositService.processExternalDeposit(request) + } + + object DepositWebhookHeaders { + const val SIGNATURE = "X-Signature" + } } diff --git a/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/controller/WalletOwnerController.kt b/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/controller/WalletOwnerController.kt index fc69f34ae..571db4d57 100644 --- a/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/controller/WalletOwnerController.kt +++ b/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/controller/WalletOwnerController.kt @@ -41,7 +41,7 @@ class WalletOwnerController( val owner = walletOwnerManager.findWalletOwner(uuid) ?: run { if (currentUserProvider.getCurrentUser()?.uuid.equals(uuid) && environment.activeProfiles.contains("otc")) walletOwnerManager.createWalletOwner( - uuid, currentUserProvider.getCurrentUser()?.mobile ?: "not set", "" + uuid, currentUserProvider.getCurrentUser()?.mobile ?: "not set", "", currentUserProvider.getCurrentUser()?.identityId ) throw OpexError.WalletOwnerNotFound.exception() } diff --git a/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/dto/CurrentUser.kt b/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/dto/CurrentUser.kt index b8fc9f4a1..772a10b3b 100644 --- a/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/dto/CurrentUser.kt +++ b/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/dto/CurrentUser.kt @@ -7,5 +7,6 @@ data class CurrentUser( val fullName: String?, val mobile: String?, val roles: List, - val level: String? + val level: String?, + val identityId: String? ) \ No newline at end of file diff --git a/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/listener/ProfileUpdatedEventListenerImpl.kt b/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/listener/ProfileUpdatedEventListenerImpl.kt index 767167bc5..f6de5d210 100644 --- a/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/listener/ProfileUpdatedEventListenerImpl.kt +++ b/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/listener/ProfileUpdatedEventListenerImpl.kt @@ -19,8 +19,7 @@ class ProfileUpdatedEventListenerImpl(private val walletOwnerManager: WalletOwne if (!event.firstName.isNullOrBlank() && !event.lastName.isNullOrBlank()) { runBlocking { logger.info("Incoming ProfileUpdated event $event") - walletOwnerManager.updateWalletOwnerName(event.userId, "${event.firstName} ${event.lastName}") - + walletOwnerManager.updateWalletOwnerName(event.userId, "${event.firstName} ${event.lastName}", event.identifier) } } else Unit } diff --git a/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/service/CurrentUser.kt b/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/service/CurrentUser.kt index 5b4f76b55..055b4f770 100644 --- a/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/service/CurrentUser.kt +++ b/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/service/CurrentUser.kt @@ -26,7 +26,8 @@ class CurrentUserProvider { ).joinToString(" "), mobile = jwt.getClaimAsString("mobile"), roles = jwt.getClaimAsStringList("roles") ?: emptyList(), - level = jwt.getClaimAsString("level") + level = jwt.getClaimAsString("level"), + identityId = jwt.getClaimAsString("identity_id") ) } } diff --git a/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/service/DepositService.kt b/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/service/DepositService.kt index 7d758e1f5..a07602d98 100644 --- a/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/service/DepositService.kt +++ b/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/service/DepositService.kt @@ -33,7 +33,7 @@ class DepositService( ) { private val logger = LoggerFactory.getLogger(DepositService::class.java) - + // ------------------------------------------------------------------------- // Helpers (NO LOGIC CHANGE) // ------------------------------------------------------------------------- @@ -107,6 +107,42 @@ class DepositService( transferMethod = TransferMethod.MANUALLY ) } + // ------------------------------------------------------------------------- + // Manual Deposit + // ------------------------------------------------------------------------- + + @Transactional + suspend fun processExternalDeposit( + request: DepositWebhookRequest + ): DepositWebhookResponse { + + logger.info( + "New incoming deposit request : $ to ${request.externalIdentifier} on ${request.symbol} at ${LocalDateTime.now()}" + ) + + val receiverUuid = walletOwnerManager.findWalletOwnerByExternalIdentifier(request.externalIdentifier)?.uuid + ?: throw OpexError.BadRequest.exception("Identifier ${request.externalIdentifier} not fount") + + with(request) { + deposit( + symbol = symbol, + receiverUuid = receiverUuid, + receiverWalletType = WalletType.MAIN, + senderUuid = walletOwnerManager.systemUuid, + amount = amount, + description = "Transfer to $depositNumber at $date, payId: $externalIdentifier", + transferRef = referenceNumber, + chain = null, + attachment = null, + depositType = DepositType.OFF_CHAIN, + gatewayUuid = null, + transferMethod = TransferMethod.SHEBA, + persistInvalidDeposit = false + ) + } + return DepositWebhookResponse(request.referenceNumber, DepositStatus.DONE.name, false) + } + // ------------------------------------------------------------------------- // Core Deposit @@ -126,6 +162,7 @@ class DepositService( depositType: DepositType, gatewayUuid: String?, transferMethod: TransferMethod?, + persistInvalidDeposit: Boolean = true, ): TransferResult? { logger.info( @@ -165,7 +202,10 @@ class DepositService( depositCommand.status = DepositStatus.INVALID } - traceDepositService.saveDepositInNewTransaction(depositCommand) + if (persistInvalidDeposit) + traceDepositService.saveDepositInNewTransaction(depositCommand) + else + depositPersister.persist(depositCommand) if (!isValid) { return null @@ -221,7 +261,7 @@ class DepositService( ): GatewayData { if (gatewayUuid == null) { - return GatewayData(true, BigDecimal.ZERO, BigDecimal.ZERO, null) + return GatewayData(true, BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.valueOf(Long.MAX_VALUE)) } val gateway = currencyServiceV2 diff --git a/wallet/wallet-app/src/main/resources/scanner-public.pem b/wallet/wallet-app/src/main/resources/scanner-public.pem new file mode 100644 index 000000000..ab3f1c0d5 --- /dev/null +++ b/wallet/wallet-app/src/main/resources/scanner-public.pem @@ -0,0 +1,9 @@ +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAufhMQdywCvA5wVOFosP5 +gkD6O1HjXq/dlcNpEDaReodG/+66hwLESXoWdVw1PnuZqam+DejUkBCxL7f3s0vM +X1gwDTmobMqFS1qdDTsGIglGUTd4o0Ln0x8dHa6NwpHigsruL9fymYLRmLuGPSIj +tip2h1wPZ0CWMzm+Knvb8yurVaXTXRzhHRGOcWR0wUGbluJ762R46DZQK38gsPfK +/slLo0GFcHyv7T+woIX7hOh1Gr3samAspt+jHUU52rXKKn9jfPanyqMt0T6mbDEk +MALiFMqGgkgCgxc5Ni5M8lgP6jjj4PwoN96K6rMF1enFosWOtrpg4XYP/aKuxwla +3wIDAQAB +-----END PUBLIC KEY----- diff --git a/wallet/wallet-core/src/main/kotlin/co/nilin/opex/wallet/core/inout/DepositWebhookRequest.kt b/wallet/wallet-core/src/main/kotlin/co/nilin/opex/wallet/core/inout/DepositWebhookRequest.kt new file mode 100644 index 000000000..c45023fa2 --- /dev/null +++ b/wallet/wallet-core/src/main/kotlin/co/nilin/opex/wallet/core/inout/DepositWebhookRequest.kt @@ -0,0 +1,13 @@ +package co.nilin.opex.wallet.core.inout + +import java.math.BigDecimal + +data class DepositWebhookRequest( + val referenceNumber: String, + val depositNumber: String, + val symbol: String, + val amount: BigDecimal, + val externalIdentifier: String, + val date: Long +) + diff --git a/wallet/wallet-core/src/main/kotlin/co/nilin/opex/wallet/core/inout/DepositWebhookResponse.kt b/wallet/wallet-core/src/main/kotlin/co/nilin/opex/wallet/core/inout/DepositWebhookResponse.kt new file mode 100644 index 000000000..c6c0b9d42 --- /dev/null +++ b/wallet/wallet-core/src/main/kotlin/co/nilin/opex/wallet/core/inout/DepositWebhookResponse.kt @@ -0,0 +1,9 @@ +package co.nilin.opex.wallet.core.inout + +data class DepositWebhookResponse( + val referenceNumber: String, + val status: String, + val duplicate: Boolean = false, + val message: String? = null +) + diff --git a/wallet/wallet-core/src/main/kotlin/co/nilin/opex/wallet/core/spi/WalletOwnerManager.kt b/wallet/wallet-core/src/main/kotlin/co/nilin/opex/wallet/core/spi/WalletOwnerManager.kt index abeb78825..9595b0051 100644 --- a/wallet/wallet-core/src/main/kotlin/co/nilin/opex/wallet/core/spi/WalletOwnerManager.kt +++ b/wallet/wallet-core/src/main/kotlin/co/nilin/opex/wallet/core/spi/WalletOwnerManager.kt @@ -10,7 +10,8 @@ interface WalletOwnerManager { suspend fun isDepositAllowed(owner: WalletOwner, amount: Amount): Boolean suspend fun isWithdrawAllowed(owner: WalletOwner, amount: Amount): Boolean suspend fun findWalletOwner(uuid: String): WalletOwner? - suspend fun createWalletOwner(uuid: String, title: String, userLevel: String): WalletOwner + suspend fun findWalletOwnerByExternalIdentifier(externalIdentifier: String): WalletOwner? + suspend fun createWalletOwner(uuid: String, title: String, userLevel: String, externalIdentifier: String?=null): WalletOwner suspend fun findAllWalletOwners(): List - suspend fun updateWalletOwnerName(uuid: String, name: String) + suspend fun updateWalletOwnerName(uuid: String, name: String, externalIdentifier: String?) } diff --git a/wallet/wallet-ports/wallet-eventlistener-kafka/src/main/kotlin/co/nilin/opex/wallet/ports/kafka/listener/model/ProfileUpdatedEvent.kt b/wallet/wallet-ports/wallet-eventlistener-kafka/src/main/kotlin/co/nilin/opex/wallet/ports/kafka/listener/model/ProfileUpdatedEvent.kt index 918a42309..ab5575714 100644 --- a/wallet/wallet-ports/wallet-eventlistener-kafka/src/main/kotlin/co/nilin/opex/wallet/ports/kafka/listener/model/ProfileUpdatedEvent.kt +++ b/wallet/wallet-ports/wallet-eventlistener-kafka/src/main/kotlin/co/nilin/opex/wallet/ports/kafka/listener/model/ProfileUpdatedEvent.kt @@ -5,5 +5,6 @@ data class ProfileUpdatedEvent( var firstName: String? = null, var lastName: String? = null, var email: String? = null, - var mobile: String? = null + var mobile: String? = null, + var identifier: String? = null, ) : AuthEvent() diff --git a/wallet/wallet-ports/wallet-persister-postgres/src/main/kotlin/co/nilin/opex/wallet/ports/postgres/dao/WalletOwnerRepository.kt b/wallet/wallet-ports/wallet-persister-postgres/src/main/kotlin/co/nilin/opex/wallet/ports/postgres/dao/WalletOwnerRepository.kt index 2425f4511..51ddd018c 100644 --- a/wallet/wallet-ports/wallet-persister-postgres/src/main/kotlin/co/nilin/opex/wallet/ports/postgres/dao/WalletOwnerRepository.kt +++ b/wallet/wallet-ports/wallet-persister-postgres/src/main/kotlin/co/nilin/opex/wallet/ports/postgres/dao/WalletOwnerRepository.kt @@ -11,4 +11,5 @@ interface WalletOwnerRepository : ReactiveCrudRepository @Query("select * from wallet_owner where uuid = :uuid") fun findByUuid(uuid: String): Mono + fun findByExternalIdentifier(externalIdentifier: String): Mono } \ No newline at end of file diff --git a/wallet/wallet-ports/wallet-persister-postgres/src/main/kotlin/co/nilin/opex/wallet/ports/postgres/impl/WalletOwnerManagerImpl.kt b/wallet/wallet-ports/wallet-persister-postgres/src/main/kotlin/co/nilin/opex/wallet/ports/postgres/impl/WalletOwnerManagerImpl.kt index b4811a52c..0d7329e39 100644 --- a/wallet/wallet-ports/wallet-persister-postgres/src/main/kotlin/co/nilin/opex/wallet/ports/postgres/impl/WalletOwnerManagerImpl.kt +++ b/wallet/wallet-ports/wallet-persister-postgres/src/main/kotlin/co/nilin/opex/wallet/ports/postgres/impl/WalletOwnerManagerImpl.kt @@ -131,8 +131,22 @@ class WalletOwnerManagerImpl( return walletOwnerRepository.findByUuid(uuid).awaitFirstOrNull()?.toPlainObject() } - override suspend fun createWalletOwner(uuid: String, title: String, userLevel: String): WalletOwner { - return walletOwnerRepository.save(WalletOwnerModel(null, uuid, title, userLevel)).awaitFirst().toPlainObject() + override suspend fun createWalletOwner( + uuid: String, + title: String, + userLevel: String, + externalIdentifier: String? + ): WalletOwner { + return walletOwnerRepository.save( + WalletOwnerModel( + null, + uuid, + title, + userLevel, + externalIdentifier = externalIdentifier + ) + ) + .awaitFirst().toPlainObject() } override suspend fun findAllWalletOwners(): List { @@ -152,14 +166,19 @@ class WalletOwnerManagerImpl( } } - override suspend fun updateWalletOwnerName(uuid: String, name: String) { + override suspend fun updateWalletOwnerName(uuid: String, name: String, externalIdentifier: String?) { val owner = walletOwnerRepository.findByUuid(uuid).awaitFirstOrNull() if (owner != null) { owner.title = owner.title.split('|')[0] + "|" + name + owner.externalIdentifier = externalIdentifier walletOwnerRepository.save(owner).awaitFirstOrNull() ?: logger.warn("Failed to update wallet owner name for UUID: $uuid") } else { logger.warn("Wallet owner not found for UUID: $uuid") } } + + override suspend fun findWalletOwnerByExternalIdentifier(externalIdentifier: String): WalletOwner? { + return walletOwnerRepository.findByExternalIdentifier(externalIdentifier).awaitFirstOrNull()?.toPlainObject() + } } diff --git a/wallet/wallet-ports/wallet-persister-postgres/src/main/kotlin/co/nilin/opex/wallet/ports/postgres/model/TerminalLocalizationModel.kt b/wallet/wallet-ports/wallet-persister-postgres/src/main/kotlin/co/nilin/opex/wallet/ports/postgres/model/TerminalLocalizationModel.kt index bc11d2b81..c1bf7892a 100644 --- a/wallet/wallet-ports/wallet-persister-postgres/src/main/kotlin/co/nilin/opex/wallet/ports/postgres/model/TerminalLocalizationModel.kt +++ b/wallet/wallet-ports/wallet-persister-postgres/src/main/kotlin/co/nilin/opex/wallet/ports/postgres/model/TerminalLocalizationModel.kt @@ -8,7 +8,7 @@ data class TerminalLocalizationModel( @Id var id: Long? = null, var terminalId: Long, - var description: String?=null, - var owner: String?=null, + var description: String? = null, + var owner: String? = null, var language: String, ) \ No newline at end of file diff --git a/wallet/wallet-ports/wallet-persister-postgres/src/main/kotlin/co/nilin/opex/wallet/ports/postgres/model/WalletOwnerModel.kt b/wallet/wallet-ports/wallet-persister-postgres/src/main/kotlin/co/nilin/opex/wallet/ports/postgres/model/WalletOwnerModel.kt index a047a7f62..b49c16f3c 100644 --- a/wallet/wallet-ports/wallet-persister-postgres/src/main/kotlin/co/nilin/opex/wallet/ports/postgres/model/WalletOwnerModel.kt +++ b/wallet/wallet-ports/wallet-persister-postgres/src/main/kotlin/co/nilin/opex/wallet/ports/postgres/model/WalletOwnerModel.kt @@ -16,4 +16,6 @@ data class WalletOwnerModel( var isWithdrawAllowed: Boolean = true, @Column("deposit_allowed") var isDepositAllowed: Boolean = true, + @Column("external_identifier") + var externalIdentifier: String?=null ) \ No newline at end of file diff --git a/wallet/wallet-ports/wallet-persister-postgres/src/main/resources/db/migration/V13__update_terminal_localization_table.sql b/wallet/wallet-ports/wallet-persister-postgres/src/main/resources/db/migration/V13__update_terminal_localization_table.sql new file mode 100644 index 000000000..0ec5cd86f --- /dev/null +++ b/wallet/wallet-ports/wallet-persister-postgres/src/main/resources/db/migration/V13__update_terminal_localization_table.sql @@ -0,0 +1,2 @@ +ALTER TABLE terminal_localization + ALTER COLUMN description DROP NOT NULL; \ No newline at end of file diff --git a/wallet/wallet-ports/wallet-persister-postgres/src/main/resources/db/migration/V14__update_wallet_owner_table.sql b/wallet/wallet-ports/wallet-persister-postgres/src/main/resources/db/migration/V14__update_wallet_owner_table.sql new file mode 100644 index 000000000..70326c8fe --- /dev/null +++ b/wallet/wallet-ports/wallet-persister-postgres/src/main/resources/db/migration/V14__update_wallet_owner_table.sql @@ -0,0 +1,4 @@ +ALTER TABLE wallet_owner add COLUMN external_identifier VARCHAR(100); +CREATE UNIQUE INDEX wallet_owner_external_identifier + ON wallet_owner (external_identifier) + WHERE external_identifier IS NOT NULL; \ No newline at end of file diff --git a/wallet/wallet-ports/wallet-persister-postgres/src/main/resources/db/migration/V15__update_deposit_table.sql b/wallet/wallet-ports/wallet-persister-postgres/src/main/resources/db/migration/V15__update_deposit_table.sql new file mode 100644 index 000000000..4d10437ee --- /dev/null +++ b/wallet/wallet-ports/wallet-persister-postgres/src/main/resources/db/migration/V15__update_deposit_table.sql @@ -0,0 +1,19 @@ +WITH duplicates AS ( + SELECT id + FROM ( + SELECT id, + ROW_NUMBER() OVER ( + PARTITION BY transaction_ref + ORDER BY id + ) rn + FROM deposits + ) t + WHERE rn > 1 +) +UPDATE deposits +SET transaction_ref = id::text || '_' || transaction_ref +WHERE id IN (SELECT id FROM duplicates); + +alter table deposits + add constraint transaction_ref_pk + unique (transaction_ref); \ No newline at end of file