gnunet-svn
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[libeufin] 01/06: Remove notion of "bank customer" from Sandbox.


From: gnunet
Subject: [libeufin] 01/06: Remove notion of "bank customer" from Sandbox.
Date: Wed, 29 Apr 2020 21:44:40 +0200

This is an automated email from the git hooks/post-receive script.

marcello pushed a commit to branch master
in repository libeufin.

commit 46d92a7f5212014266351aaf77e9f9b48e357e27
Author: Marcello Stanisci <address@hidden>
AuthorDate: Wed Apr 29 16:14:28 2020 +0200

    Remove notion of "bank customer" from Sandbox.
---
 integration-tests/test.py                          |  61 ------
 nexus/src/main/kotlin/tech/libeufin/nexus/DB.kt    |   7 +-
 nexus/src/main/kotlin/tech/libeufin/nexus/JSON.kt  |  22 +-
 nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt  |  35 +++-
 nexus/src/main/kotlin/tech/libeufin/nexus/taler.kt |   4 +-
 .../src/main/kotlin/tech/libeufin/sandbox/DB.kt    |  99 ++-------
 .../tech/libeufin/sandbox/EbicsProtocolBackend.kt  |  48 +++--
 .../src/main/kotlin/tech/libeufin/sandbox/JSON.kt  | 128 ++----------
 .../src/main/kotlin/tech/libeufin/sandbox/Main.kt  | 230 +++++----------------
 sandbox/src/test/kotlin/CamtGeneration.kt          |  45 ----
 sandbox/src/test/kotlin/DbTest.kt                  | 137 ------------
 util/src/main/kotlin/ebics_h004/EbicsResponse.kt   |   1 -
 12 files changed, 163 insertions(+), 654 deletions(-)

diff --git a/integration-tests/test.py b/integration-tests/test.py
deleted file mode 100755
index 2bb4623..0000000
--- a/integration-tests/test.py
+++ /dev/null
@@ -1,61 +0,0 @@
-#!/usr/bin/env python3
-
-from requests import post, get
-
-#1 Create a Nexus user
-USERNAME="person"
-
-resp = post(
-    "http://localhost:5001/users/{}".format(USERNAME),
-    json=dict(
-       password="secret"
-    )
-)
-
-assert(resp.status_code == 200)
-
-#2 Create a EBICS user
-
-resp = post(
-    "http://localhost:5001/ebics/subscribers/{}".format(USERNAME),
-    json=dict(
-       ebicsURL="http://localhost:5000/ebicsweb";,
-       hostID="HOST01",
-       partnerID="PARTNER1",
-       userID="USER1"
-    )
-)
-
-assert(resp.status_code == 200)
-
-#3 Upload keys to the bank INI & HIA
-resp = post(
-    "http://localhost:5001/ebics/subscribers/{}/sendINI".format(USERNAME),
-    json=dict()
-)
-
-assert(resp.status_code == 200)
-
-resp = post(
-    "http://localhost:5001/ebics/subscribers/{}/sendHIA".format(USERNAME),
-    json=dict()
-)
-
-assert(resp.status_code == 200)
-
-#4 Download keys from the bank HPB
-resp = post(
-    "http://localhost:5001/ebics/subscribers/{}/sync".format(USERNAME),
-    json=dict()
-)
-
-assert(resp.status_code == 200)
-
-#5 Request history
-#6 Prepare a payment
-#7 Execute such payment via EBICS
-#8 Request history again
-#9 Exit
-
-# The Nexus should connect to the Sandbox for
-# these tests!
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/DB.kt 
b/nexus/src/main/kotlin/tech/libeufin/nexus/DB.kt
index 8eafd3a..c850ea4 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/DB.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/DB.kt
@@ -5,9 +5,6 @@ import org.jetbrains.exposed.dao.*
 import org.jetbrains.exposed.sql.*
 import org.jetbrains.exposed.sql.transactions.TransactionManager
 import org.jetbrains.exposed.sql.transactions.transaction
-import tech.libeufin.nexus.EbicsSubscribersTable.entityId
-import tech.libeufin.nexus.EbicsSubscribersTable.nullable
-import tech.libeufin.nexus.EbicsSubscribersTable.primaryKey
 import tech.libeufin.util.IntIdTableWithAmount
 import java.sql.Connection
 
