Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
4 changes: 4 additions & 0 deletions api/api-app/src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,9 @@ package co.nilin.opex.api.core.inout

import java.time.LocalDateTime

enum class BankAccountApprovalRequestStatus {
PENDING, APPROVED, REJECTED
}

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,
Expand All @@ -20,7 +16,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,
Expand All @@ -30,20 +26,19 @@ 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
)

data class UpdateBankAccountApprovalBody(
val id: Long,
val description: String? = null,
val status: BankAccountApprovalRequestStatus
val status: BankAccountStatus
)
Original file line number Diff line number Diff line change
@@ -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
)
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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(
Expand All @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String> {
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<String> {
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<String> {
val response = fiatScannerProxy.retryWebhook(id)
return ResponseEntity.status(response.status)
.contentType(MediaType.APPLICATION_JSON)
.body(response.body)
}
}
Loading
Loading