summaryrefslogtreecommitdiff
path: root/bank/src/main/kotlin/tech/libeufin/bank/DB.kt
diff options
context:
space:
mode:
Diffstat (limited to 'bank/src/main/kotlin/tech/libeufin/bank/DB.kt')
-rw-r--r--bank/src/main/kotlin/tech/libeufin/bank/DB.kt747
1 files changed, 747 insertions, 0 deletions
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/DB.kt b/bank/src/main/kotlin/tech/libeufin/bank/DB.kt
new file mode 100644
index 00000000..523b1bc3
--- /dev/null
+++ b/bank/src/main/kotlin/tech/libeufin/bank/DB.kt
@@ -0,0 +1,747 @@
+/*
+ * This file is part of LibEuFin.
+ * Copyright (C) 2019 Stanisci and Dold.
+
+ * LibEuFin is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation; either version 3, or
+ * (at your option) any later version.
+
+ * LibEuFin is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General
+ * Public License for more details.
+
+ * You should have received a copy of the GNU Affero General Public
+ * License along with LibEuFin; see the file COPYING. If not, see
+ * <http://www.gnu.org/licenses/>
+ */
+
+package tech.libeufin.sandbox
+
+import io.ktor.http.*
+import org.jetbrains.exposed.dao.Entity
+import org.jetbrains.exposed.dao.EntityClass
+import org.jetbrains.exposed.dao.IntEntity
+import org.jetbrains.exposed.dao.LongEntity
+import org.jetbrains.exposed.dao.IntEntityClass
+import org.jetbrains.exposed.dao.LongEntityClass
+import org.jetbrains.exposed.dao.id.EntityID
+import org.jetbrains.exposed.dao.id.IdTable
+import org.jetbrains.exposed.dao.id.IntIdTable
+import org.jetbrains.exposed.dao.id.LongIdTable
+import org.jetbrains.exposed.sql.*
+import org.jetbrains.exposed.sql.transactions.transaction
+import tech.libeufin.util.*
+import kotlin.reflect.*
+import kotlin.reflect.full.*
+
+/**
+ * All the states to give a subscriber.
+ */
+enum class SubscriberState {
+ /**
+ * No keys at all given to the bank.
+ */
+ NEW,
+
+ /**
+ * Only INI electronic message was successfully sent.
+ */
+ PARTIALLY_INITIALIZED_INI,
+
+ /**r
+ * Only HIA electronic message was successfully sent.
+ */
+ PARTIALLY_INITIALIZED_HIA,
+
+ /**
+ * Both INI and HIA were electronically sent with success.
+ */
+ INITIALIZED,
+
+ /**
+ * All the keys accounted in INI and HIA have been confirmed
+ * via physical mail.
+ */
+ READY
+}
+
+/**
+ * All the states that one key can be assigned.
+ */
+enum class KeyState {
+
+ /**
+ * The key was never communicated.
+ */
+ MISSING,
+
+ /**
+ * The key has been electronically sent.
+ */
+ NEW,
+
+ /**
+ * The key has been confirmed (either via physical mail
+ * or electronically -- e.g. with certificates)
+ */
+ RELEASED
+}
+
+/**
+ * Stores one config object to the database. Each field
+ * name and value populate respectively the configKey and
+ * configValue columns. Rows are defined in the following way:
+ * demobankName | configKey | configValue
+ */
+fun insertConfigPairs(config: DemobankConfig, override: Boolean = false) {
+ // Fill the config key-value pairs in the DB.
+ config::class.declaredMemberProperties.forEach { configField ->
+ val maybeValue = configField.getter.call(config)
+ if (override) {
+ val maybeConfigPair = DemobankConfigPairEntity.find {
+ DemobankConfigPairsTable.configKey eq configField.name
+ }.firstOrNull()
+ if (maybeConfigPair == null)
+ throw internalServerError("Cannot override config value '${configField.name}' not found.")
+ maybeConfigPair.configValue = maybeValue?.toString()
+ return@forEach
+ }
+ DemobankConfigPairEntity.new {
+ this.demobankName = config.demobankName
+ this.configKey = configField.name
+ this.configValue = maybeValue?.toString()
+ }
+ }
+}
+
+object DemobankConfigPairsTable : LongIdTable() {
+ val demobankName = text("demobankName")
+ val configKey = text("configKey")
+ val configValue = text("configValue").nullable()
+}
+
+class DemobankConfigPairEntity(id: EntityID<Long>) : LongEntity(id) {
+ companion object : LongEntityClass<DemobankConfigPairEntity>(DemobankConfigPairsTable)
+ var demobankName by DemobankConfigPairsTable.demobankName
+ var configKey by DemobankConfigPairsTable.configKey
+ var configValue by DemobankConfigPairsTable.configValue
+}
+
+object DemobankConfigsTable : LongIdTable() {
+ val name = text("hostname")
+}
+
+// Helpers for handling config values in memory.
+typealias DemobankConfigKey = String
+typealias DemobankConfigValue = String?
+fun Pair<DemobankConfigKey, DemobankConfigValue>.expectValue(): String {
+ if (this.second == null) throw internalServerError("Config value for '${this.first}' is null in the database.")
+ return this.second as String
+}
+
+class DemobankConfigEntity(id: EntityID<Long>) : LongEntity(id) {
+ companion object : LongEntityClass<DemobankConfigEntity>(DemobankConfigsTable)
+ var name by DemobankConfigsTable.name
+ /**
+ * This object gets defined by parsing all the configuration
+ * values found in the DB for one demobank. Those values are
+ * retrieved from _another_ table.
+ */
+ val config: DemobankConfig by lazy {
+ // Getting all the values for this demobank.
+ val configPairs: List<Pair<DemobankConfigKey, DemobankConfigValue>> = transaction {
+ val maybeConfigPairs = DemobankConfigPairEntity.find {
+ DemobankConfigPairsTable.demobankName.eq(name)
+ }
+ if (maybeConfigPairs.empty()) throw SandboxError(
+ HttpStatusCode.InternalServerError,
+ "No config values of $name were found in the database"
+ )
+ // Copying results to a DB-agnostic list, to later operate out of "transaction {}"
+ maybeConfigPairs.map { Pair(it.configKey, it.configValue) }
+ }
+ // Building the args to instantiate a DemobankConfig (non-Exposed) object.
+ val args = mutableMapOf<KParameter, Any?>()
+ // For each constructor parameter name, find the same-named database entry.
+ val configClass = DemobankConfig::class
+ if (configClass.primaryConstructor == null) {
+ throw SandboxError(
+ HttpStatusCode.InternalServerError,
+ "${configClass.simpleName} primaryConstructor is null."
+ )
+ }
+ if (configClass.primaryConstructor?.parameters == null) {
+ throw SandboxError(
+ HttpStatusCode.InternalServerError,
+ "${configClass.simpleName} primaryConstructor" +
+ " arguments is null. Cannot set any config value."
+ )
+ }
+ // For each field in the config object, find the respective DB row.
+ configClass.primaryConstructor?.parameters?.forEach { par: KParameter ->
+ val configPairFromDb: Pair<DemobankConfigKey, DemobankConfigValue>?
+ = configPairs.firstOrNull {
+ configPair: Pair<DemobankConfigKey, DemobankConfigValue> ->
+ configPair.first == par.name
+ }
+ if (configPairFromDb == null) {
+ throw SandboxError(
+ HttpStatusCode.InternalServerError,
+ "Config key '${par.name}' not found in the database."
+ )
+ }
+ when(par.type) {
+ // non-nullable
+ typeOf<Boolean>() -> { args[par] = configPairFromDb.expectValue().toBoolean() }
+ typeOf<Int>() -> { args[par] = configPairFromDb.expectValue().toInt() }
+ // nullable
+ typeOf<Boolean?>() -> { args[par] = configPairFromDb.second?.toBoolean() }
+ typeOf<Int?>() -> { args[par] = configPairFromDb.second?.toInt() }
+ else -> args[par] = configPairFromDb.second
+ }
+ }
+ // Proceeding now to instantiate the config class, and make it a field of this type.
+ configClass.primaryConstructor!!.callBy(args)
+ }
+}
+
+/**
+ * Users who are allowed to log into the demo bank.
+ * Created via the /demobanks/{demobankname}/register endpoint.
+ */
+object DemobankCustomersTable : LongIdTable() {
+ val username = text("username")
+ val passwordHash = text("passwordHash")
+ val name = text("name").nullable()
+ val email = text("email").nullable()
+ val phone = text("phone").nullable()
+ val cashout_address = text("cashout_address").nullable()
+}
+
+class DemobankCustomerEntity(id: EntityID<Long>) : LongEntity(id) {
+ companion object : LongEntityClass<DemobankCustomerEntity>(DemobankCustomersTable)
+ var username by DemobankCustomersTable.username
+ var passwordHash by DemobankCustomersTable.passwordHash
+ var name by DemobankCustomersTable.name
+ var email by DemobankCustomersTable.email
+ var phone by DemobankCustomersTable.phone
+ var cashout_address by DemobankCustomersTable.cashout_address
+}
+
+/**
+ * This table stores RSA public keys of subscribers.
+ */
+object EbicsSubscriberPublicKeysTable : IntIdTable() {
+ val rsaPublicKey = blob("rsaPublicKey")
+ val state = enumeration("state", KeyState::class)
+}
+
+class EbicsSubscriberPublicKeyEntity(id: EntityID<Int>) : IntEntity(id) {
+ companion object : IntEntityClass<EbicsSubscriberPublicKeyEntity>(EbicsSubscriberPublicKeysTable)
+ var rsaPublicKey by EbicsSubscriberPublicKeysTable.rsaPublicKey
+ var state by EbicsSubscriberPublicKeysTable.state
+}
+
+/**
+ * Ebics 'host'(s) that are served by one Sandbox instance.
+ */
+object EbicsHostsTable : IntIdTable() {
+ val hostID = text("hostID")
+ val ebicsVersion = text("ebicsVersion")
+ val signaturePrivateKey = blob("signaturePrivateKey")
+ val encryptionPrivateKey = blob("encryptionPrivateKey")
+ val authenticationPrivateKey = blob("authenticationPrivateKey")
+}
+
+class EbicsHostEntity(id: EntityID<Int>) : IntEntity(id) {
+ companion object : IntEntityClass<EbicsHostEntity>(EbicsHostsTable)
+ var hostId by EbicsHostsTable.hostID
+ var ebicsVersion by EbicsHostsTable.ebicsVersion
+ var signaturePrivateKey by EbicsHostsTable.signaturePrivateKey
+ var encryptionPrivateKey by EbicsHostsTable.encryptionPrivateKey
+ var authenticationPrivateKey by EbicsHostsTable.authenticationPrivateKey
+}
+
+/**
+ * Ebics Subscribers table.
+ */
+object EbicsSubscribersTable : IntIdTable() {
+ val userId = text("userID")
+ val partnerId = text("partnerID")
+ val systemId = text("systemID").nullable()
+ val hostId = text("hostID")
+ val signatureKey = reference("signatureKey", EbicsSubscriberPublicKeysTable).nullable()
+ val encryptionKey = reference("encryptionKey", EbicsSubscriberPublicKeysTable).nullable()
+ val authenticationKey = reference("authorizationKey", EbicsSubscriberPublicKeysTable).nullable()
+ val nextOrderID = integer("nextOrderID")
+ val state = enumeration("state", SubscriberState::class)
+ val bankAccount = reference(
+ "bankAccount",
+ BankAccountsTable,
+ onDelete = ReferenceOption.CASCADE
+ ).nullable()
+}
+
+class EbicsSubscriberEntity(id: EntityID<Int>) : IntEntity(id) {
+ companion object : IntEntityClass<EbicsSubscriberEntity>(EbicsSubscribersTable)
+ var userId by EbicsSubscribersTable.userId
+ var partnerId by EbicsSubscribersTable.partnerId
+ var systemId by EbicsSubscribersTable.systemId
+ var hostId by EbicsSubscribersTable.hostId
+ var signatureKey by EbicsSubscriberPublicKeyEntity optionalReferencedOn EbicsSubscribersTable.signatureKey
+ var encryptionKey by EbicsSubscriberPublicKeyEntity optionalReferencedOn EbicsSubscribersTable.encryptionKey
+ var authenticationKey by EbicsSubscriberPublicKeyEntity optionalReferencedOn EbicsSubscribersTable.authenticationKey
+ var nextOrderID by EbicsSubscribersTable.nextOrderID
+ var state by EbicsSubscribersTable.state
+ var bankAccount by BankAccountEntity optionalReferencedOn EbicsSubscribersTable.bankAccount
+}
+
+/**
+ * Details of a download order.
+ */
+object EbicsDownloadTransactionsTable : IdTable<String>() {
+ override val id = text("transactionID").entityId()
+ val orderType = text("orderType")
+ val host = reference("host", EbicsHostsTable)
+ val subscriber = reference("subscriber", EbicsSubscribersTable)
+ val encodedResponse = text("encodedResponse")
+ val transactionKeyEnc = blob("transactionKeyEnc")
+ val numSegments = integer("numSegments")
+ val segmentSize = integer("segmentSize")
+ val receiptReceived = bool("receiptReceived")
+}
+
+class EbicsDownloadTransactionEntity(id: EntityID<String>) : Entity<String>(id) {
+ companion object : EntityClass<String, EbicsDownloadTransactionEntity>(EbicsDownloadTransactionsTable)
+
+ var orderType by EbicsDownloadTransactionsTable.orderType
+ var host by EbicsHostEntity referencedOn EbicsDownloadTransactionsTable.host
+ var subscriber by EbicsSubscriberEntity referencedOn EbicsDownloadTransactionsTable.subscriber
+ var encodedResponse by EbicsDownloadTransactionsTable.encodedResponse
+ var numSegments by EbicsDownloadTransactionsTable.numSegments
+ var transactionKeyEnc by EbicsDownloadTransactionsTable.transactionKeyEnc
+ var segmentSize by EbicsDownloadTransactionsTable.segmentSize
+ var receiptReceived by EbicsDownloadTransactionsTable.receiptReceived
+}
+
+/**
+ * Details of a upload order.
+ */
+object EbicsUploadTransactionsTable : IdTable<String>() {
+ override val id = text("transactionID").entityId()
+ val orderType = text("orderType")
+ val orderID = text("orderID")
+ val host = reference("host", EbicsHostsTable)
+ val subscriber = reference("subscriber", EbicsSubscribersTable)
+ val numSegments = integer("numSegments")
+ val lastSeenSegment = integer("lastSeenSegment")
+ val transactionKeyEnc = blob("transactionKeyEnc")
+}
+
+class EbicsUploadTransactionEntity(id: EntityID<String>) : Entity<String>(id) {
+ companion object : EntityClass<String, EbicsUploadTransactionEntity>(EbicsUploadTransactionsTable)
+ var orderType by EbicsUploadTransactionsTable.orderType
+ var orderID by EbicsUploadTransactionsTable.orderID
+ var host by EbicsHostEntity referencedOn EbicsUploadTransactionsTable.host
+ var subscriber by EbicsSubscriberEntity referencedOn EbicsUploadTransactionsTable.subscriber
+ var numSegments by EbicsUploadTransactionsTable.numSegments
+ var lastSeenSegment by EbicsUploadTransactionsTable.lastSeenSegment
+ var transactionKeyEnc by EbicsUploadTransactionsTable.transactionKeyEnc
+}
+
+/**
+ * FIXME: document this.
+ */
+object EbicsOrderSignaturesTable : IntIdTable() {
+ val orderID = text("orderID")
+ val orderType = text("orderType")
+ val partnerID = text("partnerID")
+ val userID = text("userID")
+ val signatureAlgorithm = text("signatureAlgorithm")
+ val signatureValue = blob("signatureValue")
+}
+
+class EbicsOrderSignatureEntity(id: EntityID<Int>) : IntEntity(id) {
+ companion object : IntEntityClass<EbicsOrderSignatureEntity>(EbicsOrderSignaturesTable)
+ var orderID by EbicsOrderSignaturesTable.orderID
+ var orderType by EbicsOrderSignaturesTable.orderType
+ var partnerID by EbicsOrderSignaturesTable.partnerID
+ var userID by EbicsOrderSignaturesTable.userID
+ var signatureAlgorithm by EbicsOrderSignaturesTable.signatureAlgorithm
+ var signatureValue by EbicsOrderSignaturesTable.signatureValue
+}
+
+/**
+ * FIXME: document this.
+ */
+object EbicsUploadTransactionChunksTable : IdTable<String>() {
+ override val id = text("transactionID").entityId()
+ val chunkIndex = integer("chunkIndex")
+ val chunkContent = blob("chunkContent")
+}
+
+// FIXME: Is upload chunking not implemented somewhere?!
+class EbicsUploadTransactionChunkEntity(id: EntityID<String>) : Entity<String>(id) {
+ companion object : EntityClass<String, EbicsUploadTransactionChunkEntity>(EbicsUploadTransactionChunksTable)
+ var chunkIndex by EbicsUploadTransactionChunksTable.chunkIndex
+ var chunkContent by EbicsUploadTransactionChunksTable.chunkContent
+}
+
+
+/**
+ * Holds those transactions that aren't yet reported in a Camt.053 document.
+ * After reporting those, the table gets emptied. Rows are merely references
+ * to the main ledger.
+ */
+object BankAccountFreshTransactionsTable : LongIdTable() {
+ val transactionRef = reference(
+ "transaction",
+ BankAccountTransactionsTable,
+ onDelete = ReferenceOption.CASCADE
+ )
+}
+class BankAccountFreshTransactionEntity(id: EntityID<Long>) : LongEntity(id) {
+ companion object : LongEntityClass<BankAccountFreshTransactionEntity>(BankAccountFreshTransactionsTable)
+ var transactionRef by BankAccountTransactionEntity referencedOn BankAccountFreshTransactionsTable.transactionRef
+}
+
+/**
+ * Table that keeps all the payments initiated by PAIN.001.
+ */
+object BankAccountTransactionsTable : LongIdTable() {
+ val creditorIban = text("creditorIban")
+ val creditorBic = text("creditorBic").nullable()
+ val creditorName = text("creditorName")
+ val debtorIban = text("debtorIban")
+ val debtorBic = text("debtorBic").nullable()
+ val debtorName = text("debtorName")
+ val subject = text("subject")
+ // Amount is a BigDecimal in String form.
+ val amount = text("amount")
+ val currency = text("currency")
+ // Milliseconds since the Epoch.
+ val date = long("date")
+
+ /**
+ * UID assigned to the payment by Sandbox. Despite the camt-looking
+ * name, this UID is always given, even when no EBICS or camt are being
+ * served.
+ */
+ val accountServicerReference = text("accountServicerReference")
+ /**
+ * The following two values are pain.001 specific. Sandbox stores
+ * them when it serves EBICS connections.
+ */
+ val pmtInfId = text("pmtInfId").nullable()
+ val endToEndId = text("EndToEndId").nullable()
+ val direction = text("direction")
+ /**
+ * Bank account of the party whose 'direction' refers. This version allows
+ * only both parties to be registered at the running Sandbox.
+ */
+ val account = reference(
+ "account", BankAccountsTable,
+ onDelete = ReferenceOption.CASCADE
+ )
+ // Redundantly storing the demobank for query convenience.
+ val demobank = reference("demobank", DemobankConfigsTable)
+}
+
+class BankAccountTransactionEntity(id: EntityID<Long>) : LongEntity(id) {
+ companion object : LongEntityClass<BankAccountTransactionEntity>(BankAccountTransactionsTable) {
+ override fun new(init: BankAccountTransactionEntity.() -> Unit): BankAccountTransactionEntity {
+ /**
+ * Fresh transactions are those that wait to be included in a
+ * "history" report, likely a Camt.5x message. The "fresh transactions"
+ * table keeps a list of such transactions.
+ */
+ val freshTx = super.new(init)
+ BankAccountFreshTransactionsTable.insert {
+ it[transactionRef] = freshTx.id
+ }
+ /**
+ * The bank account involved in this transaction points to
+ * it as the "last known" transaction, to make it easier to
+ * build histories that depend on such record.
+ */
+ freshTx.account.lastTransaction = freshTx
+ return freshTx
+ }
+ }
+ var creditorIban by BankAccountTransactionsTable.creditorIban
+ var creditorBic by BankAccountTransactionsTable.creditorBic
+ var creditorName by BankAccountTransactionsTable.creditorName
+ var debtorIban by BankAccountTransactionsTable.debtorIban
+ var debtorBic by BankAccountTransactionsTable.debtorBic
+ var debtorName by BankAccountTransactionsTable.debtorName
+ var subject by BankAccountTransactionsTable.subject
+ var amount by BankAccountTransactionsTable.amount
+ var currency by BankAccountTransactionsTable.currency
+ var date by BankAccountTransactionsTable.date
+ var accountServicerReference by BankAccountTransactionsTable.accountServicerReference
+ var pmtInfId by BankAccountTransactionsTable.pmtInfId
+ var endToEndId by BankAccountTransactionsTable.endToEndId
+ var direction by BankAccountTransactionsTable.direction
+ var account by BankAccountEntity referencedOn BankAccountTransactionsTable.account
+ var demobank by DemobankConfigEntity referencedOn BankAccountTransactionsTable.demobank
+}
+
+/**
+ * Table that keeps information about which bank accounts (iban+bic+name)
+ * are active in the system. In the current version, 'label' and 'owner'
+ * are always equal; future versions may change this, when one customer can
+ * own multiple bank accounts.
+ */
+object BankAccountsTable : IntIdTable() {
+ val balance = text("balance").default("0")
+ val iban = text("iban")
+ val bic = text("bic").default("SANDBOXX")
+ val label = text("label").uniqueIndex("accountLabelIndex")
+ /**
+ * This field is the username of the customer that owns the
+ * bank account. Admin is the only exception: that can specify
+ * this field as "admin" although no customer backs it.
+ */
+ val owner = text("owner")
+ val isPublic = bool("isPublic").default(false)
+ val demoBank = reference("demoBank", DemobankConfigsTable)
+
+ /**
+ * Point to the last transaction related to this account, regardless
+ * of it being credit or debit. This reference helps to construct
+ * history results that start from / depend on the last transaction.
+ */
+ val lastTransaction = reference("lastTransaction", BankAccountTransactionsTable).nullable()
+
+ /**
+ * Points to the transaction that was last submitted by the conversion
+ * service to Nexus, in order to initiate a fiat payment related to a
+ * cash-out operation.
+ */
+ val lastFiatSubmission = reference("lastFiatSubmission", BankAccountTransactionsTable).nullable()
+
+ /**
+ * Tracks the last fiat payment that was read from Nexus. This tracker
+ * gets updated ONLY IF the exchange gets successfully paid with the related
+ * amount in the regional currency.
+ */
+ val lastFiatFetch = text("lastFiatFetch").default("0")
+}
+
+class BankAccountEntity(id: EntityID<Int>) : IntEntity(id) {
+ companion object : IntEntityClass<BankAccountEntity>(BankAccountsTable)
+
+ var balance by BankAccountsTable.balance
+ var iban by BankAccountsTable.iban
+ var bic by BankAccountsTable.bic
+ var label by BankAccountsTable.label
+ var owner by BankAccountsTable.owner
+ var isPublic by BankAccountsTable.isPublic
+ var demoBank by DemobankConfigEntity referencedOn BankAccountsTable.demoBank
+ var lastTransaction by BankAccountTransactionEntity optionalReferencedOn BankAccountsTable.lastTransaction
+ var lastFiatSubmission by BankAccountTransactionEntity optionalReferencedOn BankAccountsTable.lastFiatSubmission
+ var lastFiatFetch by BankAccountsTable.lastFiatFetch
+}
+
+object BankAccountStatementsTable : IntIdTable() {
+ val statementId = text("statementId")
+ val creationTime = long("creationTime")
+ val xmlMessage = text("xmlMessage")
+ val bankAccount = reference("bankAccount", BankAccountsTable)
+ // Signed BigDecimal representing a Camt.053 CLBD field.
+ val balanceClbd = text("balanceClbd").nullable()
+}
+
+class BankAccountStatementEntity(id: EntityID<Int>) : IntEntity(id) {
+ companion object : IntEntityClass<BankAccountStatementEntity>(BankAccountStatementsTable)
+ var statementId by BankAccountStatementsTable.statementId
+ var creationTime by BankAccountStatementsTable.creationTime
+ var xmlMessage by BankAccountStatementsTable.xmlMessage
+ var bankAccount by BankAccountEntity referencedOn BankAccountStatementsTable.bankAccount
+ var balanceClbd by BankAccountStatementsTable.balanceClbd
+}
+
+enum class CashoutOperationStatus { CONFIRMED, PENDING }
+object CashoutOperationsTable : LongIdTable() {
+ val uuid = uuid("uuid").autoGenerate()
+ /**
+ * This amount is the one the user entered in the cash-out
+ * dialog. That will show up as the outgoing transfer in their
+ * local currency bank account.
+ */
+ val amountDebit = text("amountDebit")
+ val amountCredit = text("amountCredit")
+ val buyAtRatio = text("buyAtRatio")
+ val buyInFee = text("buyInFee")
+ val sellAtRatio = text("sellAtRatio")
+ val sellOutFee = text("sellOutFee")
+ val subject = text("subject")
+ val creationTime = long("creationTime") // in milliseconds.
+ val confirmationTime = long("confirmationTime").nullable() // in milliseconds.
+ val tanChannel = enumeration("tanChannel", SupportedTanChannels::class)
+ val account = text("account")
+ val cashoutAddress = text("cashoutAddress")
+ val tan = text("tan")
+ val status = enumeration("status", CashoutOperationStatus::class).default(CashoutOperationStatus.PENDING)
+}
+
+class CashoutOperationEntity(id: EntityID<Long>) : LongEntity(id) {
+ companion object : LongEntityClass<CashoutOperationEntity>(CashoutOperationsTable)
+ var uuid by CashoutOperationsTable.uuid
+ var amountDebit by CashoutOperationsTable.amountDebit
+ var amountCredit by CashoutOperationsTable.amountCredit
+ var buyAtRatio by CashoutOperationsTable.buyAtRatio
+ var buyInFee by CashoutOperationsTable.buyInFee
+ var sellAtRatio by CashoutOperationsTable.sellAtRatio
+ var sellOutFee by CashoutOperationsTable.sellOutFee
+ var subject by CashoutOperationsTable.subject
+ var creationTime by CashoutOperationsTable.creationTime
+ var confirmationTime by CashoutOperationsTable.confirmationTime
+ var tanChannel by CashoutOperationsTable.tanChannel
+ var account by CashoutOperationsTable.account
+ var cashoutAddress by CashoutOperationsTable.cashoutAddress
+ var tan by CashoutOperationsTable.tan
+ var status by CashoutOperationsTable.status
+}
+object TalerWithdrawalsTable : LongIdTable() {
+ val wopid = uuid("wopid").autoGenerate()
+ val amount = text("amount") // $currency:x.y
+ /**
+ * Turns to true after the wallet gave the reserve public key
+ * and the exchange details to the bank.
+ */
+ val selectionDone = bool("selectionDone").default(false)
+ val aborted = bool("aborted").default(false)
+ /**
+ * Turns to true after the wire transfer to the exchange bank account
+ * gets completed _on the bank's side_. This does never guarantees that
+ * the payment arrived at the exchange's bank yet.
+ */
+ val confirmationDone = bool("confirmationDone").default(false)
+ val reservePub = text("reservePub").nullable()
+ val selectedExchangePayto = text("selectedExchangePayto").nullable()
+ val walletBankAccount = reference("walletBankAccount", BankAccountsTable)
+}
+class TalerWithdrawalEntity(id: EntityID<Long>) : LongEntity(id) {
+ companion object : LongEntityClass<TalerWithdrawalEntity>(TalerWithdrawalsTable)
+ var wopid by TalerWithdrawalsTable.wopid
+ var selectionDone by TalerWithdrawalsTable.selectionDone
+ var confirmationDone by TalerWithdrawalsTable.confirmationDone
+ var reservePub by TalerWithdrawalsTable.reservePub
+ var selectedExchangePayto by TalerWithdrawalsTable.selectedExchangePayto
+ var amount by TalerWithdrawalsTable.amount
+ var walletBankAccount by BankAccountEntity referencedOn TalerWithdrawalsTable.walletBankAccount
+ var aborted by TalerWithdrawalsTable.aborted
+}
+
+object BankAccountReportsTable : IntIdTable() {
+ val reportId = text("reportId")
+ val creationTime = long("creationTime")
+ val xmlMessage = text("xmlMessage")
+ val bankAccount = reference("bankAccount", BankAccountsTable)
+}
+
+/**
+ * This table tracks the cash-out requests that Sandbox sends to Nexus.
+ * Only successful requests make it to this table. Failed request would
+ * either _stop_ the conversion service (for client-side errors) or get retried
+ * at a later time (for server-side errors.)
+ */
+object CashoutSubmissionsTable: LongIdTable() {
+ val localTransaction = reference("localTransaction", BankAccountTransactionsTable).uniqueIndex()
+ val maybeNexusResponse = text("maybeNexusResponse").nullable()
+ val submissionTime = long("submissionTime").nullable() // failed don't have it.
+}
+
+class CashoutSubmissionEntity(id: EntityID<Long>) : LongEntity(id) {
+ companion object : LongEntityClass<CashoutSubmissionEntity>(CashoutSubmissionsTable)
+ var localTransaction by CashoutSubmissionsTable.localTransaction
+ var maybeNexusResposnse by CashoutSubmissionsTable.maybeNexusResponse
+ var submissionTime by CashoutSubmissionsTable.submissionTime
+}
+
+fun dbDropTables(connStringFromEnv: String) {
+ connectWithSchema(getJdbcConnectionFromPg(connStringFromEnv))
+ if (isPostgres()) {
+ val ret = execCommand(
+ listOf(
+ "libeufin-load-sql",
+ "-d",
+ connStringFromEnv,
+ "-s",
+ "sandbox",
+ "-r" // the drop option
+ ),
+ /**
+ * Tolerating a failure here helps to manage the case
+ * where an empty database is attempted to be dropped.
+ */
+ throwIfFails = false
+ )
+ if (ret != 0)
+ logger.warn("Dropping the sandbox tables failed. Was the DB filled before?")
+ return
+ }
+ transaction {
+ SchemaUtils.drop(
+ CashoutSubmissionsTable,
+ EbicsSubscribersTable,
+ EbicsSubscriberPublicKeysTable,
+ EbicsHostsTable,
+ EbicsDownloadTransactionsTable,
+ EbicsUploadTransactionsTable,
+ EbicsUploadTransactionChunksTable,
+ EbicsOrderSignaturesTable,
+ BankAccountTransactionsTable,
+ BankAccountFreshTransactionsTable,
+ BankAccountsTable,
+ BankAccountReportsTable,
+ BankAccountStatementsTable,
+ DemobankConfigsTable,
+ DemobankConfigPairsTable,
+ TalerWithdrawalsTable,
+ DemobankCustomersTable,
+ CashoutOperationsTable
+ )
+ }
+
+}
+
+fun dbCreateTables(connStringFromEnv: String) {
+ connectWithSchema(getJdbcConnectionFromPg(connStringFromEnv))
+ if (isPostgres()) {
+ execCommand(listOf(
+ "libeufin-load-sql",
+ "-d",
+ connStringFromEnv,
+ "-s",
+ "sandbox"
+ ))
+ return
+ }
+ // Still using the legacy way for other DBMSs, like SQLite.
+ transaction {
+ SchemaUtils.create(
+ CashoutSubmissionsTable,
+ DemobankConfigsTable,
+ DemobankConfigPairsTable,
+ EbicsSubscribersTable,
+ EbicsSubscriberPublicKeysTable,
+ EbicsHostsTable,
+ EbicsDownloadTransactionsTable,
+ EbicsUploadTransactionsTable,
+ EbicsUploadTransactionChunksTable,
+ EbicsOrderSignaturesTable,
+ BankAccountTransactionsTable,
+ BankAccountFreshTransactionsTable,
+ BankAccountsTable,
+ BankAccountReportsTable,
+ BankAccountStatementsTable,
+ TalerWithdrawalsTable,
+ DemobankCustomersTable,
+ CashoutOperationsTable
+ )
+ }
+}