@@ -86,7 +83,7 @@ class TalerIncomingPaymentEntity(id: EntityID<Long>) : 
LongEntity(id) {
  * CAMT message.
  */
 object RawBankTransactionsTable : LongIdTable() {
-    val nexusSubscriber = reference("subscriber", EbicsSubscribersTable)
+    val nexusUser = reference("nexusUser", NexusUsersTable)
     val sourceFileName = text("sourceFileName") /* ZIP entry's name */
     val unstructuredRemittanceInformation = 
text("unstructuredRemittanceInformation")
     val transactionType = text("transactionType") /* DBIT or CRDT */
@@ -114,7 +111,7 @@ class RawBankTransactionEntity(id: EntityID<Long>) : 
LongEntity(id) {
     var creditorIban by RawBankTransactionsTable.creditorIban
     var counterpartBic by RawBankTransactionsTable.counterpartBic
     var bookingDate by RawBankTransactionsTable.bookingDate
-    var nexusSubscriber by EbicsSubscriberEntity referencedOn 
RawBankTransactionsTable.nexusSubscriber
+    var nexusUser by NexusUserEntity referencedOn 
RawBankTransactionsTable.nexusUser
     var status by RawBankTransactionsTable.status
 }
 
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/JSON.kt 
b/nexus/src/main/kotlin/tech/libeufin/nexus/JSON.kt
index 7336045..188aa2d 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/JSON.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/JSON.kt
@@ -138,8 +138,11 @@ class TestSubscriber()
 /** PAYMENT INSTRUCTIONS TYPES */
 
 /** Represents a prepared payment at the nexus.  This structure is
- * used to SHOW a prepared payment to the called.  */
+ * used to SHOW a prepared payment to the caller.  */
 data class PaymentInfoElement(
+    /**
+     * This field is the mnemonic value that bank accounts usually have.
+     */
     val debtorAccount: String,
     val creditorIban: String,
     val creditorBic: String,
@@ -160,4 +163,21 @@ data class Pain001Data(
     val sum: Amount,
     val currency: String = "EUR",
     val subject: String
+)
+
+/**
+ * This type is more of a debug one, and it shows details
+ * about one payment that was accounted by the bank (typically
+ * in a CAMT.05x entry)
+ */
+data class RawPayment(
+    val creditorIban: String,
+    val debitorIban: String,
+    val amount: String,
+    val subject: String,
+    val date: String
+)
+
+data class RawPayments(
+    var payments: MutableList<RawPayment> = mutableListOf()
 )
\ No newline at end of file
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt 
b/nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt
index a5dc64a..1b87c27 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt
@@ -45,6 +45,7 @@ import kotlinx.coroutines.io.jvm.javaio.toInputStream
 import kotlinx.io.core.ExperimentalIoApi
 import org.jetbrains.exposed.sql.and
 import org.jetbrains.exposed.sql.transactions.transaction
+import org.joda.time.DateTime
 import org.slf4j.Logger
 import org.slf4j.LoggerFactory
 import org.slf4j.event.Level
@@ -285,6 +286,31 @@ fun main() {
                 )
                 return@post
             }
+            get("/users/{id}/raw-payments") {
+                val nexusUser = extractNexusUser(call.parameters["id"])
+                var ret = RawPayments()
+                transaction {
+                    RawBankTransactionEntity.find {
+                        RawBankTransactionsTable.nexusUser eq 
nexusUser.id.value
+                    }.forEach {
+                        ret.payments.add(
+                            RawPayment(
+                                creditorIban = it.creditorIban,
+                                debitorIban = it.debitorIban,
+                                subject = it.unstructuredRemittanceInformation,
+                                date = DateTime(it.bookingDate).toDashedDate(),
+                                amount = it.amount + " " + it.currency
+                            )
+                        )
+                    }
+                }
+                call.respond(
+                    HttpStatusCode.OK,
+                    ret
+                )
+                return@get
+            }
+
             /** Associate a EBICS subscriber to the existing user */
             post("/ebics/subscribers/{id}") {
                 val nexusUser = extractNexusUser(call.parameters["id"])
@@ -587,10 +613,6 @@ fun main() {
                 )
                 return@post
             }
-            post("/ebics/subscribers/{id}/collect-transactions-c52") {
-                // FIXME(florian): Download C52 and store the result in the 
right database table
-
-            }
             /** exports keys backup copy */
             post("/ebics/subscribers/{id}/backup") {
                 val body = call.receive<EbicsBackupRequestJson>()
@@ -628,7 +650,6 @@ fun main() {
                     response
                 )
             }
-
             /** Download keys from bank */
             post("/ebics/subscribers/{id}/sync") {
                 val nexusUser = extractNexusUser(call.parameters["id"])
@@ -696,13 +717,13 @@ fun main() {
                             transaction {
                                 RawBankTransactionEntity.new {
                                     sourceFileName = fileName
-                                    unstructuredRemittanceInformation = 
camt53doc.pickString("//*[local-name()='Ntry']//*[local-name()='Amt']/@Ccy")
+                                    unstructuredRemittanceInformation = 
camt53doc.pickString("//*[local-name()='Ntry']//*[local-name()='Ustrd']")
                                     transactionType = 
camt53doc.pickString("//*[local-name()='Ntry']//*[local-name()='CdtDbtInd']")
                                     currency = 
camt53doc.pickString("//*[local-name()='Ntry']//*[local-name()='Amt']/@Ccy")
                                     amount = 
camt53doc.pickString("//*[local-name()='Ntry']//*[local-name()='Amt']")
                                     status = 
camt53doc.pickString("//*[local-name()='Ntry']//*[local-name()='Sts']")
                                     bookingDate = 
parseDate(camt53doc.pickString("//*[local-name()='BookgDt']//*[local-name()='Dt']")).millis
-                                    nexusSubscriber = 
getSubscriberEntityFromNexusUserId(id)
+                                    nexusUser = extractNexusUser(id)
                                     creditorName = 
camt53doc.pickString("//*[local-name()='RltdPties']//*[local-name()='Dbtr']//*[local-name()='Nm']")
                                     creditorIban = 
camt53doc.pickString("//*[local-name()='CdtrAcct']//*[local-name()='IBAN']")
                                     debitorName = 
camt53doc.pickString("//*[local-name()='RltdPties']//*[local-name()='Dbtr']//*[local-name()='Nm']")
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/taler.kt 
b/nexus/src/main/kotlin/tech/libeufin/nexus/taler.kt
index 6e4b110..db8ad63 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/taler.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/taler.kt
@@ -282,7 +282,7 @@ class Taler(app: Route) {
                         creditorIban = creditorObj.iban
                         counterpartBic = creditorObj.bic
                         bookingDate = DateTime.now().millis
-                        nexusSubscriber = getEbicsSubscriberFromUser(nexusUser)
+                        this.nexusUser = nexusUser
                         status = "BOOK"
                     }
                 } else null
@@ -338,7 +338,7 @@ class Taler(app: Route) {
                     counterpartBic = debtor.bic
                     bookingDate = DateTime.now().millis
                     status = "BOOK"
-                    nexusSubscriber = 
getSubscriberEntityFromNexusUserId(exchangeId)
+                    nexusUser = extractNexusUser(exchangeId)
                 }
                 /** This payment is "valid by default" and will be returned
                  * as soon as the exchange will ask for new payments.  */
diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/DB.kt 
b/sandbox/src/main/kotlin/tech/libeufin/sandbox/DB.kt
index b9d872b..d91f1b2 100644
--- a/sandbox/src/main/kotlin/tech/libeufin/sandbox/DB.kt
+++ b/sandbox/src/main/kotlin/tech/libeufin/sandbox/DB.kt
@@ -19,27 +19,12 @@
 
 package tech.libeufin.sandbox
 
-import io.ktor.http.HttpStatusCode
 import org.jetbrains.exposed.dao.*
 import org.jetbrains.exposed.sql.*
 import org.jetbrains.exposed.sql.transactions.TransactionManager
 import org.jetbrains.exposed.sql.transactions.transaction
-import tech.libeufin.util.IntIdTableWithAmount
-import java.lang.ArithmeticException
-import java.math.BigDecimal
-import java.math.MathContext
-import java.math.RoundingMode
-import java.sql.Blob
 import java.sql.Connection
 
-const val CUSTOMER_NAME_MAX_LENGTH = 20
-const val EBICS_HOST_ID_MAX_LENGTH = 10
-const val EBICS_USER_ID_MAX_LENGTH = 10
-const val EBICS_PARTNER_ID_MAX_LENGTH = 10
-const val EBICS_SYSTEM_ID_MAX_LENGTH = 10
-const val MAX_ID_LENGTH = 21 // enough to contain IBANs
-const val MAX_SUBJECT_LENGTH = 140 // okay?
-
 /**
  * All the states to give a subscriber.
  */
@@ -93,48 +78,6 @@ enum class KeyState {
     RELEASED
 }
 
-
-object BankTransactionsTable : IntIdTableWithAmount() {
-    /* Using varchar to store the IBAN - or possibly other formats
-     * - from the counterpart.  */
-    val counterpart = varchar("counterpart", MAX_ID_LENGTH)
-    val amount = amount("amount")
-    val subject = varchar("subject", MAX_SUBJECT_LENGTH)
-    val operationDate = long("operationDate")
-    val valueDate = long("valueDate")
-    val localCustomer = reference("localCustomer", BankCustomersTable)
-}
-
-class BankTransactionEntity(id: EntityID<Int>) : IntEntity(id) {
-    companion object : 
IntEntityClass<BankTransactionEntity>(BankTransactionsTable)
-    /* the id of the local customer involved in this transaction,
-    * either as the credit or the debit part; makes lookups easier */
-    var localCustomer by BankCustomerEntity referencedOn 
BankTransactionsTable.localCustomer
-    /* keeping as strings, as to allow hosting IBANs and/or other
-    * unobvious formats.  */
-    var counterpart by BankTransactionsTable.counterpart
-    var subject by BankTransactionsTable.subject
-    var operationDate by BankTransactionsTable.operationDate
-    var valueDate by BankTransactionsTable.valueDate
-    var amount by BankTransactionsTable.amount
-}
-
-
-/**
- * This table information *not* related to EBICS, for all
- * its customers.
- */
-object BankCustomersTable : IntIdTable() {
-    // Customer ID is the default 'id' field provided by the constructor.
-    val customerName = varchar("customerName", CUSTOMER_NAME_MAX_LENGTH)
-}
-
-class BankCustomerEntity(id: EntityID<Int>) : IntEntity(id) {
-    companion object : IntEntityClass<BankCustomerEntity>(BankCustomersTable)
-    var customerName by BankCustomersTable.customerName
-}
-
-
 /**
  * This table stores RSA public keys of subscribers.
  */
@@ -142,10 +85,6 @@ object EbicsSubscriberPublicKeysTable : IntIdTable() {
     val rsaPublicKey = blob("rsaPublicKey")
     val state = enumeration("state", KeyState::class)
 }
-
-/**
- * Definition of a row in the [EbicsSubscriberPublicKeyEntity] table
- */
 class EbicsSubscriberPublicKeyEntity(id: EntityID<Int>) : IntEntity(id) {
     companion object : 
IntEntityClass<EbicsSubscriberPublicKeyEntity>(EbicsSubscriberPublicKeysTable)
 
@@ -153,7 +92,9 @@ class EbicsSubscriberPublicKeyEntity(id: EntityID<Int>) : 
IntEntity(id) {
     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")
@@ -161,11 +102,8 @@ object EbicsHostsTable : IntIdTable() {
     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
@@ -174,8 +112,7 @@ class EbicsHostEntity(id: EntityID<Int>) : IntEntity(id) {
 }
 
 /**
- * Subscribers table.  This table associates users with partners
- * and systems.  Each value can appear multiple times in the same column.
+ * Ebics Subscribers table.
  */
 object EbicsSubscribersTable : IntIdTable() {
     val userId = text("userID")
@@ -187,28 +124,23 @@ object EbicsSubscribersTable : IntIdTable() {
     val authenticationKey = reference("authorizationKey", 
EbicsSubscriberPublicKeysTable).nullable()
     val nextOrderID = integer("nextOrderID")
     val state = enumeration("state", SubscriberState::class)
-    val bankCustomer = reference("bankCustomer", BankCustomersTable)
 }
-
-
 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 bankCustomer by BankCustomerEntity referencedOn 
EbicsSubscribersTable.bankCustomer
 }
 
-
+/**
+ * Details of a download order.
+ */
 object EbicsDownloadTransactionsTable : IdTable<String>() {
     override val id = text("transactionID").entityId()
     val orderType = text("orderType")
@@ -220,7 +152,6 @@ object EbicsDownloadTransactionsTable : IdTable<String>() {
     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
@@ -233,6 +164,9 @@ class EbicsDownloadTransactionEntity(id: EntityID<String>) 
: Entity<String>(id)
     var receiptReceived by EbicsDownloadTransactionsTable.receiptReceived
 }
 
+/**
+ * Details of a upload order.
+ */
 object EbicsUploadTransactionsTable : IdTable<String>() {
     override val id = text("transactionID").entityId()
     val orderType = text("orderType")
@@ -243,10 +177,8 @@ object EbicsUploadTransactionsTable : IdTable<String>() {
     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
@@ -256,6 +188,9 @@ class EbicsUploadTransactionEntity(id: EntityID<String>) : 
Entity<String>(id) {
     var transactionKeyEnc by EbicsUploadTransactionsTable.transactionKeyEnc
 }
 
+/**
+ * FIXME: document this.
+ */
 object EbicsOrderSignaturesTable : IntIdTable() {
     val orderID = text("orderID")
     val orderType = text("orderType")
@@ -264,7 +199,6 @@ object EbicsOrderSignaturesTable : IntIdTable() {
     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
@@ -275,13 +209,15 @@ class EbicsOrderSignatureEntity(id: EntityID<Int>) : 
IntEntity(id) {
     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")
 }
-
 class EbicsUploadTransactionChunkEntity(id : EntityID<String>): 
Entity<String>(id) {
     companion object : EntityClass<String, 
EbicsUploadTransactionChunkEntity>(EbicsUploadTransactionChunksTable)
     var chunkIndex by EbicsUploadTransactionChunksTable.chunkIndex
@@ -292,12 +228,9 @@ class EbicsUploadTransactionChunkEntity(id : 
EntityID<String>): Entity<String>(i
 fun dbCreateTables() {
     Database.connect("jdbc:sqlite:libeufin-sandbox.sqlite3", "org.sqlite.JDBC")
     TransactionManager.manager.defaultIsolationLevel = 
Connection.TRANSACTION_SERIALIZABLE
-
     transaction {
         addLogger(StdOutSqlLogger)
         SchemaUtils.create(
-            BankCustomersTable,
-            BankTransactionsTable,
             EbicsSubscribersTable,
             EbicsHostsTable,
             EbicsDownloadTransactionsTable,
diff --git 
a/sandbox/src/main/kotlin/tech/libeufin/sandbox/EbicsProtocolBackend.kt 
b/sandbox/src/main/kotlin/tech/libeufin/sandbox/EbicsProtocolBackend.kt
index 0113413..b7d309c 100644
--- a/sandbox/src/main/kotlin/tech/libeufin/sandbox/EbicsProtocolBackend.kt
+++ b/sandbox/src/main/kotlin/tech/libeufin/sandbox/EbicsProtocolBackend.kt
@@ -140,7 +140,7 @@ private suspend fun 
ApplicationCall.respondEbicsKeyManagement(
     respondText(text, ContentType.Application.Xml, HttpStatusCode.OK)
 }
 
-fun buildCamtString(history: SizedIterable<BankTransactionEntity>, type: Int): 
String {
+fun buildCamtString(type: Int): String {
 
     /**
      * Booking period: we keep two "lines" of booking periods; one for c52 and 
one for c53.
@@ -409,22 +409,14 @@ fun buildCamtString(history: 
SizedIterable<BankTransactionEntity>, type: Int): S
  * @param history the list of all the history elements
  * @param type 52 or 53.
  */
-private fun constructCamtResponse(type: Int, customerId: Int, header: 
EbicsRequest.Header): String {
-
+private fun constructCamtResponse(type: Int, header: EbicsRequest.Header): 
String {
     val dateRange = (header.static.orderDetails?.orderParams as 
EbicsRequest.StandardOrderParams).dateRange
     val (start: org.joda.time.DateTime, end: org.joda.time.DateTime) = if 
(dateRange != null) {
         Pair(DateTime(dateRange.start.toGregorianCalendar().time), 
DateTime(dateRange.end.toGregorianCalendar().time))
     } else Pair(DateTime(0), DateTime.now())
-    val history = extractHistory(
-        customerId,
-        start,
-        end
-    )
-    logger.debug("${history.count()} history elements found for account 
$customerId")
-    return buildCamtString(history, type)
+    return buildCamtString(type)
 }
 
-
 private fun handleEbicsTSD(requestContext: RequestContext): ByteArray {
     return "Hello World".toByteArray()
 }
@@ -433,11 +425,36 @@ private fun handleEbicsPTK(requestContext: 
RequestContext): ByteArray {
     return "Hello I am a dummy PTK response.".toByteArray()
 }
 
+/**
+ * Process a payment request in the pain.001 format.
+ */
+private fun handleCct(paymentRequest: String, ebicsSubscriberEntity: 
EbicsSubscriberEntity) {
+    /**
+     * NOTE: this function is ONLY required to store some details
+     * to put then in the camt report.  IBANs / amount / subject / names?
+     */
+    val painDoc = XMLUtil.parseStringIntoDom(paymentRequest)
+    val creditorIban = 
painDoc.pickString("//*[local-name()='CdtrAcct']//*[local-name()='IBAN']")
+    val debitorIban = 
painDoc.pickString("//*[local-name()='DbtrAcct']//*[local-name()='IBAN']")
+    val subject = painDoc.pickString("//*[local-name()='Ustrd']")
+    val currency = painDoc.pickString("//*[local-name()='InstdAmt']/@ccy")
+    val amount = painDoc.pickString("//*[local-name()='InstdAmt']")
+
+    /*
+    transaction {
+        PaymentEntity.new {
+            this.creditorIban = creditorIban
+            this.debitorIban = debitorIban
+            this.subject = subject
+            this.amount = "${currency}:${amount}"
+        }
+    }*/
+}
+
 private fun handleEbicsC52(requestContext: RequestContext): ByteArray {
     val subscriber = requestContext.subscriber
     val camt = constructCamtResponse(
         52,
-        subscriber.bankCustomer.id.value,
         requestContext.requestObject.header
     )
     return camt.toByteArray().zip()
@@ -447,7 +464,6 @@ private fun handleEbicsC53(requestContext: RequestContext): 
ByteArray {
     val subscriber = requestContext.subscriber
     val camt = constructCamtResponse(
         53,
-        subscriber.bankCustomer.id.value,
         requestContext.requestObject.header
     )
     return camt.toByteArray().zip()
@@ -612,7 +628,6 @@ private suspend fun ApplicationCall.receiveEbicsXml(): 
Document {
     return requestDocument
 }
 
-
 fun handleEbicsHtd(): ByteArray {
     val htd = HTDResponseOrderData().apply {
         this.partnerInfo = EbicsTypes.PartnerInfo().apply {
@@ -909,7 +924,6 @@ private fun 
handleEbicsUploadTransactionInitialization(requestContext: RequestCo
     return EbicsResponse.createForUploadInitializationPhase(transactionID, 
orderID)
 }
 
-
 private fun handleEbicsUploadTransactionTransmission(requestContext: 
RequestContext): EbicsResponse {
     val uploadTransaction = requestContext.uploadTransaction ?: throw 
EbicsInvalidRequestError()
     val requestObject = requestContext.requestObject
@@ -952,6 +966,10 @@ private fun 
handleEbicsUploadTransactionTransmission(requestContext: RequestCont
             }
         }
 
+        /** Handling a payment request */
+        if ("CCT" == 
requestContext.requestObject.header.static.orderDetails?.orderType) {
+            handleCct(unzippedData.toString(Charsets.UTF_8), 
requestContext.subscriber)
+        }
         return EbicsResponse.createForUploadTransferPhase(
             requestTransactionID,
             requestSegmentNumber,
diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/JSON.kt 
b/sandbox/src/main/kotlin/tech/libeufin/sandbox/JSON.kt
index 997cf57..1440afc 100644
--- a/sandbox/src/main/kotlin/tech/libeufin/sandbox/JSON.kt
+++ b/sandbox/src/main/kotlin/tech/libeufin/sandbox/JSON.kt
@@ -19,122 +19,23 @@
 
 package tech.libeufin.sandbox
 
-/**
- * Error message.
- */
+/** Error message */
 data class SandboxError(
     val message: String
 )
 
 /**
- * Request for POST /admin/customers
- */
-data class CustomerRequest(
-    val name: String
-)
-
-data class CustomerResponse(
-    val id: Int
-)
-
-data class CustomerBalance(
-    val name: String,
-    val balance: String
-)
-
-/**
- * Response for GET /admin/customers/:id
- */
-data class CustomerInfo(
-    val name: String,
-    val ebicsInfo: CustomerEbicsInfo
-)
-
-data class CustomerEbicsInfo(
-    val userId: String
-)
-
-data class CustomerHistoryRequest(
-    val start: String?,
-    val end: String?
-)
-
-data class CustomerHistoryResponseElement(
-    var amount: String,
-    val subject: String,
-    val counterpart: String,
-    val operationDate: String,
-    val valueDate: String
-)
-
-data class CustomerHistoryResponse(
-    var history: MutableList<CustomerHistoryResponseElement> = mutableListOf()
-)
-
-/**
- * Wrapper type around initialization letters
- * for RSA keys.
+ * Used to show the list of Ebics hosts that exist
+ * in the system.
  */
-data class IniHiaLetters(
-    val ini: IniLetter,
-    val hia: HiaLetter
-)
-
-/**
- * Request for INI letter.
- */
-data class IniLetter(
-    val userId: String,
-    val customerId: String,
-    val name: String,
-    val date: String,
-    val time: String,
-    val recipient: String,
-    val public_exponent_length: Int,
-    val public_exponent: String,
-    val public_modulus_length: Int,
-    val public_modulus: String,
-    val hash: String
-)
-
-/**
- * Request for HIA letter.
- */
-data class HiaLetter(
-    val userId: String,
-    val customerId: String,
-    val name: String,
-    val date: String,
-    val time: String,
-    val recipient: String,
-    val ia_public_exponent_length: Int,
-    val ia_public_exponent: String,
-    val ia_public_modulus_length: Int,
-    val ia_public_modulus: String,
-    val ia_hash: String,
-    val enc_public_exponent_length: Int,
-    val enc_public_exponent: String,
-    val enc_public_modulus_length: Int,
-    val enc_public_modulus: String,
-    val enc_hash: String
-)
-
-data class EbicsSubscribersResponse(
-    val subscribers: List<String>
-)
-
-data class EbicsSubscriberResponse(
-    val id: String,
-    val partnerID: String,
-    val userID: String,
-    val systemID: String?,
-    val state: String
-)
-
 data class EbicsHostsResponse(
     val ebicsHosts: List<String>
 )
 
+/**
+ * Used to show information about ONE particular
+ * Ebics host that is active in the system.
+ */
 data class EbicsHostResponse(
     val hostID: String,
     val ebicsVersion: String
@@ -145,21 +46,16 @@ data class EbicsHostCreateRequest(
     val ebicsVersion: String
 )
 
-data class AdminAddSubscriberRequest(
-    val name: String, // person's name
+/**
+ * Used to create AND show one Ebics subscriber in the system.
+ */
+data class EbicsSubscriberElement(
     val hostID: String,
     val partnerID: String,
     val userID: String,
     val systemID: String? = null
 )
 
-data class AdminSubscriberElement(
-    var name: String,
-    var userId: String,
-    var partnerID: String,
-    var hostID: String
-)
-
 data class AdminGetSubscribers(
-    var subscribers: MutableList<AdminSubscriberElement> = mutableListOf()
+    var subscribers: MutableList<EbicsSubscriberElement> = mutableListOf()
 )
diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/Main.kt 
b/sandbox/src/main/kotlin/tech/libeufin/sandbox/Main.kt
index 2af9c6a..23fec93 100644
--- a/sandbox/src/main/kotlin/tech/libeufin/sandbox/Main.kt
+++ b/sandbox/src/main/kotlin/tech/libeufin/sandbox/Main.kt
@@ -60,20 +60,6 @@ class UnacceptableFractional(badNumber: BigDecimal) : 
Exception(
     "Unacceptable fractional part ${badNumber}"
 )
 val LOGGER: Logger = LoggerFactory.getLogger("tech.libeufin.sandbox")
-fun findCustomer(id: String?): BankCustomerEntity {
-
-    val idN = try {
-        id!!.toInt()
-    } catch (e: Exception) {
-        e.printStackTrace()
-        throw BadInputData(id)
-    }
-
-    return transaction {
-        addLogger(StdOutSqlLogger)
-        BankCustomerEntity.findById(idN) ?: throw CustomerNotFound(id)
-    }
-}
 
 fun findEbicsSubscriber(partnerID: String, userID: String, systemID: String?): 
EbicsSubscriberEntity? {
     return if (systemID == null) {
@@ -120,82 +106,8 @@ fun BigDecimal.signToString(): String {
     // minus sign is added by default already.
 }
 
-fun sampleData() {
-    transaction {
-        val pairA = CryptoUtil.generateRsaKeyPair(2048)
-        val pairB = CryptoUtil.generateRsaKeyPair(2048)
-        val pairC = CryptoUtil.generateRsaKeyPair(2048)
-        EbicsHostEntity.new {
-            hostId = "host01"
-            ebicsVersion = "H004"
-            authenticationPrivateKey = SerialBlob(pairA.private.encoded)
-            encryptionPrivateKey = SerialBlob(pairB.private.encoded)
-            signaturePrivateKey = SerialBlob(pairC.private.encoded)
-        }
-        val customerEntity = BankCustomerEntity.new {
-            addLogger(StdOutSqlLogger)
-            customerName = "Mina"
-        }
-        LOGGER.debug("Creating customer number: ${customerEntity.id}")
-        EbicsSubscriberEntity.new {
-            partnerId = "PARTNER1"
-            userId = "USER1"
-            systemId = null
-            hostId = "HOST01"
-            state = SubscriberState.NEW
-            nextOrderID = 1
-            bankCustomer = customerEntity
-        }
-        for (i in listOf(Amount("-0.44"), Amount("6.02"))) {
-            BankTransactionEntity.new {
-                counterpart = "IBAN"
-                amount = i
-                subject = "transaction $i"
-                operationDate = DateTime.now().millis
-                valueDate = DateTime.now().millis
-                localCustomer = customerEntity
-            }
-        }
-    }
-}
-
-/**
- * @param id the customer whose history must be returned.  This
- * id is local to the bank and is not reused/encoded into other
- * EBICS id values.
- *
- * @return result set of all the operations related to the customer
- * identified by @p id.
- */
-fun extractHistory(id: Int, start: DateTime, end: DateTime): 
SizedIterable<BankTransactionEntity> {
-    LOGGER.debug("Fetching history from ${start.toLocalDateTime()} to 
${end.toLocalDateTime()}")
-    return transaction {
-        addLogger(StdOutSqlLogger)
-        BankTransactionEntity.find {
-            BankTransactionsTable.localCustomer eq id and 
BankTransactionsTable.valueDate.between(start.millis, end.millis)
-        }
-    }
-}
-
-fun calculateBalance(id: Int, start: String?, end: String?): BigDecimal {
-    val s = if (start != null) DateTime.parse(start) else DateTime(0)
-    val e = if (end != null) DateTime.parse(end) else DateTime.now()
-
-    var ret = BigDecimal(0)
-
-    transaction {
-        BankTransactionEntity.find {
-            BankTransactionsTable.localCustomer eq id and 
BankTransactionsTable.operationDate.between(s.millis, e.millis)
-        }.forEach { ret += it.amount }
-    }
-    return ret
-}
-
 fun main() {
-
     dbCreateTables()
-    sampleData()
-
     val server = embeddedServer(Netty, port = 5000) {
         install(CallLogging) {
             this.level = Level.DEBUG
@@ -225,87 +137,65 @@ fun main() {
             }
         }
         routing {
-            post("/{id}/history") {
-                val req = call.receive<CustomerHistoryRequest>()
-                val customer = findCustomer(call.parameters["id"])
-                val ret = CustomerHistoryResponse()
-                val history = extractHistory(
-                    customer.id.value,
-                    DateTime.parse(req.start ?: "1970-01-01"),
-                    DateTime.parse(req.end ?: "3000-01-01")
-                )
+            get("/") {
+                call.respondText("Hello Sandbox!\n", ContentType.Text.Plain)
+            }
 
+            /** EBICS ADMIN ENDPOINTS */
+
+            post("/admin/ebics-subscriber") {
+                val body = call.receive<EbicsSubscriberElement>()
                 transaction {
-                    history.forEach {
-                        ret.history.add(
-                            CustomerHistoryResponseElement(
-                                subject = it.subject,
-                                amount = 
"${it.amount.signToString()}${it.amount} EUR",
-                                counterpart = it.counterpart,
-                                operationDate = 
DateTime(it.operationDate).toString("Y-M-d"),
-                                valueDate = 
DateTime(it.valueDate).toString("Y-M-d")
-                            )
-                        )
+                    EbicsSubscriberEntity.new {
+                        partnerId = body.partnerID
+                        userId = body.userID
+                        systemId = null
+                        hostId = body.hostID
+                        state = SubscriberState.NEW
+                        nextOrderID = 1
                     }
                 }
-                call.respond(ret)
-                return@post
-            }
-            get("/{id}/balance") {
-                val customer = findCustomer(call.parameters["id"])
-                val balance = calculateBalance(customer.id.value, null, null)
-                call.respond(
-                    CustomerBalance(
-                    name = customer.customerName,
-                    balance = "${balance} EUR"
-                    )
+                call.respondText(
+                    "Subscriber created.",
+                    ContentType.Text.Plain, HttpStatusCode.OK
                 )
-                return@get
+                return@post
             }
-            get("/admin/subscribers") {
+            get("/admin/ebics-subscribers") {
                 var ret = AdminGetSubscribers()
                 transaction {
                     EbicsSubscriberEntity.all().forEach {
                         ret.subscribers.add(
-                            AdminSubscriberElement(
-                            userId = it.userId, partnerID = it.partnerId, 
hostID = it.hostId, name = it.bankCustomer.customerName))
+                            EbicsSubscriberElement(
+                                userID = it.userId,
+                                partnerID = it.partnerId,
+                                hostID = it.hostId
+                            )
+                        )
                     }
                 }
                 call.respond(ret)
                 return@get
             }
-            post("/admin/add/subscriber") {
-                val body = call.receive<AdminAddSubscriberRequest>()
 
-                transaction {
-                    val customerEntity = BankCustomerEntity.new {
-                        addLogger(StdOutSqlLogger)
-                        customerName = body.name
-                    }
-                    EbicsSubscriberEntity.new {
-                        partnerId = body.partnerID
-                        userId = body.userID
-                        systemId = null
-                        hostId = body.hostID
-                        state = SubscriberState.NEW
-                        nextOrderID = 1
-                        bankCustomer = customerEntity
-                    }
+            /* Show details about ONE Ebics host */
+            get("/ebics/hosts/{id}") {
+                val resp = transaction {
+                    val host = EbicsHostEntity.find { EbicsHostsTable.hostID 
eq call.parameters["id"]!! }.firstOrNull()
+                    if (host == null) null
+                    else EbicsHostResponse(
+                        host.hostId,
+                        host.ebicsVersion
+                    )
                 }
-
-                call.respondText("Subscriber created.", 
ContentType.Text.Plain, HttpStatusCode.OK)
-                return@post
+                if (resp == null) call.respond(
+                    HttpStatusCode.NotFound,
+                    SandboxError("host not found")
+                )
+                else call.respond(resp)
             }
 
-            get("/") {
-                call.respondText("Hello LibEuFin!\n", ContentType.Text.Plain)
-            }
-            get("/ebics/hosts") {
-                val ebicsHosts = transaction {
-                    EbicsHostEntity.all().map { it.hostId }
-                }
-                call.respond(EbicsHostsResponse(ebicsHosts))
-            }
+            /** Create a new EBICS host. */
             post("/ebics/hosts") {
                 val req = call.receive<EbicsHostCreateRequest>()
                 val pairA = CryptoUtil.generateRsaKeyPair(2048)
@@ -323,47 +213,25 @@ fun main() {
                     }
                 }
                 call.respondText(
-                    "Host created.",
+                    "Host '${req.hostId}' created.",
                     ContentType.Text.Plain,
                     HttpStatusCode.OK
                 )
                 return@post
             }
-            get("/ebics/hosts/{id}") {
-                val resp = transaction {
-                    val host = EbicsHostEntity.find { EbicsHostsTable.hostID 
eq call.parameters["id"]!! }.firstOrNull()
-                    if (host == null) null
-                    else EbicsHostResponse(host.hostId, host.ebicsVersion)
-                }
-                if (resp == null) call.respond(
-                    HttpStatusCode.NotFound,
-                    SandboxError("host not found")
-                )
-                else call.respond(resp)
-            }
-            get("/ebics/subscribers") {
-                val subscribers = transaction {
-                    EbicsSubscriberEntity.all().map { it.id.value.toString() }
-                }
-                call.respond(EbicsSubscribersResponse(subscribers))
-            }
-            get("/ebics/subscribers/{id}") {
-                val resp = transaction {
-                    val id = call.parameters["id"]!!
-                    val subscriber = 
EbicsSubscriberEntity.findById(id.toInt())!!
-                    EbicsSubscriberResponse(
-                        id,
-                        subscriber.partnerId,
-                        subscriber.userId,
-                        subscriber.systemId,
-                        subscriber.state.name
-                    )
+            /* Show ONLY names of all the Ebics hosts */
+            get("/ebics/hosts") {
+                val ebicsHosts = transaction {
+                    EbicsHostEntity.all().map { it.hostId }
                 }
-                call.respond(resp)
+                call.respond(EbicsHostsResponse(ebicsHosts))
             }
+
+            /** MAIN EBICS handler. */
             post("/ebicsweb") {
                 call.ebicsweb()
             }
+
         }
     }
     LOGGER.info("Up and running")
diff --git a/sandbox/src/test/kotlin/CamtGeneration.kt 
b/sandbox/src/test/kotlin/CamtGeneration.kt
deleted file mode 100644
index 2b789ab..0000000
--- a/sandbox/src/test/kotlin/CamtGeneration.kt
+++ /dev/null
@@ -1,45 +0,0 @@
-package tech.libeufin.sandbox
-
-import org.jetbrains.exposed.sql.transactions.transaction
-import org.joda.time.DateTime
-import org.junit.Test
-import org.junit.Before
-import tech.libeufin.util.Amount
-import org.jetbrains.exposed.sql.SchemaUtils
-import org.jetbrains.exposed.sql.Database
-
-class CamtGeneration {
-
-    @Before
-    fun connectAndMakeTables() {
-        Database.connect("jdbc:h2:mem:test;DB_CLOSE_DELAY=-1", driver = 
"org.h2.Driver")
-        transaction {
-            SchemaUtils.create(BankCustomersTable)
-            SchemaUtils.create(BankTransactionsTable)
-        }
-    }
-
-    @Test
-    fun generateCamt() {
-        transaction {
-            val customer = BankCustomerEntity.new {
-                customerName = "payer"
-            }
-            for (iter in 1..5) {
-                BankTransactionEntity.new {
-                    localCustomer = customer
-                    counterpart = "IBAN${iter}"
-                    subject = "subject #${iter}"
-                    operationDate = DateTime.parse("3000-01-01").millis
-                    valueDate = DateTime.parse("3000-01-02").millis
-                    amount = Amount("${iter}.0")
-                }
-            }
-            val string = buildCamtString(
-                BankTransactionEntity.all(),
-                53
-            )
-            println(string)
-        }
-    }
-}
\ No newline at end of file
diff --git a/sandbox/src/test/kotlin/DbTest.kt 
b/sandbox/src/test/kotlin/DbTest.kt
deleted file mode 100644
index a0ff853..0000000
--- a/sandbox/src/test/kotlin/DbTest.kt
+++ /dev/null
@@ -1,137 +0,0 @@
-package tech.libeufin.sandbox
-
-import org.jetbrains.exposed.exceptions.ExposedSQLException
-import org.jetbrains.exposed.sql.Database
-import org.jetbrains.exposed.sql.SchemaUtils
-import org.jetbrains.exposed.sql.transactions.transaction
-import org.joda.time.DateTime
-import org.junit.Before
-import org.junit.Test
-import java.io.ByteArrayOutputStream
-import java.io.PrintStream
-import java.math.BigDecimal
-import java.math.BigInteger
-import java.math.MathContext
-import java.math.RoundingMode
-import kotlin.math.abs
-import kotlin.test.assertEquals
-import kotlin.test.assertFailsWith
-import kotlin.test.assertTrue
-import tech.libeufin.util.Amount
-import tech.libeufin.util.BadAmount
-
-
-class DbTest {
-
-    @Before
-    fun muteStderr() {
-        System.setErr(PrintStream(ByteArrayOutputStream()))
-    }
-
-    @Before
-    fun connectAndMakeTables() {
-        Database.connect("jdbc:h2:mem:test;DB_CLOSE_DELAY=-1", driver = 
"org.h2.Driver")
-        transaction {
-            SchemaUtils.create(BankTransactionsTable)
-            SchemaUtils.create(BankCustomersTable)
-        }
-    }
-
-    @Test
-    fun customers() {
-        transaction {
-            BankCustomerEntity.new {
-                customerName = "Test Name"
-            }
-            findCustomer("1")
-        }
-    }
-
-
-    @Test
-    fun goodAmount() {
-
-        transaction {
-            BankTransactionEntity.new {
-                amount = Amount("1")
-                counterpart = "IBAN"
-                subject = "Salary"
-                operationDate = DateTime.now().millis
-                valueDate = DateTime.now().millis
-                localCustomer = BankCustomerEntity.new {
-                    customerName = "employee"
-                }
-            }
-
-            BankTransactionEntity.new {
-                amount = Amount("1.11")
-                counterpart = "IBAN"
-                subject = "Salary"
-                operationDate = DateTime.now().millis
-                valueDate = DateTime.now().millis
-                localCustomer = BankCustomerEntity.new {
-                    customerName = "employee"
-                }
-            }
-
-            BankTransactionEntity.new {
-                amount = Amount("1.110000000000") // BigDecimal does not crop 
the trailing zeros
-                counterpart = "IBAN"
-                subject = "Salary"
-                operationDate = DateTime.now().millis
-                valueDate = DateTime.now().millis
-                localCustomer = BankCustomerEntity.new {
-                    customerName = "employee"
-                }
-            }
-        }
-    }
-
-    @Test
-    fun badAmount() {
-        assertFailsWith<BadAmount> {
-            transaction {
-                BankTransactionEntity.new {
-                    amount = Amount("1.10001")
-                    counterpart = "IBAN"
-                    subject = "Salary"
-                    operationDate = DateTime.now().millis
-                    valueDate = DateTime.now().millis
-                    localCustomer = BankCustomerEntity.new {
-                        customerName = "employee"
-                    }
-                }
-            }
-        }
-    }
-
-    @Test
-    fun timeBasedQuery() {
-
-
-        val NQUERIES = 10
-
-        transaction {
-
-            for (i in 1..NQUERIES) {
-                BankTransactionEntity.new {
-                    amount = Amount("1")
-                    counterpart = "IBAN"
-                    subject = "Salary"
-                    operationDate = DateTime.now().millis
-                    valueDate = DateTime.now().millis
-                    localCustomer = BankCustomerEntity.new {
-                        customerName = "employee"
-                    }
-                }
-            }
-
-            assertEquals(
-                NQUERIES,
-                BankTransactionEntity.find {
-                    
BankTransactionsTable.valueDate.between(DateTime.parse("1970-01-01").millis, 
DateTime.parse("2999-12-31").millis)
-                }.count()
-            )
-        }
-    }
-}
\ No newline at end of file
diff --git a/util/src/main/kotlin/ebics_h004/EbicsResponse.kt 
b/util/src/main/kotlin/ebics_h004/EbicsResponse.kt
index 67a8b02..bdc17b0 100644
--- a/util/src/main/kotlin/ebics_h004/EbicsResponse.kt
+++ b/util/src/main/kotlin/ebics_h004/EbicsResponse.kt
@@ -150,7 +150,6 @@ class EbicsResponse {
             }
         }
 
-
         fun createForDownloadReceiptPhase(transactionID: String, positiveAck: 
Boolean): EbicsResponse {
             return EbicsResponse().apply {
                 this.version = "H004"

-- 
To stop receiving notification emails like this one, please contact
address@hidden.



reply via email to

[Prev in Thread] Current Thread [Next in Thread]