From 7d36eeec10eae822586aa7113e07c6d45d2d975f Mon Sep 17 00:00:00 2001 From: Amir Rajabi Date: Sat, 31 Oct 2026 18:30:59 +0330 Subject: [PATCH 1/2] feat(api): add fiat-scanner admin proxy and align bank account status naming Introduce an admin-facing integration with the fiat-scanner subscription service and clean up bank account approval status naming to remove a redundant enum. - Add FiatScannerProxy/FiatScannerProxyImpl to call fiat-scanner's podium statements and webhook endpoints, signing each request with HMAC-SHA256 via the new RequestSigner (timestamp, method, path, query, body hash). - Add FiatScannerAdminController exposing /opex/v1/admin/fiat-scanner/podium/statements, /opex/v1/admin/fiat-scanner/webhooks and /opex/v1/admin/fiat-scanner/webhooks/{id}/retry, relaying upstream status and body as-is. - Wire a dedicated fiatScannerWebClient bean (API-key header, larger in-memory buffer) and new app.fiat-scanner.{url,api-key,secret-key} config in application.yml and docker-compose.yml. - Replace BankAccountApprovalRequestStatus with BankAccountStatus across BankAccountApproval DTOs, since a review request has no state of its own beyond the bank account's own status; update controller OpenAPI docs (BankAccountAdminController, BankAccountController) accordingly (PENDING/APPROVED -> WAITING/ VERIFIED). - Add DEPOSIT and WITHDRAW to UiPage enum. --- .../opex/api/app/config/WebClientConfig.kt | 11 ++ .../src/main/resources/application.yml | 4 + .../api/core/inout/BankAccountApproval.kt | 17 +-- .../nilin/opex/api/core/inout/FiatScanner.kt | 13 ++ .../nilin/opex/api/core/inout/PageMessage.kt | 2 +- .../opex/api/core/spi/FiatScannerProxy.kt | 14 ++ .../controller/BankAccountAdminController.kt | 13 +- .../opex/controller/BankAccountController.kt | 2 +- .../controller/FiatScannerAdminController.kt | 144 ++++++++++++++++++ .../ports/proxy/impl/FiatScannerProxyImpl.kt | 109 +++++++++++++ .../api/ports/proxy/security/RequestSigner.kt | 40 +++++ docker-compose.yml | 3 + .../app/dto/UpdateBankAccountApprovalBody.kt | 5 +- .../BankAccountApprovalRequestManagement.kt | 26 ++-- .../app/service/BankAccountManagement.kt | 2 - .../BankAccountApprovalAdminResponse.kt | 4 +- .../profile/BankAccountApprovalRequest.kt | 1 - .../BankAccountApprovalRequestFilter.kt | 2 +- .../BankAccountApprovalRequestStatus.kt | 5 - .../BankAccountApprovalUserResponse.kt | 2 +- .../BankAccountApprovalRequestRepository.kt | 29 ++-- ...BankAccountApprovalRequestManagementImp.kt | 14 +- .../model/base/BankAccountApprovalRequest.kt | 2 - ...p_bank_account_approval_request_status.sql | 7 + 24 files changed, 395 insertions(+), 76 deletions(-) create mode 100644 api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/FiatScanner.kt create mode 100644 api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/FiatScannerProxy.kt create mode 100644 api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/FiatScannerAdminController.kt create mode 100644 api/api-ports/api-proxy-rest/src/main/kotlin/co/nilin/opex/api/ports/proxy/impl/FiatScannerProxyImpl.kt create mode 100644 api/api-ports/api-proxy-rest/src/main/kotlin/co/nilin/opex/api/ports/proxy/security/RequestSigner.kt delete mode 100644 profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/profile/BankAccountApprovalRequestStatus.kt create mode 100644 profile/profile-ports/profile-postgres/src/main/resources/db/migration/V11__drop_bank_account_approval_request_status.sql diff --git a/api/api-app/src/main/kotlin/co/nilin/opex/api/app/config/WebClientConfig.kt b/api/api-app/src/main/kotlin/co/nilin/opex/api/app/config/WebClientConfig.kt index c2d67d527..b2b52d89a 100644 --- a/api/api-app/src/main/kotlin/co/nilin/opex/api/app/config/WebClientConfig.kt +++ b/api/api-app/src/main/kotlin/co/nilin/opex/api/app/config/WebClientConfig.kt @@ -96,6 +96,17 @@ class WebClientConfig( .build() } + @Bean("fiatScannerWebClient") + fun fiatScannerWebClient( + @Value("\${app.fiat-scanner.api-key}") apiKey: String + ): WebClient { + return WebClient.builder() + .clientConnector(ReactorClientHttpConnector(client)) + .defaultHeader("X-API-Key", apiKey) + .codecs { it.defaultCodecs().maxInMemorySize(5 * 1024 * 1024) } + .build() + } + @Bean("keycloakWebClient") fun keycloakWebClient(logbook: Logbook): WebClient { val provider = ConnectionProvider.builder("keycloakPool") diff --git a/api/api-app/src/main/resources/application.yml b/api/api-app/src/main/resources/application.yml index 922b10665..3ca37ad4e 100644 --- a/api/api-app/src/main/resources/application.yml +++ b/api/api-app/src/main/resources/application.yml @@ -129,6 +129,10 @@ app: url: ${BC_GATEWAY_URL:http://opex-bc-gateway} storage: url: ${STORAGE_URL:http://opex-storage} + fiat-scanner: + url: ${FIAT_SCANNER_URL:} + api-key: ${FIAT_SCANNER_API_KEY:} + secret-key: ${FIAT_SCANNER_SECRET_KEY:} config: url: ${CONFIG_URL:http://opex-config} auth-gateway: diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/BankAccountApproval.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/BankAccountApproval.kt index b799e988d..a8cd665e2 100644 --- a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/BankAccountApproval.kt +++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/BankAccountApproval.kt @@ -2,13 +2,13 @@ package co.nilin.opex.api.core.inout import java.time.LocalDateTime -enum class BankAccountApprovalRequestStatus { - PENDING, APPROVED, REJECTED -} - +/** + * A review has no state of its own: the bank account's [BankAccountStatus] is it. WAITING means the + * review is still open, VERIFIED and REJECTED are how it ended. + */ data class BankAccountApprovalRequestFilter( val userId: String? = null, - val status: BankAccountApprovalRequestStatus? = null, + val status: BankAccountStatus? = null, val createDateFrom: Long? = null, val createDateTo: Long? = null, val limit: Int = 10, @@ -20,7 +20,7 @@ data class BankAccountApprovalAdminResponse( var id: Long, var bankAccountId: Long, var userId: String, - var status: BankAccountApprovalRequestStatus, + var status: BankAccountStatus? = null, var createDate: LocalDateTime, var updateDate: LocalDateTime? = null, var updater: String? = null, @@ -30,14 +30,13 @@ data class BankAccountApprovalAdminResponse( var iban: String? = null, var accountNumber: String? = null, var bank: String? = null, - var bankAccountStatus: BankAccountStatus? = null, var firstName: String? = null, var lastName: String? = null ) data class BankAccountApprovalUserResponse( var bankAccountId: Long, - var status: BankAccountApprovalRequestStatus, + var status: BankAccountStatus, var createDate: LocalDateTime, var description: String? = null ) @@ -45,5 +44,5 @@ data class BankAccountApprovalUserResponse( data class UpdateBankAccountApprovalBody( val id: Long, val description: String? = null, - val status: BankAccountApprovalRequestStatus + val status: BankAccountStatus ) diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/FiatScanner.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/FiatScanner.kt new file mode 100644 index 000000000..e1b884499 --- /dev/null +++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/FiatScanner.kt @@ -0,0 +1,13 @@ +package co.nilin.opex.api.core.inout + +data class PodiumStatementsRequest( + val depositNumber: String, + val startTime: Long, + val endTime: Long, + val limit: Int? = null, + val offset: Int? = null +) +data class FiatScannerResponse( + val status: Int, + val body: String +) diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/PageMessage.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/PageMessage.kt index ad1e52096..d81fee561 100644 --- a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/PageMessage.kt +++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/PageMessage.kt @@ -3,7 +3,7 @@ package co.nilin.opex.api.core.inout import co.nilin.opex.common.data.UserLanguage enum class UiPage { - MAIN, DASHBOARD, WALLET, LOGIN, REGISTER, SWAP, ADVANCE_MARKET + MAIN, DASHBOARD, WALLET, LOGIN, REGISTER, SWAP, ADVANCE_MARKET, DEPOSIT, WITHDRAW } enum class PageMessageType { diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/FiatScannerProxy.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/FiatScannerProxy.kt new file mode 100644 index 000000000..2e699d281 --- /dev/null +++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/FiatScannerProxy.kt @@ -0,0 +1,14 @@ +package co.nilin.opex.api.core.spi + +import co.nilin.opex.api.core.inout.FiatScannerResponse +import co.nilin.opex.api.core.inout.PodiumStatementsRequest + +interface FiatScannerProxy { + + suspend fun fetchPodiumStatements(request: PodiumStatementsRequest): FiatScannerResponse + + /** [status], [limit] and [offset] are all optional; upstream applies its own defaults. */ + suspend fun listWebhooks(status: String?, limit: Int?, offset: Int?): FiatScannerResponse + + suspend fun retryWebhook(id: Long): FiatScannerResponse +} diff --git a/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/BankAccountAdminController.kt b/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/BankAccountAdminController.kt index 274a4c8f8..c50254b18 100644 --- a/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/BankAccountAdminController.kt +++ b/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/BankAccountAdminController.kt @@ -43,8 +43,7 @@ Security: Bearer admin-token required. Required authority: ROLE_admin. Behavior: Lists bank account review requests, newest first unless `ascendingByTime` is true. Requests exist only while admin approval is enabled: with automatic verification off every account creates one, and with it on only an inquiry the provider could not answer (timeout, provider down) does. A provider answer that rejects the user's data rejects the account instead of creating a request. Each row carries the account under review and the owner's name, so no second call is needed. Filters: `userId`, `status`, `createDateFrom` and `createDateTo` (epoch millis), `limit`, `offset`, `ascendingByTime`. Every filter is optional. Allowed values: -- status: PENDING, APPROVED, REJECTED. -- bankAccountStatus: WAITING, VERIFIED, REJECTED.""", +- status: WAITING, VERIFIED, REJECTED. This is the bank account's own status; a review has none of its own, so WAITING is a review still open.""", security = [SecurityRequirement(name = "bearerAuth")], responses = [ ApiResponse( @@ -85,7 +84,7 @@ Allowed values: Security: Bearer admin-token required. Required authority: ROLE_admin. Behavior: Returns one review request with the account under review and the owner's first and last name. Allowed values: -- status: PENDING, APPROVED, REJECTED.""", +- status: WAITING, VERIFIED, REJECTED. This is the bank account's own status; a review has none of its own.""", security = [SecurityRequirement(name = "bearerAuth")], responses = [ ApiResponse( @@ -123,10 +122,10 @@ Allowed values: summary = "Update bank account approval request status", description = """PUT /opex/v1/admin/bank-account/approval-request. Security: Bearer admin-token required. Required authority: ROLE_admin. -Validation: `id` and `status` are required. `status` must be APPROVED or REJECTED, and only a PENDING request can be decided on. -Behavior: Closes the review and moves the bank account with it: APPROVED verifies the account, REJECTED rejects it. The acting admin is recorded. Only the status changes; the account data itself is never edited here. +Validation: `id` and `status` are required. `status` must be VERIFIED or REJECTED, and only an account still WAITING can be decided on. +Behavior: Closes the review by setting the bank account's status. The acting admin and the note are recorded on the request. A review has no status of its own, so this is the account's own status and there is nothing that could disagree with it. Only the status changes; the account data itself is never edited here. Allowed values: -- status: APPROVED, REJECTED.""", +- status: VERIFIED, REJECTED.""", security = [SecurityRequirement(name = "bearerAuth")], responses = [ ApiResponse( @@ -139,7 +138,7 @@ Allowed values: ), ApiResponse( responseCode = "400", - description = "The request is not pending, or the status is not APPROVED or REJECTED, or the same card or iban was verified for another user in the meantime.", + description = "The review is already closed, or the status is not VERIFIED or REJECTED, or the same card or iban was verified for another user in the meantime.", content = [Content()] ), ApiResponse( diff --git a/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/BankAccountController.kt b/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/BankAccountController.kt index 6ecdf2f12..8306184b4 100644 --- a/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/BankAccountController.kt +++ b/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/BankAccountController.kt @@ -98,7 +98,7 @@ Allowed values: Security: Bearer user-token required. Requires authenticated user JWT. Behavior: Returns the admin review of the caller's own bank account, including the admin's note once a decision is made. A bank account only has one when its identity inquiry could not be completed. Reading another user's bank account is forbidden. Allowed values: -- status: PENDING, APPROVED, REJECTED.""", +- status: WAITING, VERIFIED, REJECTED. This is the bank account's own status, so WAITING means the review is still open.""", security = [SecurityRequirement(name = "bearerAuth")], responses = [ ApiResponse( diff --git a/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/FiatScannerAdminController.kt b/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/FiatScannerAdminController.kt new file mode 100644 index 000000000..639594e4b --- /dev/null +++ b/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/FiatScannerAdminController.kt @@ -0,0 +1,144 @@ +package co.nilin.opex.api.ports.opex.controller + +import co.nilin.opex.api.core.inout.PodiumStatementsRequest +import co.nilin.opex.api.core.spi.FiatScannerProxy +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.http.MediaType +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RequestParam +import org.springframework.web.bind.annotation.RestController + +@RestController +@RequestMapping("/opex/v1/admin/fiat-scanner") +@Tag( + name = "Fiat Scanner Admin", + description = "Admin access to the fiat-scanner subscription service: bank statements and webhook deliveries." +) +class FiatScannerAdminController( + private val fiatScannerProxy: FiatScannerProxy +) { + + @PostMapping("/podium/statements", produces = [MediaType.APPLICATION_JSON_VALUE]) + @Operation( + summary = "Fetch bank statements from podium", + description = """POST /opex/v1/admin/fiat-scanner/podium/statements. +Security: Bearer admin-token required. Required authority: ROLE_admin. +Validation: `depositNumber`, `startTime` and `endTime` are required. The times are epoch millis. `limit` and `offset` are optional. +Behavior: Asks the fiat-scanner subscription service for the statements of a deposit number, which forwards to podium. The subscription service decides which deposit numbers this platform may read from its own subscriptions, so a deposit number outside them answers 403. +Response: Relayed from upstream as it arrived, body and status code both. The shape is podium's, not this gateway's, so it is passed through rather than remodelled here.""", + security = [SecurityRequirement(name = "bearerAuth")], + responses = [ + ApiResponse( + responseCode = "200", + description = "Successful response. The body is podium's statement payload.", + content = [Content(mediaType = "application/json", schema = Schema(implementation = String::class))] + ), + ApiResponse( + responseCode = "401", + description = "Unauthorized. Bearer token is missing, invalid, or expired. No response body.", + content = [Content()] + ), + ApiResponse( + responseCode = "403", + description = "Forbidden. Either the caller is missing ROLE_admin, or the deposit number is not one this platform may read.", + content = [Content()] + ) + ] + ) + suspend fun fetchPodiumStatements( + @RequestBody request: PodiumStatementsRequest + ): ResponseEntity { + val response = fiatScannerProxy.fetchPodiumStatements(request) + return ResponseEntity.status(response.status) + .contentType(MediaType.APPLICATION_JSON) + .body(response.body) + } + + @GetMapping("/webhooks", produces = [MediaType.APPLICATION_JSON_VALUE]) + @Operation( + summary = "List webhook deliveries", + description = """GET /opex/v1/admin/fiat-scanner/webhooks. +Security: Bearer admin-token required. Required authority: ROLE_admin. +Behavior: Lists the webhook calls the notifier made for this platform's deposit numbers. `status`, `limit` and `offset` are all optional and are passed through untouched; left out, upstream applies its own defaults of 100 and 0. +Response: Relayed from upstream as it arrived, body and status code both.""", + security = [SecurityRequirement(name = "bearerAuth")], + responses = [ + ApiResponse( + responseCode = "200", + description = "Successful response. The body is the notifier's webhook list.", + content = [Content(mediaType = "application/json", schema = Schema(implementation = String::class))] + ), + ApiResponse( + responseCode = "401", + description = "Unauthorized. Bearer token is missing, invalid, or expired. No response body.", + content = [Content()] + ), + ApiResponse( + responseCode = "403", + description = "Forbidden. Required authority is missing: ROLE_admin. No response body.", + content = [Content()] + ) + ] + ) + suspend fun listWebhooks( + @Parameter(name = "status", description = "Optional webhook delivery status filter, passed through to the notifier.") + @RequestParam(required = false) status: String?, + @Parameter(name = "limit", description = "Optional page size. Upstream defaults to 10.") + @RequestParam(required = false) limit: Int?, + @Parameter(name = "offset", description = "Optional page offset. Upstream defaults to 0.") + @RequestParam(required = false) offset: Int? + ): ResponseEntity { + val response = fiatScannerProxy.listWebhooks(status, limit, offset) + return ResponseEntity.status(response.status) + .contentType(MediaType.APPLICATION_JSON) + .body(response.body) + } + + @PostMapping("/webhooks/{id}/retry", produces = [MediaType.APPLICATION_JSON_VALUE]) + @Operation( + summary = "Retry a webhook delivery", + description = """POST /opex/v1/admin/fiat-scanner/webhooks/{id}/retry. +Security: Bearer admin-token required. Required authority: ROLE_admin. +Behavior: Asks the notifier to deliver a webhook call again. The subscription service checks the call belongs to one of this platform's deposit numbers first. +Response: Relayed from upstream as it arrived, body and status code both.""", + security = [SecurityRequirement(name = "bearerAuth")], + responses = [ + ApiResponse( + responseCode = "200", + description = "Successful response.", + content = [Content(mediaType = "application/json", schema = Schema(implementation = String::class))] + ), + ApiResponse( + responseCode = "401", + description = "Unauthorized. Bearer token is missing, invalid, or expired. No response body.", + content = [Content()] + ), + ApiResponse( + responseCode = "403", + description = "Forbidden. Either the caller is missing ROLE_admin, or the webhook call is not one this platform may touch.", + content = [Content()] + ), + ApiResponse(responseCode = "404", description = "No webhook call with this id.", content = [Content()]) + ] + ) + suspend fun retryWebhook( + @Parameter(name = "id", description = "Webhook call id.", required = true) + @PathVariable("id") id: Long + ): ResponseEntity { + val response = fiatScannerProxy.retryWebhook(id) + return ResponseEntity.status(response.status) + .contentType(MediaType.APPLICATION_JSON) + .body(response.body) + } +} diff --git a/api/api-ports/api-proxy-rest/src/main/kotlin/co/nilin/opex/api/ports/proxy/impl/FiatScannerProxyImpl.kt b/api/api-ports/api-proxy-rest/src/main/kotlin/co/nilin/opex/api/ports/proxy/impl/FiatScannerProxyImpl.kt new file mode 100644 index 000000000..fd5628f24 --- /dev/null +++ b/api/api-ports/api-proxy-rest/src/main/kotlin/co/nilin/opex/api/ports/proxy/impl/FiatScannerProxyImpl.kt @@ -0,0 +1,109 @@ +package co.nilin.opex.api.ports.proxy.impl + +import co.nilin.opex.api.core.inout.FiatScannerResponse +import co.nilin.opex.api.core.inout.PodiumStatementsRequest +import co.nilin.opex.api.core.spi.FiatScannerProxy +import co.nilin.opex.api.ports.proxy.security.RequestSigner +import co.nilin.opex.common.utils.LoggerDelegate +import com.fasterxml.jackson.databind.ObjectMapper +import kotlinx.coroutines.reactor.awaitSingle +import org.springframework.beans.factory.annotation.Qualifier +import org.springframework.beans.factory.annotation.Value +import org.springframework.http.MediaType +import org.springframework.stereotype.Component +import org.springframework.web.reactive.function.client.WebClient +import java.net.URI +import java.net.URLEncoder +import java.nio.charset.StandardCharsets + +@Component +class FiatScannerProxyImpl( + @Qualifier("fiatScannerWebClient") private val client: WebClient, + private val objectMapper: ObjectMapper, + @Value("\${app.fiat-scanner.url}") private val baseUrl: String, + @Value("\${app.fiat-scanner.secret-key}") private val secretKey: String +) : FiatScannerProxy { + + private val logger by LoggerDelegate() + + override suspend fun fetchPodiumStatements(request: PodiumStatementsRequest): FiatScannerResponse { + val path = "/v1/admin/podium/statements" + val body = objectMapper.writeValueAsBytes(request) + + return client.post() + .uri(URI.create("$baseUrl$path")) + .accept(MediaType.APPLICATION_JSON) + .contentType(MediaType.APPLICATION_JSON) + .headers { it.putAll(signatureHeaders("POST", path, "", body)) } + .bodyValue(body) + .exchangeToMono { response -> + response.bodyToMono(String::class.java) + .defaultIfEmpty("") + .map { FiatScannerResponse(response.statusCode().value(), it) } + } + .awaitSingle() + .also { logger.info("fiat-scanner podium statements answered ${it.status}") } + } + + override suspend fun listWebhooks(status: String?, limit: Int?, offset: Int?): FiatScannerResponse { + val path = "/v1/admin/webhooks" + // Built once and used for both the signature and the URI: the far side hashes the raw + // query string it receives, so the two have to be the same text + val rawQuery = buildList { + status?.takeIf { it.isNotBlank() } + ?.let { add("status=" + URLEncoder.encode(it, StandardCharsets.UTF_8)) } + limit?.let { add("limit=$it") } + offset?.let { add("offset=$it") } + }.joinToString("&") + val uri = if (rawQuery.isEmpty()) "$baseUrl$path" else "$baseUrl$path?$rawQuery" + + return client.get() + .uri(URI.create(uri)) + .accept(MediaType.APPLICATION_JSON) + .headers { it.putAll(signatureHeaders("GET", path, rawQuery, ByteArray(0))) } + .exchangeToMono { response -> + response.bodyToMono(String::class.java) + .defaultIfEmpty("") + .map { FiatScannerResponse(response.statusCode().value(), it) } + } + .awaitSingle() + .also { logger.info("fiat-scanner webhook list answered ${it.status}") } + } + + override suspend fun retryWebhook(id: Long): FiatScannerResponse { + val path = "/v1/admin/webhooks/$id/retry" + + return client.post() + .uri(URI.create("$baseUrl$path")) + .accept(MediaType.APPLICATION_JSON) + .headers { it.putAll(signatureHeaders("POST", path, "", ByteArray(0))) } + .exchangeToMono { response -> + response.bodyToMono(String::class.java) + .defaultIfEmpty("") + .map { FiatScannerResponse(response.statusCode().value(), it) } + } + .awaitSingle() + .also { logger.info("fiat-scanner webhook $id retry answered ${it.status}") } + } + + private fun signatureHeaders( + method: String, + path: String, + rawQuery: String, + body: ByteArray + ): Map> { + val timestamp = System.currentTimeMillis() + val signature = RequestSigner.sign( + secretKey = secretKey, + timestamp = timestamp, + method = method, + path = path, + rawQuery = rawQuery, + body = body + ) + return mapOf( + "X-Timestamp" to listOf(timestamp.toString()), + "X-Signature" to listOf(signature) + ) + } +} diff --git a/api/api-ports/api-proxy-rest/src/main/kotlin/co/nilin/opex/api/ports/proxy/security/RequestSigner.kt b/api/api-ports/api-proxy-rest/src/main/kotlin/co/nilin/opex/api/ports/proxy/security/RequestSigner.kt new file mode 100644 index 000000000..192a516f1 --- /dev/null +++ b/api/api-ports/api-proxy-rest/src/main/kotlin/co/nilin/opex/api/ports/proxy/security/RequestSigner.kt @@ -0,0 +1,40 @@ +package co.nilin.opex.api.ports.proxy.security + +import java.nio.charset.StandardCharsets +import java.security.MessageDigest +import javax.crypto.Mac +import javax.crypto.spec.SecretKeySpec + +object RequestSigner { + + private const val HMAC_ALGORITHM = "HmacSHA256" + + fun sign( + secretKey: String, + timestamp: Long, + method: String, + path: String, + rawQuery: String, + body: ByteArray + ): String { + val payload = listOf( + timestamp.toString(), + method.uppercase(), + path, + rawQuery, + sha256Hex(body) + ).joinToString("\n") + return hmacSha256Hex(secretKey, payload) + } + + private fun sha256Hex(body: ByteArray): String = + MessageDigest.getInstance("SHA-256").digest(body).toHex() + + private fun hmacSha256Hex(secretKey: String, payload: String): String { + val mac = Mac.getInstance(HMAC_ALGORITHM) + mac.init(SecretKeySpec(secretKey.toByteArray(StandardCharsets.UTF_8), HMAC_ALGORITHM)) + return mac.doFinal(payload.toByteArray(StandardCharsets.UTF_8)).toHex() + } + + private fun ByteArray.toHex(): String = joinToString("") { "%02x".format(it.toInt() and 0xff) } +} diff --git a/docker-compose.yml b/docker-compose.yml index 1d1360cc5..ccd6ff9a6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -519,6 +519,9 @@ services: - OPEN_API_SERVER_URL=${OPEN_API_SERVER_URL_API} - SUPPORT_ATTACHMENTS_BUCKET=${SUPPORT_ATTACHMENTS_BUCKET} - COMPANY_DOCUMENTS_BUCKET=${COMPANY_DOCUMENTS_BUCKET} + - FIAT_SCANNER_URL=${FIAT_SCANNER_URL} + - FIAT_SCANNER_API_KEY=${FIAT_SCANNER_API_KEY} + - FIAT_SCANNER_SECRET_KEY=${FIAT_SCANNER_SECRET_KEY} depends_on: - consul - postgres-api diff --git a/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/dto/UpdateBankAccountApprovalBody.kt b/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/dto/UpdateBankAccountApprovalBody.kt index 0fa9474d6..777e9fec3 100644 --- a/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/dto/UpdateBankAccountApprovalBody.kt +++ b/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/dto/UpdateBankAccountApprovalBody.kt @@ -1,10 +1,9 @@ package co.nilin.opex.profile.app.dto -import co.nilin.opex.profile.core.data.profile.BankAccountApprovalRequestStatus +import co.nilin.opex.profile.core.data.profile.BankAccountStatus data class UpdateBankAccountApprovalBody( val id: Long, val description: String?, - /** APPROVED or REJECTED. */ - val status: BankAccountApprovalRequestStatus + val status: BankAccountStatus ) diff --git a/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/service/BankAccountApprovalRequestManagement.kt b/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/service/BankAccountApprovalRequestManagement.kt index 83c3e665c..327e25825 100644 --- a/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/service/BankAccountApprovalRequestManagement.kt +++ b/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/service/BankAccountApprovalRequestManagement.kt @@ -4,7 +4,6 @@ import co.nilin.opex.common.OpexError import co.nilin.opex.profile.app.dto.UpdateBankAccountApprovalBody import co.nilin.opex.profile.core.data.profile.BankAccountApprovalAdminResponse import co.nilin.opex.profile.core.data.profile.BankAccountApprovalRequestFilter -import co.nilin.opex.profile.core.data.profile.BankAccountApprovalRequestStatus import co.nilin.opex.profile.core.data.profile.BankAccountApprovalUserResponse import co.nilin.opex.profile.core.data.profile.BankAccountStatus import co.nilin.opex.profile.core.spi.BankAccountApprovalRequestPersister @@ -46,31 +45,28 @@ class BankAccountApprovalRequestManagement( body: UpdateBankAccountApprovalBody, adminId: String ): BankAccountApprovalAdminResponse { - val approvalRequest = bankAccountApprovalRequestPersister.getRequestById(body.id) - - if (approvalRequest.status != BankAccountApprovalRequestStatus.PENDING || - body.status == BankAccountApprovalRequestStatus.PENDING - ) { - throw OpexError.InvalidBankAccountApprovalRequestStatus.exception() - } + if (body.status != BankAccountStatus.VERIFIED && body.status != BankAccountStatus.REJECTED) + throw OpexError.InvalidBankAccountApprovalRequestStatus.exception("Status must be VERIFIED or REJECTED") + val approvalRequest = bankAccountApprovalRequestPersister.getRequestById(body.id) val bankAccount = bankAccountPersister.findById(approvalRequest.bankAccountId) - if (body.status == BankAccountApprovalRequestStatus.APPROVED) { + if (bankAccount.status != BankAccountStatus.WAITING) + throw OpexError.InvalidBankAccountApprovalRequestStatus.exception("This review is already closed") + + if (body.status == BankAccountStatus.VERIFIED) assertNotVerifiedForAnotherUser(bankAccount.uuid, bankAccount.cardNumber, bankAccount.iban) - bankAccount.status = BankAccountStatus.VERIFIED - } else { - bankAccount.status = BankAccountStatus.REJECTED - } + + bankAccount.status = body.status bankAccount.updateDate = LocalDateTime.now() bankAccountPersister.update(bankAccount) - approvalRequest.status = body.status approvalRequest.updateDate = LocalDateTime.now() approvalRequest.updater = adminId approvalRequest.description = body.description + bankAccountApprovalRequestPersister.update(approvalRequest) - return bankAccountApprovalRequestPersister.update(approvalRequest) + return bankAccountApprovalRequestPersister.getRequestById(body.id) } private suspend fun assertNotVerifiedForAnotherUser(uuid: String, cardNumber: String?, iban: String?) { diff --git a/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/service/BankAccountManagement.kt b/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/service/BankAccountManagement.kt index 3680e0fb3..bdc7dc170 100644 --- a/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/service/BankAccountManagement.kt +++ b/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/service/BankAccountManagement.kt @@ -7,7 +7,6 @@ import co.nilin.opex.profile.app.utils.toBankAccountResponse import co.nilin.opex.profile.core.data.inquiry.IbanInfo import co.nilin.opex.profile.core.data.profile.BankAccount import co.nilin.opex.profile.core.data.profile.BankAccountApprovalRequest -import co.nilin.opex.profile.core.data.profile.BankAccountApprovalRequestStatus import co.nilin.opex.profile.core.data.profile.BankAccountStatus import co.nilin.opex.profile.core.data.profile.NationalityType import co.nilin.opex.profile.core.spi.BankAccountApprovalRequestPersister @@ -86,7 +85,6 @@ class BankAccountManagement( bankAccountId = bankAccount.id ?: throw OpexError.BadRequest.exception("Saved bank account has no id"), userId = bankAccount.uuid, - status = BankAccountApprovalRequestStatus.PENDING, createDate = LocalDateTime.now() ) ) diff --git a/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/profile/BankAccountApprovalAdminResponse.kt b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/profile/BankAccountApprovalAdminResponse.kt index a8741b7e7..e520cc2db 100644 --- a/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/profile/BankAccountApprovalAdminResponse.kt +++ b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/profile/BankAccountApprovalAdminResponse.kt @@ -6,18 +6,16 @@ data class BankAccountApprovalAdminResponse( var id: Long, var bankAccountId: Long, var userId: String, - var status: BankAccountApprovalRequestStatus, + var status: BankAccountStatus? = null, var createDate: LocalDateTime, var updateDate: LocalDateTime? = null, var updater: String? = null, var description: String? = null, - // The account under review, so the admin does not need a second call to see what to decide on var name: String? = null, var cardNumber: String? = null, var iban: String? = null, var accountNumber: String? = null, var bank: String? = null, - var bankAccountStatus: BankAccountStatus? = null, var firstName: String? = null, var lastName: String? = null ) diff --git a/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/profile/BankAccountApprovalRequest.kt b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/profile/BankAccountApprovalRequest.kt index 408de4a9a..9555daa8a 100644 --- a/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/profile/BankAccountApprovalRequest.kt +++ b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/profile/BankAccountApprovalRequest.kt @@ -5,7 +5,6 @@ import java.time.LocalDateTime data class BankAccountApprovalRequest( var bankAccountId: Long, var userId: String, - var status: BankAccountApprovalRequestStatus? = BankAccountApprovalRequestStatus.PENDING, var createDate: LocalDateTime? = null, var updateDate: LocalDateTime? = null, var updater: String? = null, diff --git a/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/profile/BankAccountApprovalRequestFilter.kt b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/profile/BankAccountApprovalRequestFilter.kt index d4320f4de..4567662b1 100644 --- a/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/profile/BankAccountApprovalRequestFilter.kt +++ b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/profile/BankAccountApprovalRequestFilter.kt @@ -2,7 +2,7 @@ package co.nilin.opex.profile.core.data.profile data class BankAccountApprovalRequestFilter( val userId: String? = null, - val status: BankAccountApprovalRequestStatus? = null, + val status: BankAccountStatus? = null, val createDateFrom: Long? = null, val createDateTo: Long? = null, val limit: Int = 10, diff --git a/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/profile/BankAccountApprovalRequestStatus.kt b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/profile/BankAccountApprovalRequestStatus.kt deleted file mode 100644 index d19fd7742..000000000 --- a/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/profile/BankAccountApprovalRequestStatus.kt +++ /dev/null @@ -1,5 +0,0 @@ -package co.nilin.opex.profile.core.data.profile - -enum class BankAccountApprovalRequestStatus { - PENDING, APPROVED, REJECTED -} diff --git a/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/profile/BankAccountApprovalUserResponse.kt b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/profile/BankAccountApprovalUserResponse.kt index a094b3e92..3460f3578 100644 --- a/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/profile/BankAccountApprovalUserResponse.kt +++ b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/profile/BankAccountApprovalUserResponse.kt @@ -4,7 +4,7 @@ import java.time.LocalDateTime data class BankAccountApprovalUserResponse( var bankAccountId: Long, - var status: BankAccountApprovalRequestStatus, + var status: BankAccountStatus, var createDate: LocalDateTime, var description: String? = null ) diff --git a/profile/profile-ports/profile-postgres/src/main/kotlin/co/nilin/opex/profile/ports/postgres/dao/BankAccountApprovalRequestRepository.kt b/profile/profile-ports/profile-postgres/src/main/kotlin/co/nilin/opex/profile/ports/postgres/dao/BankAccountApprovalRequestRepository.kt index c6f41f090..b4d8a8b84 100644 --- a/profile/profile-ports/profile-postgres/src/main/kotlin/co/nilin/opex/profile/ports/postgres/dao/BankAccountApprovalRequestRepository.kt +++ b/profile/profile-ports/profile-postgres/src/main/kotlin/co/nilin/opex/profile/ports/postgres/dao/BankAccountApprovalRequestRepository.kt @@ -1,8 +1,8 @@ package co.nilin.opex.profile.ports.postgres.dao import co.nilin.opex.profile.core.data.profile.BankAccountApprovalAdminResponse -import co.nilin.opex.profile.core.data.profile.BankAccountApprovalRequestStatus import co.nilin.opex.profile.core.data.profile.BankAccountApprovalUserResponse +import co.nilin.opex.profile.core.data.profile.BankAccountStatus import co.nilin.opex.profile.ports.postgres.model.entity.BankAccountApprovalRequestModel import kotlinx.coroutines.flow.Flow import org.springframework.data.r2dbc.repository.Query @@ -15,10 +15,7 @@ import java.time.LocalDateTime interface BankAccountApprovalRequestRepository : ReactiveCrudRepository { - fun findByBankAccountIdAndStatus( - bankAccountId: Long, - status: BankAccountApprovalRequestStatus - ): Mono + fun findByBankAccountId(bankAccountId: Long): Mono @Query( """ @@ -26,7 +23,6 @@ interface BankAccountApprovalRequestRepository : r.id, r.bank_account_id, r.user_id, - r.status, r.create_date, r.update_date, r.updater, @@ -36,14 +32,14 @@ interface BankAccountApprovalRequestRepository : b.iban, b.account_number, b.bank, - b.status AS bank_account_status, + b.status, u.first_name, u.last_name FROM bank_account_approval_request r LEFT JOIN bank_account b ON b.id = r.bank_account_id LEFT JOIN profile u ON u.user_id = r.user_id WHERE (:userId IS NULL OR r.user_id = :userId) - AND (:status IS NULL OR r.status = :status) + AND (:status IS NULL OR b.status = :status) AND (:createDateFrom IS NULL OR r.create_date >= :createDateFrom) AND (:createDateTo IS NULL OR r.create_date <= :createDateTo) ORDER BY r.create_date @@ -52,7 +48,7 @@ interface BankAccountApprovalRequestRepository : ) fun findByCriteriaAsc( userId: String?, - status: BankAccountApprovalRequestStatus?, + status: BankAccountStatus?, createDateFrom: LocalDateTime?, createDateTo: LocalDateTime?, limit: Int, @@ -65,7 +61,6 @@ interface BankAccountApprovalRequestRepository : r.id, r.bank_account_id, r.user_id, - r.status, r.create_date, r.update_date, r.updater, @@ -75,14 +70,14 @@ interface BankAccountApprovalRequestRepository : b.iban, b.account_number, b.bank, - b.status AS bank_account_status, + b.status, u.first_name, u.last_name FROM bank_account_approval_request r LEFT JOIN bank_account b ON b.id = r.bank_account_id LEFT JOIN profile u ON u.user_id = r.user_id WHERE (:userId IS NULL OR r.user_id = :userId) - AND (:status IS NULL OR r.status = :status) + AND (:status IS NULL OR b.status = :status) AND (:createDateFrom IS NULL OR r.create_date >= :createDateFrom) AND (:createDateTo IS NULL OR r.create_date <= :createDateTo) ORDER BY r.create_date desc @@ -91,7 +86,7 @@ interface BankAccountApprovalRequestRepository : ) fun findByCriteriaDesc( userId: String?, - status: BankAccountApprovalRequestStatus?, + status: BankAccountStatus?, createDateFrom: LocalDateTime?, createDateTo: LocalDateTime?, limit: Int, @@ -104,7 +99,6 @@ interface BankAccountApprovalRequestRepository : r.id, r.bank_account_id, r.user_id, - r.status, r.create_date, r.update_date, r.updater, @@ -114,7 +108,7 @@ interface BankAccountApprovalRequestRepository : b.iban, b.account_number, b.bank, - b.status AS bank_account_status, + b.status, u.first_name, u.last_name FROM bank_account_approval_request r @@ -127,12 +121,13 @@ interface BankAccountApprovalRequestRepository : @Query( """ - select r.bank_account_id, r.status, r.create_date, r.description + select r.bank_account_id, b.status, r.create_date, r.description from bank_account_approval_request r + left join bank_account b on b.id = r.bank_account_id where r.bank_account_id = :bankAccountId order by r.create_date desc limit 1 """ ) - fun findByBankAccountId(bankAccountId: Long): Mono + fun findUserResponseByBankAccountId(bankAccountId: Long): Mono } diff --git a/profile/profile-ports/profile-postgres/src/main/kotlin/co/nilin/opex/profile/ports/postgres/imp/BankAccountApprovalRequestManagementImp.kt b/profile/profile-ports/profile-postgres/src/main/kotlin/co/nilin/opex/profile/ports/postgres/imp/BankAccountApprovalRequestManagementImp.kt index b4ca343ee..678e1c331 100644 --- a/profile/profile-ports/profile-postgres/src/main/kotlin/co/nilin/opex/profile/ports/postgres/imp/BankAccountApprovalRequestManagementImp.kt +++ b/profile/profile-ports/profile-postgres/src/main/kotlin/co/nilin/opex/profile/ports/postgres/imp/BankAccountApprovalRequestManagementImp.kt @@ -4,7 +4,6 @@ import co.nilin.opex.common.OpexError import co.nilin.opex.profile.core.data.profile.BankAccountApprovalAdminResponse import co.nilin.opex.profile.core.data.profile.BankAccountApprovalRequest import co.nilin.opex.profile.core.data.profile.BankAccountApprovalRequestFilter -import co.nilin.opex.profile.core.data.profile.BankAccountApprovalRequestStatus import co.nilin.opex.profile.core.data.profile.BankAccountApprovalUserResponse import co.nilin.opex.profile.core.spi.BankAccountApprovalRequestPersister import co.nilin.opex.profile.core.utils.convert @@ -24,12 +23,10 @@ class BankAccountApprovalRequestManagementImp( ) : BankAccountApprovalRequestPersister { override suspend fun save(request: BankAccountApprovalRequest): BankAccountApprovalRequest { - bankAccountApprovalRequestRepository.findByBankAccountIdAndStatus( - request.bankAccountId, - BankAccountApprovalRequestStatus.PENDING - ).awaitFirstOrNull()?.let { - throw OpexError.BankAccountApprovalRequestAlreadyExists.exception() - } ?: run { + bankAccountApprovalRequestRepository.findByBankAccountId(request.bankAccountId) + .awaitFirstOrNull()?.let { + throw OpexError.BankAccountApprovalRequestAlreadyExists.exception() + } ?: run { val model: BankAccountApprovalRequestModel = request.convert(BankAccountApprovalRequestModel::class.java) return bankAccountApprovalRequestRepository.save(model).awaitFirst() @@ -70,7 +67,8 @@ class BankAccountApprovalRequestManagementImp( } override suspend fun getRequestByBankAccountId(bankAccountId: Long): BankAccountApprovalUserResponse { - return bankAccountApprovalRequestRepository.findByBankAccountId(bankAccountId).awaitFirstOrNull() + return bankAccountApprovalRequestRepository.findUserResponseByBankAccountId(bankAccountId) + .awaitFirstOrNull() ?: throw OpexError.BankAccountApprovalRequestNotFound.exception() } diff --git a/profile/profile-ports/profile-postgres/src/main/kotlin/co/nilin/opex/profile/ports/postgres/model/base/BankAccountApprovalRequest.kt b/profile/profile-ports/profile-postgres/src/main/kotlin/co/nilin/opex/profile/ports/postgres/model/base/BankAccountApprovalRequest.kt index 1395e5ee3..d3834fd5f 100644 --- a/profile/profile-ports/profile-postgres/src/main/kotlin/co/nilin/opex/profile/ports/postgres/model/base/BankAccountApprovalRequest.kt +++ b/profile/profile-ports/profile-postgres/src/main/kotlin/co/nilin/opex/profile/ports/postgres/model/base/BankAccountApprovalRequest.kt @@ -1,12 +1,10 @@ package co.nilin.opex.profile.ports.postgres.model.base -import co.nilin.opex.profile.core.data.profile.BankAccountApprovalRequestStatus import java.time.LocalDateTime open class BankAccountApprovalRequest { var bankAccountId: Long = 0 lateinit var userId: String - var status: BankAccountApprovalRequestStatus? = BankAccountApprovalRequestStatus.PENDING var createDate: LocalDateTime? = null var updateDate: LocalDateTime? = null var updater: String? = null diff --git a/profile/profile-ports/profile-postgres/src/main/resources/db/migration/V11__drop_bank_account_approval_request_status.sql b/profile/profile-ports/profile-postgres/src/main/resources/db/migration/V11__drop_bank_account_approval_request_status.sql new file mode 100644 index 000000000..3aa0edd05 --- /dev/null +++ b/profile/profile-ports/profile-postgres/src/main/resources/db/migration/V11__drop_bank_account_approval_request_status.sql @@ -0,0 +1,7 @@ +ALTER TABLE bank_account_approval_request + DROP CONSTRAINT IF EXISTS bank_account_approval_request_bank_account_id_status_key; +DROP INDEX IF EXISTS idx_bank_account_approval_request_status_create_date; +ALTER TABLE bank_account_approval_request + DROP COLUMN IF EXISTS status; +ALTER TABLE bank_account_approval_request + ADD CONSTRAINT uq_bank_account_approval_request_bank_account UNIQUE (bank_account_id); From 4ba6950a2ad1f90396577a14747a0049144e0d6f Mon Sep 17 00:00:00 2001 From: Amir Rajabi Date: Sat, 31 Oct 2026 18:34:00 +0330 Subject: [PATCH 2/2] Remove stale comment --- .../co/nilin/opex/api/core/inout/BankAccountApproval.kt | 4 ---- .../nilin/opex/api/ports/proxy/impl/FiatScannerProxyImpl.kt | 2 -- 2 files changed, 6 deletions(-) diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/BankAccountApproval.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/BankAccountApproval.kt index a8cd665e2..fe0a78b3d 100644 --- a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/BankAccountApproval.kt +++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/BankAccountApproval.kt @@ -2,10 +2,6 @@ package co.nilin.opex.api.core.inout import java.time.LocalDateTime -/** - * A review has no state of its own: the bank account's [BankAccountStatus] is it. WAITING means the - * review is still open, VERIFIED and REJECTED are how it ended. - */ data class BankAccountApprovalRequestFilter( val userId: String? = null, val status: BankAccountStatus? = null, diff --git a/api/api-ports/api-proxy-rest/src/main/kotlin/co/nilin/opex/api/ports/proxy/impl/FiatScannerProxyImpl.kt b/api/api-ports/api-proxy-rest/src/main/kotlin/co/nilin/opex/api/ports/proxy/impl/FiatScannerProxyImpl.kt index fd5628f24..faf309a77 100644 --- a/api/api-ports/api-proxy-rest/src/main/kotlin/co/nilin/opex/api/ports/proxy/impl/FiatScannerProxyImpl.kt +++ b/api/api-ports/api-proxy-rest/src/main/kotlin/co/nilin/opex/api/ports/proxy/impl/FiatScannerProxyImpl.kt @@ -47,8 +47,6 @@ class FiatScannerProxyImpl( override suspend fun listWebhooks(status: String?, limit: Int?, offset: Int?): FiatScannerResponse { val path = "/v1/admin/webhooks" - // Built once and used for both the signature and the URI: the far side hashes the raw - // query string it receives, so the two have to be the same text val rawQuery = buildList { status?.takeIf { it.isNotBlank() } ?.let { add("status=" + URLEncoder.encode(it, StandardCharsets.UTF_8)) }