commit 95994e9af317eb115a64341e179c76772afc3055
parent b37353d25cf0e4a8c08fea5ecd6437ccdec550e3
Author: Marcello Stanisci <stanisci.m@gmail.com>
Date: Thu, 9 Apr 2020 18:39:48 +0200
DB definitions for payments ordered by the exchange.
Diffstat:
3 files changed, 83 insertions(+), 34 deletions(-)
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/DB.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/DB.kt
@@ -10,6 +10,33 @@ import java.sql.Connection
const val ID_MAX_LENGTH = 50
+/**
+ * This table holds the values that exchange gave to issue a payment,
+ * plus a reference to the prepared pain.001 version of.
+ */
+object TalerRequestedPayments: LongIdTable() {
+ val payment = TalerIncomingPayments.reference("payment", Pain001Table)
+ val requestUId = text("request_uid")
+ val amount = text("amount")
+ val exchangeBaseUrl = text("exchange_base_url")
+ val wtid = text("wtid")
+ val creditAccount = text("credit_account")
+}
+
+class TalerRequestedPaymentEntity(id: EntityID<Long>) : LongEntity(id) {
+ var payment by Pain001Entity referencedOn TalerRequestedPayments.payment
+ var requestUId by TalerRequestedPayments.requestUId
+ var amount by TalerRequestedPayments.amount
+ var exchangeBaseUrl by TalerRequestedPayments.exchangeBaseUrl
+ var wtid by TalerRequestedPayments.wtid
+ var creditAccount by TalerRequestedPayments.creditAccount
+}
+
+/**
+ * This table "augments" the information given in the raw payments table, with Taler-related
+ * ones. It tells if a payment is valid and/or it was refunded already. And moreover, it is
+ * the table whose ("clean") IDs the exchange will base its history requests on.
+ */
object TalerIncomingPayments: LongIdTable() {
val payment = reference("payment", EbicsRawBankTransactionsTable)
val valid = bool("valid")
@@ -17,10 +44,15 @@ object TalerIncomingPayments: LongIdTable() {
val refunded = bool("refunded").default(false)
}
-class TalerIncomingPaymentEntry(id: EntityID<Long>) : LongEntity(id) {
- companion object : LongEntityClass<TalerIncomingPaymentEntry>(TalerIncomingPayments) {
- override fun new(init: TalerIncomingPaymentEntry.() -> Unit): TalerIncomingPaymentEntry {
+class TalerIncomingPaymentEntity(id: EntityID<Long>) : LongEntity(id) {
+ companion object : LongEntityClass<TalerIncomingPaymentEntity>(TalerIncomingPayments) {
+ override fun new(init: TalerIncomingPaymentEntity.() -> Unit): TalerIncomingPaymentEntity {
val newRow = super.new(init)
+ /**
+ * In case the exchange asks for all the values strictly lesser than MAX_VALUE,
+ * it would lose the row whose id == MAX_VALUE. So the check below makes this
+ * situation impossible by disallowing MAX_VALUE as a id value.
+ */
if (newRow.id.value == Long.MAX_VALUE) {
throw NexusError(
HttpStatusCode.InsufficientStorage, "Cannot store rows anymore"
@@ -29,7 +61,7 @@ class TalerIncomingPaymentEntry(id: EntityID<Long>) : LongEntity(id) {
return newRow
}
}
- var payment by EbicsRawBankTransactionEntry referencedOn TalerIncomingPayments.payment
+ var payment by EbicsRawBankTransactionEntity referencedOn TalerIncomingPayments.payment
var valid by TalerIncomingPayments.valid
var refunded by TalerIncomingPayments.refunded
}
@@ -59,8 +91,8 @@ object EbicsRawBankTransactionsTable : LongIdTable() {
val bookingDate = text("bookingDate")
}
-class EbicsRawBankTransactionEntry(id: EntityID<Long>) : LongEntity(id) {
- companion object : LongEntityClass<EbicsRawBankTransactionEntry>(EbicsRawBankTransactionsTable)
+class EbicsRawBankTransactionEntity(id: EntityID<Long>) : LongEntity(id) {
+ companion object : LongEntityClass<EbicsRawBankTransactionEntity>(EbicsRawBankTransactionsTable)
var sourceType by EbicsRawBankTransactionsTable.sourceType // C52 or C53 or C54?
var sourceFileName by EbicsRawBankTransactionsTable.sourceFileName
var unstructuredRemittanceInformation by EbicsRawBankTransactionsTable.unstructuredRemittanceInformation
@@ -76,6 +108,12 @@ class EbicsRawBankTransactionEntry(id: EntityID<Long>) : LongEntity(id) {
var nexusSubscriber by EbicsSubscriberEntity referencedOn EbicsRawBankTransactionsTable.nexusSubscriber
}
+/**
+ * NOTE: every column in this table corresponds to a particular
+ * value described in the pain.001 official documentation; therefore
+ * this table is not really suitable to hold custom data (like Taler-related,
+ * for example)
+ */
object Pain001Table : IntIdTableWithAmount() {
val msgId = long("msgId").uniqueIndex().autoIncrement()
val paymentId = long("paymentId")
@@ -93,7 +131,7 @@ object Pain001Table : IntIdTableWithAmount() {
/* Indicates whether the bank didn't perform the payment: note that
* this state can be reached when the payment gets listed in a CRZ
- * response OR when the payment doesn's show up in a C52/C53 response
+ * response OR when the payment doesn't show up in a C52/C53 response
*/
val invalid = bool("invalid").default(false)
}
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt
@@ -314,7 +314,7 @@ fun createPain001document(pain001Entity: Pain001Entity): String {
/**
* Insert one row in the database, and leaves it marked as non-submitted.
*/
-fun createPain001entry(entry: Pain001Data, debtorAccountId: String) {
+fun createPain001entity(entry: Pain001Data, debtorAccountId: String) {
val randomId = Random().nextLong()
transaction {
Pain001Entity.new {
@@ -483,7 +483,7 @@ fun main() {
accountInfo.id.value
}
val pain001data = call.receive<Pain001Data>()
- createPain001entry(pain001data, acctid)
+ createPain001entity(pain001data, acctid)
call.respondText(
"Payment instructions persisted in DB",
ContentType.Text.Plain, HttpStatusCode.OK
@@ -630,7 +630,7 @@ fun main() {
var ret = ""
transaction {
val subscriber: EbicsSubscriberEntity = getSubscriberEntityFromId(id)
- EbicsRawBankTransactionEntry.find {
+ EbicsRawBankTransactionEntity.find {
(EbicsRawBankTransactionsTable.nexusSubscriber eq subscriber.id.value) and
(EbicsRawBankTransactionsTable.sourceType eq "C53")
}.forEach {
@@ -668,7 +668,7 @@ fun main() {
val fileName = it.first
val camt53doc = XMLUtil.parseStringIntoDom(it.second)
transaction {
- EbicsRawBankTransactionEntry.new {
+ EbicsRawBankTransactionEntity.new {
sourceType = "C53"
sourceFileName = fileName
unstructuredRemittanceInformation = camt53doc.pickString("//*[local-name()='Ntry']//*[local-name()='Amt']/@Ccy")
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/taler.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/taler.kt
@@ -17,7 +17,6 @@ import org.joda.time.format.DateTimeFormat
import tech.libeufin.util.Amount
import tech.libeufin.util.CryptoUtil
import tech.libeufin.util.toZonedString
-import java.util.*
import kotlin.math.abs
class Taler(app: Route) {
@@ -79,7 +78,9 @@ class Taler(app: Route) {
val row_id: Long
)
- /** Helper data structures. */
+ /**
+ * Helper data structures.
+ */
data class Payto(
val name: String,
val iban: String,
@@ -90,7 +91,9 @@ class Taler(app: Route) {
val amount: Amount
)
- /** Helper functions */
+ /**
+ * Helper functions
+ */
fun parsePayto(paytoUri: String): Payto {
// payto://iban/BIC?/IBAN?name=<name>
@@ -106,7 +109,7 @@ class Taler(app: Route) {
val (currency, number) = match.destructured
return AmountWithCurrency(currency, Amount(number))
}
-
+ /** Sort query results in descending order for negative deltas, and ascending otherwise. */
private fun <T : Entity<Long>> SizedIterable<T>.orderTaler(delta: Int): List<T> {
return if (delta < 0) {
this.sortedByDescending { it.id }
@@ -120,10 +123,8 @@ class Taler(app: Route) {
private fun parseDate(date: String): DateTime {
return DateTime.parse(date, DateTimeFormat.forPattern("YYYY-MM-DD"))
}
- /**
- * Builds the comparison operator for history entries based on the
- * sign of 'delta'
- */
+
+ /** Builds the comparison operator for history entries based on the sign of 'delta' */
private fun getComparisonOperator(delta: Int, start: Long): Op<Boolean> {
return if (delta < 0) {
Expression.build {
@@ -135,9 +136,7 @@ class Taler(app: Route) {
}
}
}
- /**
- * Helper handling 'start' being optional and its dependence on 'delta'.
- */
+ /** Helper handling 'start' being optional and its dependence on 'delta'. */
private fun handleStartArgument(start: String?, delta: Int): Long {
return expectLong(start) ?: if (delta >= 0) {
/**
@@ -158,16 +157,15 @@ class Taler(app: Route) {
/** attaches Taler endpoints to the main Web server */
init {
+ /** Test-API that creates one new payment addressed to the exchange. */
app.post("/taler/admin/add-incoming") {
val exchangeId = authenticateRequest(call.request.headers["Authorization"])
val addIncomingData = call.receive<TalerAdminAddIncoming>()
val debtor = parsePayto(addIncomingData.debit_account)
val amount = parseAmount(addIncomingData.amount)
-
- /** Decompose amount and payto fields. */
val (bookingDate, opaque_row_id) = transaction {
val exchangeBankAccount = getBankAccountsInfoFromId(exchangeId).first()
- val rawPayment = EbicsRawBankTransactionEntry.new {
+ val rawPayment = EbicsRawBankTransactionEntity.new {
sourceFileName = "test"
sourceType = "C53"
unstructuredRemittanceInformation = addIncomingData.reserve_pub
@@ -183,7 +181,7 @@ class Taler(app: Route) {
}
/** This payment is "valid by default" and will be returned
* as soon as the exchange will ask for new payments. */
- val row = TalerIncomingPaymentEntry.new {
+ val row = TalerIncomingPaymentEntity.new {
payment = rawPayment
}
Pair(rawPayment.bookingDate, row.id.value)
@@ -194,6 +192,11 @@ class Taler(app: Route) {
))
return@post
}
+
+ /** This endpoint triggers the refunding of invalid payments. 'Refunding'
+ * in this context means that nexus _prepares_ the payment instruction and
+ * places it into a further table. Eventually, another routine will perform
+ * all the prepared payments. */
app.post("/ebics/taler/{id}/accounts/{acctid}/refund-invalid-payments") {
transaction {
val subscriber = expectIdTransaction(call.parameters["id"])
@@ -204,10 +207,10 @@ class Taler(app: Route) {
"Such subscriber (${subscriber.id}) can't drive such account (${acctid.id})"
)
}
- TalerIncomingPaymentEntry.find {
+ TalerIncomingPaymentEntity.find {
TalerIncomingPayments.refunded eq false and (TalerIncomingPayments.valid eq false)
}.forEach {
- createPain001entry(
+ createPain001entity(
Pain001Data(
creditorName = it.payment.debitorName,
creditorIban = it.payment.debitorIban,
@@ -222,6 +225,11 @@ class Taler(app: Route) {
}
return@post
}
+
+ /** This endpoint triggers the examination of raw incoming payments aimed
+ * at separating the good payments (those that will lead to a new reserve
+ * being created), from the invalid payments (those with a invalid subject
+ * that will soon be refunded.) */
app.post("/ebics/taler/{id}/digest-incoming-transactions") {
val id = expectId(call.parameters["id"])
// first find highest ID value of already processed rows.
@@ -235,22 +243,22 @@ class Taler(app: Route) {
* that was last processed. On the other hand, the "row_id" value that the exchange
* will get along each history element will be the id in the _digested entries table_.
*/
- val latestId: Long = TalerIncomingPaymentEntry.all().sortedByDescending {
+ val latestId: Long = TalerIncomingPaymentEntity.all().sortedByDescending {
it.payment.id
}.firstOrNull()?.payment?.id?.value ?: -1
val subscriberAccount = getBankAccountsInfoFromId(id).first()
/* search for fresh transactions having the exchange IBAN in the creditor field. */
- EbicsRawBankTransactionEntry.find {
+ EbicsRawBankTransactionEntity.find {
EbicsRawBankTransactionsTable.creditorIban eq subscriberAccount.iban and
(EbicsRawBankTransactionsTable.id.greater(latestId))
}.forEach {
if (CryptoUtil.checkValidEddsaPublicKey(it.unstructuredRemittanceInformation)) {
- TalerIncomingPaymentEntry.new {
+ TalerIncomingPaymentEntity.new {
payment = it
valid = true
}
} else {
- TalerIncomingPaymentEntry.new {
+ TalerIncomingPaymentEntity.new {
payment = it
valid = false
}
@@ -264,6 +272,8 @@ class Taler(app: Route) {
)
return@post
}
+ /** Responds only with the payments that the EXCHANGE made. Typically to
+ * merchants but possibly to refund invalid incoming payments. */
app.get("/taler/history/outgoing") {
/* sanitize URL arguments */
val subscriberId = authenticateRequest(call.request.headers["Authorization"])
@@ -275,7 +285,7 @@ class Taler(app: Route) {
transaction {
/** Retrieve all the outgoing payments from the _raw transactions table_ */
val subscriberBankAccount = getBankAccountsInfoFromId(subscriberId)
- EbicsRawBankTransactionEntry.find {
+ EbicsRawBankTransactionEntity.find {
EbicsRawBankTransactionsTable.debitorIban eq subscriberBankAccount.first().iban and startCmpOp
}.orderTaler(delta).subList(0, abs(delta)).forEach {
history.outgoing_transactions.add(
@@ -297,6 +307,7 @@ class Taler(app: Route) {
)
return@get
}
+ /** Responds only with the valid incoming payments */
app.get("/taler/history/incoming") {
val subscriberId = authenticateRequest(call.request.headers["Authorization"])
val delta: Int = expectInt(call.expectUrlParameter("delta"))
@@ -305,7 +316,7 @@ class Taler(app: Route) {
val startCmpOp = getComparisonOperator(delta, start)
transaction {
val subscriberBankAccount = getBankAccountsInfoFromId(subscriberId)
- TalerIncomingPaymentEntry.find {
+ TalerIncomingPaymentEntity.find {
TalerIncomingPayments.valid eq true and startCmpOp
}.orderTaler(delta).subList(0, abs(delta)).forEach {
history.incoming_transactions.add(