libeufin

Integration and sandbox testing for FinTech APIs and data formats
Log | Files | Refs | Submodules | README | LICENSE

PreparedTransferApi.kt (5569B)


      1 /*
      2  * This file is part of LibEuFin.
      3  * Copyright (C) 2026 Taler Systems S.A.
      4 
      5  * LibEuFin is free software; you can redistribute it and/or modify
      6  * it under the terms of the GNU Affero General Public License as
      7  * published by the Free Software Foundation; either version 3, or
      8  * (at your option) any later version.
      9 
     10  * LibEuFin is distributed in the hope that it will be useful, but
     11  * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
     12  * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Affero General
     13  * Public License for more details.
     14 
     15  * You should have received a copy of the GNU Affero General Public
     16  * License along with LibEuFin; see the file COPYING.  If not, see
     17  * <http://www.gnu.org/licenses/>
     18  */
     19 
     20 package tech.libeufin.nexus.api
     21 
     22 import io.ktor.http.*
     23 import io.ktor.server.request.*
     24 import io.ktor.server.response.*
     25 import io.ktor.server.routing.*
     26 import io.github.smiley4.ktoropenapi.*
     27 import io.ktor.util.pipeline.*
     28 import tech.libeufin.common.*
     29 import tech.libeufin.common.crypto.CryptoUtil
     30 import tech.libeufin.nexus.NexusConfig
     31 import tech.libeufin.nexus.db.Database
     32 import tech.libeufin.nexus.db.TransferDAO.RegistrationResult
     33 import java.time.Instant
     34 import java.time.Duration
     35 
     36 fun Routing.preparedTransferAPI(db: Database, cfg: NexusConfig) = conditional(cfg.wireGatewayApiCfg) {
     37     get("/taler-prepared-transfer/config", {
     38         operationId = "getPreparedTransferConfig"
     39         description = "Get the configuration of the prepared transfer API"
     40         tags = listOf("Prepared Transfer")
     41         response {
     42             code(HttpStatusCode.OK) { description = "Configuration of the prepared transfer API"; body<PreparedTransferConfig>() }
     43         }
     44     }) {
     45         call.respond(
     46             PreparedTransferConfig(
     47                 currency = cfg.currency,
     48                 supported_formats = listOf(SubjectFormat.SIMPLE, SubjectFormat.CH_QR_BILL)
     49             )
     50         )
     51     }
     52     post("/taler-prepared-transfer/registration", {
     53         operationId = "registerTransfer"
     54         description = "Register a prepared transfer"
     55         tags = listOf("Prepared Transfer")
     56         request {
     57             body<SubjectRequest>()
     58         }
     59         response {
     60             code(HttpStatusCode.OK) { description = "Registration successful"; body<SubjectResult>() }
     61             code(HttpStatusCode.Conflict) { description = "Reserve pub or subject derivation already used" }
     62         }
     63     }) {
     64         val req = call.receive<SubjectRequest>();
     65 
     66         if (!req.verify())
     67             throw forbidden(
     68                 "invalid signature",
     69                 TalerErrorCode.BANK_BAD_SIGNATURE
     70             )
     71 
     72         val iban = req.credit_account.expectIban().iban
     73         val reference = if (iban == cfg.ebics.account.iban) {
     74             null
     75         } else if (iban == cfg.ebics.qrIban) {
     76             subjectFmtQrBill(req.authorization_pub)
     77         } else {
     78             throw conflict(
     79                 "Creditor account '${req.credit_account}' is not supported",
     80                 TalerErrorCode.BANK_UNKNOWN_CREDITOR
     81             )
     82         }
     83 
     84         when (val result = db.transfer.register(
     85             req.type,
     86             req.account_pub,
     87             req.authorization_pub,
     88             req.authorization_sig,
     89             req.recurrent,
     90             reference,
     91             Instant.now()
     92         )) {
     93             RegistrationResult.ReservePubReuse -> throw conflict(
     94                 "reserve_pub used already",
     95                 TalerErrorCode.BANK_DUPLICATE_RESERVE_PUB_SUBJECT
     96             )
     97             RegistrationResult.SubjectReuse ->  throw conflict(
     98                 "subject derivation used already",
     99                 TalerErrorCode.BANK_DERIVATION_REUSE
    100             )
    101             RegistrationResult.Success -> {
    102                 call.respond(
    103                     SubjectResult(
    104                         if (reference != null) {
    105                             listOf(TransferSubject.QrBill(reference, req.credit_amount))
    106                         } else {
    107                             listOf(TransferSubject.Simple(fmtIncomingSubject(IncomingType.map, req.authorization_pub), req.credit_amount))
    108                         },
    109                         TalerTimestamp.never()
    110                     )
    111                 )
    112             }
    113         }
    114     }
    115     post("/taler-prepared-transfer/unregistration", {
    116         operationId = "unregisterTransfer"
    117         description = "Unregister a prepared subject"
    118         tags = listOf("Prepared Transfer")
    119         response {
    120             code(HttpStatusCode.NoContent) { description = "Successfully unregistered" }
    121             code(HttpStatusCode.NotFound) { description = "Prepared transfer not found" }
    122             code(HttpStatusCode.Conflict) { description = "Invalid signature or timestamp too old" }
    123         }
    124     }) {
    125         val req = call.receive<Unregistration>();
    126 
    127         if (req.timestamp.instant.isBefore(Instant.now().minus(Duration.ofMinutes(15))))
    128             throw conflict(
    129                 "timestamp too old",
    130                 TalerErrorCode.BANK_OLD_TIMESTAMP
    131             )
    132 
    133         if (!req.verify())
    134             throw forbidden(
    135                 "invalid signature",
    136                 TalerErrorCode.BANK_BAD_SIGNATURE
    137             )
    138                 
    139         if (db.transfer.unregister(req.authorization_pub, Instant.now())) {
    140             call.respond(HttpStatusCode.NoContent)
    141         } else {
    142             throw notFound(
    143                 "Prepared transfer '${req.authorization_pub}' not found",
    144                 TalerErrorCode.BANK_TRANSACTION_NOT_FOUND
    145             )
    146         }
    147     }
    148 }