diff --git a/src/main/scala/bootstrap/liftweb/Boot.scala b/src/main/scala/bootstrap/liftweb/Boot.scala index 667b0f51d..284fa4e6a 100755 --- a/src/main/scala/bootstrap/liftweb/Boot.scala +++ b/src/main/scala/bootstrap/liftweb/Boot.scala @@ -62,7 +62,7 @@ import code.model.dataAccess._ import code.products.MappedProduct import code.transaction_types.MappedTransactionType import code.snippet.{OAuthAuthorisation, OAuthWorkedThanks} -import code.transactionrequests.MappedTransactionRequest +import code.transactionrequests.{MappedTransactionRequest210, MappedTransactionRequest} import code.usercustomerlinks.MappedUserCustomerLink import net.liftweb.common._ import net.liftweb.http._ @@ -405,6 +405,7 @@ object ToSchemify { MappedBankAccount, MappedTransaction, MappedTransactionRequest, + MappedTransactionRequest210, MappedTransactionImage, MappedMetric, MappedCustomer, diff --git a/src/main/scala/code/api/v2_1_0/APIMethods210.scala b/src/main/scala/code/api/v2_1_0/APIMethods210.scala index 56ff4f8bd..fbf35d736 100644 --- a/src/main/scala/code/api/v2_1_0/APIMethods210.scala +++ b/src/main/scala/code/api/v2_1_0/APIMethods210.scala @@ -3,7 +3,13 @@ package code.api.v2_1_0 import java.text.SimpleDateFormat import code.api.util.ApiRole._ import code.api.util.ErrorMessages -import code.api.v2_1_0.JSONFactory210 +import code.api.v1_2_1.AmountOfMoneyJSON +import code.api.v1_4_0.JSONFactory1_4_0.TransactionRequestAccountJSON +import code.api.v2_0_0.JSONFactory200._ +import code.api.v2_0_0.{JSONFactory200, TransactionRequestBodyJSON} +import code.api.v2_1_0.JSONFactory210._ +import code.bankconnectors.Connector +import code.fx.fx import code.model._ import net.liftweb.http.Req @@ -21,11 +27,13 @@ import code.api.APIFailure import code.api.util.APIUtil._ import code.sandbox.{OBPDataImport, SandboxDataImport} import code.util.Helper -import net.liftweb.common.Box +import net.liftweb.common.{Empty, Full, Box} import net.liftweb.http.JsonResponse import net.liftweb.http.js.JE.JsRaw import net.liftweb.http.rest.RestHelper import net.liftweb.util.Helpers._ +import net.liftweb.json._ +import net.liftweb.json.Serialization.{read, write} trait APIMethods210 { @@ -132,6 +140,198 @@ trait APIMethods210 { } } } + + + import net.liftweb.json.JsonAST._ + import net.liftweb.json.Extraction._ + import net.liftweb.json.Printer._ + val exchangeRates = pretty(render(decompose(fx.exchangeRates))) + + resourceDocs += ResourceDoc( + createTransactionRequest, + apiVersion, + "createTransactionRequest", + "POST", + "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/transaction-request-types/TRANSACTION_REQUEST_TYPE/transaction-requests", + "Create Transaction Request.", + s"""Initiate a Payment via a Transaction Request. + | + |This is the preferred method to create a payment and supersedes makePayment in 1.2.1. + | + |PSD2 Context: Third party access access to payments is a core tenent of PSD2. + | + |This call satisfies that requirement from several perspectives: + | + |1) A transaction can be initiated by a third party application. + | + |2) The customer is informed of the charge that will incurred. + | + |3) The call uses delegated authentication (OAuth) + | + |See [this python code](https://github.com/OpenBankProject/Hello-OBP-DirectLogin-Python/blob/master/hello_payments.py) for a complete example of this flow. + | + |In sandbox mode, if the amount is less than 100 (any currency), the transaction request will create a transaction without a challenge, else a challenge will need to be answered. + | + |You can transfer between different currency accounts. (new in 2.0.0). The currency in body must match the sending account. + | + |Currently TRANSACTION_REQUEST_TYPE must be set to SANDBOX_TAN + | + |The following static FX rates are available in sandbox mode: + | + |${exchangeRates} + | + | + |The payer is set in the URL. Money comes out of the BANK_ID and ACCOUNT_ID specified in the URL + | + |The payee is set in the request body. Money goes into the BANK_ID and ACCOUNT_IDO specified in the request body. + | + | + |${authenticationRequiredMessage(true)} + | + |""", + Extraction.decompose(TransactionRequestBodyJSON ( + TransactionRequestAccountJSON("BANK_ID", "ACCOUNT_ID"), + AmountOfMoneyJSON("EUR", "100.53"), + "A description for the transaction to be created" + ) + ), + emptyObjectJson, + emptyObjectJson :: Nil, + true, + true, + true, + List(apiTagTransactionRequest)) + + lazy val createTransactionRequest: PartialFunction[Req, Box[User] => Box[JsonResponse]] = { + case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: ViewId(viewId) :: "transaction-request-types" :: + TransactionRequestType(transactionRequestType) :: "transaction-requests" :: Nil JsonPost json -> _ => { + user => + if (Props.getBool("transactionRequests_enabled", false)) { + for { + /* TODO: + * check if user has access using the view that is given (now it checks if user has access to owner view), will need some new permissions for transaction requests + * test: functionality, error messages if user not given or invalid, if any other value is not existing + */ + u <- user ?~ ErrorMessages.UserNotLoggedIn + + // Get Transaction Request Types from Props "transactionRequests_supported_types". Default is empty string + validTransactionRequestTypes <- tryo{Props.get("transactionRequests_supported_types", "")} + // Use a list instead of a string to avoid partial matches + validTransactionRequestTypesList <- tryo{validTransactionRequestTypes.split(",")} + isValidTransactionRequestType <- tryo(assert(transactionRequestType.value != "TRANSACTION_REQUEST_TYPE" && validTransactionRequestTypesList.contains(transactionRequestType.value))) ?~! s"${ErrorMessages.InvalidTransactionRequestType} : Invalid value is: '${transactionRequestType.value}' Valid values are: ${validTransactionRequestTypes}" + + transDetailsJson <- transactionRequestType.value match { + case "SANDBOX_TAN" => tryo { + json.extract[TransactionRequestDetailsSandBoxTanJSON] + } ?~ { + ErrorMessages.InvalidJsonFormat + } + case "SEPA" => tryo { + json.extract[TransactionRequestDetailsSEPAJSON] + } ?~ { + ErrorMessages.InvalidJsonFormat + } + } + + transDetails <- transactionRequestType.value match { + case "SANDBOX_TAN" => tryo{getTransactionRequestDetailsSandBoxTanFromJson(transDetailsJson.asInstanceOf[TransactionRequestDetailsSandBoxTanJSON])} + case "SEPA" => tryo{getTransactionRequestDetailsSEPAFromJson(transDetailsJson.asInstanceOf[TransactionRequestDetailsSEPAJSON])} + } + + fromBank <- Bank(bankId) ?~! {ErrorMessages.BankNotFound} + fromAccount <- BankAccount(bankId, accountId) ?~! {ErrorMessages.AccountNotFound} + isOwnerOrHasEntitlement <- booleanToBox(u.ownerAccess(fromAccount) == true || hasEntitlement(fromAccount.bankId.value, u.userId, CanCreateAnyTransactionRequest) == true , ErrorMessages.InsufficientAuthorisationToCreateTransactionRequest) + + // Prevent default value for transaction request type (at least). + transferCurrencyEqual <- tryo(assert(transDetailsJson.value.currency == fromAccount.currency)) ?~! {"Transfer body currency and holder account currency must be the same."} + + transDetailsSerialized <- tryo{ + implicit val formats = Serialization.formats(NoTypeHints) + write(transDetailsJson) + } + + createdTransactionRequest <- transactionRequestType.value match { + case "SANDBOX_TAN" => { + for { + toBankId <- Full(BankId(transDetailsJson.asInstanceOf[TransactionRequestDetailsSandBoxTanJSON].to.bank_id)) + toAccountId <- Full(AccountId(transDetailsJson.asInstanceOf[TransactionRequestDetailsSandBoxTanJSON].to.account_id)) + toAccount <- BankAccount(toBankId, toAccountId) ?~! {ErrorMessages.CounterpartyNotFound} + createdTransactionRequest <- Connector.connector.vend.createTransactionRequestv210(u, fromAccount, Full(toAccount), transactionRequestType, transDetails, transDetailsSerialized) + } yield createdTransactionRequest + + } + case "SEPA" => Connector.connector.vend.createTransactionRequestv210(u, fromAccount, Empty, transactionRequestType, transDetails, transDetailsSerialized) + } + } yield { + // Explicitly format as v2.0.0 json + val json = JSONFactory210.createTransactionRequestWithChargeJSON(createdTransactionRequest) + createdJsonResponse(Extraction.decompose(json)) + } + } else { + Full(errorJsonResponse("Sorry, Transaction Requests are not enabled in this API instance.")) + } + } + } + + + resourceDocs += ResourceDoc( + getTransactionRequests, + apiVersion, + "getTransactionRequests", + "GET", + "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/transaction-requests", + "Get Transaction Requests." , + """Returns transaction requests for account specified by ACCOUNT_ID at bank specified by BANK_ID. + | + |The VIEW_ID specified must be 'owner' and the user must have access to this view. + | + |Version 2.0.0 now returns charge information. + | + |Transaction Requests serve to initiate transactions that may or may not proceed. They contain information including: + | + |* Transaction Request Id + |* Type + |* Status (INITIATED, COMPLETED) + |* Challenge (in order to confirm the request) + |* From Bank / Account + |* Details including Currency, Value, Description and other initiation information specific to each type. (Could potentialy include a list of future transactions.) + |* Related Transactions + | + |PSD2 Context: PSD2 requires transparency of charges to the customer. + |This endpoint provides the charge that would be applied if the Transaction Request proceeds - and a record of that charge there after. + |The customer can proceed with the Transaction by answering the security challenge. + | + | + """.stripMargin, + emptyObjectJson, + emptyObjectJson, + emptyObjectJson :: Nil, + true, + true, + true, + List(apiTagTransactionRequest)) + + lazy val getTransactionRequests: PartialFunction[Req, Box[User] => Box[JsonResponse]] = { + case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: ViewId(viewId) :: "transaction-requests" :: Nil JsonGet _ => { + user => + if (Props.getBool("transactionRequests_enabled", false)) { + for { + u <- user ?~ ErrorMessages.UserNotLoggedIn + fromBank <- Bank(bankId) ?~! {ErrorMessages.BankNotFound} + fromAccount <- BankAccount(bankId, accountId) ?~! {ErrorMessages.AccountNotFound} + view <- tryo(fromAccount.permittedViews(user).find(_ == viewId)) ?~ {"Current user does not have access to the view " + viewId} + transactionRequests <- Connector.connector.vend.getTransactionRequests210(u, fromAccount) + } + yield { + // Format the data as V2.0.0 json + val json = JSONFactory210.createTransactionRequestJSONs(transactionRequests) + successJsonResponse(Extraction.decompose(json)) + } + } else { + Full(errorJsonResponse("Sorry, Transaction Requests are not enabled in this API instance.")) + } + } + } } } diff --git a/src/main/scala/code/api/v2_1_0/JSONFactory2.1.0.scala b/src/main/scala/code/api/v2_1_0/JSONFactory2.1.0.scala index 4b518adc4..3d85e3f0f 100644 --- a/src/main/scala/code/api/v2_1_0/JSONFactory2.1.0.scala +++ b/src/main/scala/code/api/v2_1_0/JSONFactory2.1.0.scala @@ -31,9 +31,49 @@ Berlin 13359, Germany */ package code.api.v2_1_0 +import java.util.Date + +import code.api.v1_2_1.AmountOfMoneyJSON +import code.api.v1_4_0.JSONFactory1_4_0.{ChallengeJSON, TransactionRequestAccountJSON} +import code.api.v2_0_0.{TransactionRequestWithChargeJSONs, TransactionRequestChargeJSON, TransactionRequestBodyJSON} +import code.model.AmountOfMoney +import code.transactionrequests.TransactionRequests._ + case class TransactionRequestTypeJSON(transaction_request_type: String) case class TransactionRequestTypesJSON(transaction_request_types: List[TransactionRequestTypeJSON]) +trait TransactionRequestDetailsJSON { + val value : AmountOfMoneyJSON +} + +case class TransactionRequestDetailsSandBoxTanJSON( + to: TransactionRequestAccountJSON, + value : AmountOfMoneyJSON, + description : String + ) extends TransactionRequestDetailsJSON + +case class TransactionRequestDetailsSEPAJSON( + value : AmountOfMoneyJSON, + description : String + ) extends TransactionRequestDetailsJSON + +case class TransactionRequestWithChargeJSON210( + id: String, + `type`: String, + from: TransactionRequestAccountJSON, + details: String, + transaction_ids: String, + status: String, + start_date: Date, + end_date: Date, + challenge: ChallengeJSON, + charge : TransactionRequestChargeJSON + ) + +case class TransactionRequestWithChargeJSONs210( + transaction_requests_with_charges : List[TransactionRequestWithChargeJSON210] + ) + object JSONFactory210{ def createTransactionRequestTypeJSON(transactionRequestType : String ) : TransactionRequestTypeJSON = { new TransactionRequestTypeJSON( @@ -44,4 +84,69 @@ object JSONFactory210{ def createTransactionRequestTypeJSON(transactionRequestTypes : List[String]) : TransactionRequestTypesJSON = { TransactionRequestTypesJSON(transactionRequestTypes.map(createTransactionRequestTypeJSON)) } + + //transaction requests + def getTransactionRequestDetailsSandBoxTanFromJson(details: TransactionRequestDetailsSandBoxTanJSON) : TransactionRequestDetailsSandBoxTan = { + val toAcc = TransactionRequestAccount ( + bank_id = details.to.bank_id, + account_id = details.to.account_id + ) + val amount = AmountOfMoney ( + currency = details.value.currency, + amount = details.value.amount + ) + + TransactionRequestDetailsSandBoxTan ( + to = toAcc, + value = amount, + description = details.description + ) + } + + def getTransactionRequestDetailsSEPAFromJson(details: TransactionRequestDetailsSEPAJSON) : TransactionRequestDetailsSEPA = { + val amount = AmountOfMoney ( + currency = details.value.currency, + amount = details.value.amount + ) + + TransactionRequestDetailsSEPA ( + value = amount, + description = details.description + ) + } + + /** Creates v2.1.0 representation of a TransactionType + * + * @param tr An internal TransactionRequest instance + * @return a v2.1.0 representation of a TransactionRequest + */ + + def createTransactionRequestWithChargeJSON(tr : TransactionRequest210) : TransactionRequestWithChargeJSON210 = { + new TransactionRequestWithChargeJSON210( + id = tr.id.value, + `type` = tr.`type`, + from = TransactionRequestAccountJSON ( + bank_id = tr.from.bank_id, + account_id = tr.from.account_id), + details = tr.details, + transaction_ids = tr.transaction_ids, + status = tr.status, + start_date = tr.start_date, + end_date = tr.end_date, + // Some (mapped) data might not have the challenge. TODO Make this nicer + challenge = { + try {ChallengeJSON (id = tr.challenge.id, allowed_attempts = tr.challenge.allowed_attempts, challenge_type = tr.challenge.challenge_type)} + // catch { case _ : Throwable => ChallengeJSON (id = "", allowed_attempts = 0, challenge_type = "")} + catch { case _ : Throwable => null} + }, + charge = TransactionRequestChargeJSON (summary = tr.charge.summary, + value = AmountOfMoneyJSON(currency = tr.charge.value.currency, + amount = tr.charge.value.amount) + ) + ) + } + + def createTransactionRequestJSONs(trs : List[TransactionRequest210]) : TransactionRequestWithChargeJSONs210 = { + TransactionRequestWithChargeJSONs210(trs.map(createTransactionRequestWithChargeJSON)) + } } \ No newline at end of file diff --git a/src/main/scala/code/api/v2_1_0/OBPAPI2_1_0.scala b/src/main/scala/code/api/v2_1_0/OBPAPI2_1_0.scala index 663e55f56..ac9f65ae8 100644 --- a/src/main/scala/code/api/v2_1_0/OBPAPI2_1_0.scala +++ b/src/main/scala/code/api/v2_1_0/OBPAPI2_1_0.scala @@ -143,9 +143,9 @@ object OBPAPI2_1_0 extends OBPRestHelper with APIMethods130 with APIMethods140 w Implementations2_0_0.allAccountsAtOneBank, Implementations2_0_0.privateAccountsAtOneBank, Implementations2_0_0.publicAccountsAtOneBank, - Implementations2_0_0.createTransactionRequest, + // Now in 2.1.0 Implementations2_0_0.createTransactionRequest, Implementations2_0_0.answerTransactionRequestChallenge, - Implementations2_0_0.getTransactionRequests, // Now has charges information + // Now in 2.1.0 Implementations2_0_0.getTransactionRequests, // Now has charges information // Updated in 2.0.0 (added sorting and better guards / error messages) Implementations2_0_0.accountById, Implementations2_0_0.getPermissionsForBankAccount, @@ -182,7 +182,9 @@ object OBPAPI2_1_0 extends OBPRestHelper with APIMethods130 with APIMethods140 w Implementations2_0_0.getCustomers, // New in 2.1.0 Implementations2_1_0.sandboxDataImport, - Implementations2_1_0.getTransactionRequestTypesSupportedByBank + Implementations2_1_0.getTransactionRequestTypesSupportedByBank, + Implementations2_1_0.createTransactionRequest, + Implementations2_1_0.getTransactionRequests ) routes.foreach(route => { diff --git a/src/main/scala/code/bankconnectors/Connector.scala b/src/main/scala/code/bankconnectors/Connector.scala index 38dd1311e..2023cc795 100644 --- a/src/main/scala/code/bankconnectors/Connector.scala +++ b/src/main/scala/code/bankconnectors/Connector.scala @@ -10,7 +10,7 @@ import code.management.ImporterAPI.ImporterTransaction import code.model.{OtherBankAccount, Transaction, User, _} import code.tesobe.CashTransaction import code.transactionrequests.TransactionRequests -import code.transactionrequests.TransactionRequests.{TransactionRequest, TransactionRequestBody, TransactionRequestChallenge, TransactionRequestCharge} +import code.transactionrequests.TransactionRequests._ import code.util.Helper._ import net.liftweb.common.{Box, Empty, Failure, Full} import net.liftweb.util.Helpers._ @@ -169,6 +169,7 @@ trait Connector { } yield transactionId } + protected def makePaymentImpl(fromAccount : AccountType, toAccount : AccountType, amt : BigDecimal, description : String) : Box[TransactionId] @@ -302,6 +303,71 @@ trait Connector { result } + def createTransactionRequestv210(initiator : User, fromAccount : BankAccount, toAccount: Box[BankAccount], transactionRequestType: TransactionRequestType, details: TransactionRequestDetails, detailsPlain: String) : Box[TransactionRequest210] = { + //set initial status + //for sandbox / testing: depending on amount, we ask for challenge or not + val status = + if (transactionRequestType.value == TransactionRequests.CHALLENGE_SANDBOX_TAN && BigDecimal(details.value.amount) < 1000) { + TransactionRequests.STATUS_COMPLETED + } else { + TransactionRequests.STATUS_INITIATED + } + + + // Always create a new Transaction Request + var result = for { + fromAccountType <- getBankAccount(fromAccount.bankId, fromAccount.accountId) ?~ + s"account ${fromAccount.accountId} not found at bank ${fromAccount.bankId}" + isOwner <- booleanToBox(initiator.ownerAccess(fromAccount) == true || hasEntitlement(fromAccount.bankId.value, initiator.userId, CanCreateAnyTransactionRequest) == true , ErrorMessages.InsufficientAuthorisationToCreateTransactionRequest) + + rawAmt <- tryo { BigDecimal(details.value.amount) } ?~! s"amount ${details.value.amount} not convertible to number" + // isValidTransactionRequestType is checked at API layer. Maybe here too. + isPositiveAmtToSend <- booleanToBox(rawAmt > BigDecimal("0"), s"Can't send a payment with a value of 0 or less. (${rawAmt})") + + + // For now, arbitary charge value to demonstrate PSD2 charge transparency principle. Eventually this would come from Transaction Type? 10 decimal places of scaling so can add small percentage per transaction. + chargeValue <- tryo {(BigDecimal(details.value.amount) * 0.0001).setScale(10, BigDecimal.RoundingMode.HALF_UP).toDouble} ?~! s"could not create charge for ${details.value.amount}" + charge = TransactionRequestCharge("Total charges for completed transaction", AmountOfMoney(details.value.currency, chargeValue.toString())) + + + + transactionRequest <- createTransactionRequestImpl210(TransactionRequestId(java.util.UUID.randomUUID().toString), transactionRequestType, fromAccount, detailsPlain, status, charge) + } yield transactionRequest + + //make sure we get something back + result = Full(result.openOrThrowException("Exception: Couldn't create transactionRequest")) + + // If no challenge necessary, create Transaction immediately and put in data store and object to return + if (status == TransactionRequests.STATUS_COMPLETED) { + val createdTransactionId = transactionRequestType.value match { + case "SANDBOX_TAN" => Connector.connector.vend.makePaymentv200(initiator, BankAccountUID(fromAccount.bankId, fromAccount.accountId), + BankAccountUID(toAccount.get.bankId, toAccount.get.accountId), BigDecimal(details.value.amount), details.asInstanceOf[TransactionRequestDetailsSandBoxTan].description) + case "SEPA" => Empty + } + + //set challenge to null + result = Full(result.get.copy(challenge = null)) + + //save transaction_id if we have one + createdTransactionId match { + case Full(ti) => { + if (! createdTransactionId.isEmpty) { + saveTransactionRequestTransaction(result.get.id, ti) + result = Full(result.get.copy(transaction_ids = ti.value)) + } + } + case _ => None + } + } else { + //if challenge necessary, create a new one + var challenge = TransactionRequestChallenge(id = java.util.UUID.randomUUID().toString, allowed_attempts = 3, challenge_type = TransactionRequests.CHALLENGE_SANDBOX_TAN) + saveTransactionRequestChallenge(result.get.id, challenge) + result = Full(result.get.copy(challenge = challenge)) + } + + result + } + @@ -311,6 +377,10 @@ trait Connector { fromAccount : BankAccount, counterparty : BankAccount, body: TransactionRequestBody, status: String, charge: TransactionRequestCharge) : Box[TransactionRequest] + protected def createTransactionRequestImpl210(transactionRequestId: TransactionRequestId, transactionRequestType: TransactionRequestType, + fromAccount : BankAccount, details: String, + status: String, charge: TransactionRequestCharge) : Box[TransactionRequest210] + def saveTransactionRequestTransaction(transactionRequestId: TransactionRequestId, transactionId: TransactionId) = { //put connector agnostic logic here if necessary @@ -351,8 +421,33 @@ trait Connector { } } + def getTransactionRequests210(initiator : User, fromAccount : BankAccount) : Box[List[TransactionRequest210]] = { + val transactionRequests = + for { + fromAccount <- getBankAccount(fromAccount.bankId, fromAccount.accountId) ?~ + s"account ${fromAccount.accountId} not found at bank ${fromAccount.bankId}" + isOwner <- booleanToBox(initiator.ownerAccess(fromAccount), "user does not have access to owner view") + transactionRequests <- getTransactionRequestsImpl210(fromAccount) + } yield transactionRequests + + //make sure we return null if no challenge was saved (instead of empty fields) + if (!transactionRequests.isEmpty) { + Full( + transactionRequests.get.map(tr => if (tr.challenge.id == "") { + tr.copy(challenge = null) + } else { + tr + }) + ) + } else { + transactionRequests + } + } + protected def getTransactionRequestsImpl(fromAccount : BankAccount) : Box[List[TransactionRequest]] + protected def getTransactionRequestsImpl210(fromAccount : BankAccount) : Box[List[TransactionRequest210]] + protected def getTransactionRequestImpl(transactionRequestId: TransactionRequestId) : Box[TransactionRequest] def getTransactionRequestTypes(initiator : User, fromAccount : BankAccount) : Box[List[TransactionRequestType]] = { diff --git a/src/main/scala/code/bankconnectors/KafkaMappedConnector.scala b/src/main/scala/code/bankconnectors/KafkaMappedConnector.scala index c94b2eaee..ed8f35972 100644 --- a/src/main/scala/code/bankconnectors/KafkaMappedConnector.scala +++ b/src/main/scala/code/bankconnectors/KafkaMappedConnector.scala @@ -15,8 +15,8 @@ import code.model._ import code.model.dataAccess._ import code.sandbox.{CreateViewImpls, Saveable} import code.transaction.MappedTransaction -import code.transactionrequests.MappedTransactionRequest -import code.transactionrequests.TransactionRequests.{TransactionRequest, TransactionRequestBody, TransactionRequestChallenge, TransactionRequestCharge } +import code.transactionrequests.{MappedTransactionRequest210, MappedTransactionRequest} +import code.transactionrequests.TransactionRequests._ import code.util.{Helper, TTLCache} import code.views.Views import net.liftweb.common._ @@ -488,6 +488,21 @@ object KafkaMappedConnector extends Connector with CreateViewImpls with Loggable Full(mappedTransactionRequest).flatMap(_.toTransactionRequest) } + override def createTransactionRequestImpl210(transactionRequestId: TransactionRequestId, transactionRequestType: TransactionRequestType, + account : BankAccount, details: String, + status: String, charge: TransactionRequestCharge) : Box[TransactionRequest210] = { + val mappedTransactionRequest = MappedTransactionRequest210.create + .mTransactionRequestId(transactionRequestId.value) + .mType(transactionRequestType.value) + .mFrom_BankId(account.bankId.value) + .mFrom_AccountId(account.accountId.value) + .mDetails(details) + .mStatus(status) + .mStartDate(now) + .mEndDate(now).saveMe + Full(mappedTransactionRequest).flatMap(_.toTransactionRequest210) + } + override def saveTransactionRequestTransactionImpl(transactionRequestId: TransactionRequestId, transactionId: TransactionId): Box[Boolean] = { val mappedTransactionRequest = MappedTransactionRequest.find(By(MappedTransactionRequest.mTransactionRequestId, transactionRequestId.value)) mappedTransactionRequest match { @@ -524,6 +539,13 @@ object KafkaMappedConnector extends Connector with CreateViewImpls with Loggable Full(transactionRequests.flatMap(_.toTransactionRequest)) } + override def getTransactionRequestsImpl210(fromAccount : BankAccount) : Box[List[TransactionRequest210]] = { + val transactionRequests = MappedTransactionRequest210.findAll(By(MappedTransactionRequest210.mFrom_AccountId, fromAccount.accountId.value), + By(MappedTransactionRequest210.mFrom_BankId, fromAccount.bankId.value)) + + Full(transactionRequests.flatMap(_.toTransactionRequest210)) + } + override def getTransactionRequestImpl(transactionRequestId: TransactionRequestId) : Box[TransactionRequest] = { val transactionRequest = MappedTransactionRequest.find(By(MappedTransactionRequest.mTransactionRequestId, transactionRequestId.value)) transactionRequest.flatMap(_.toTransactionRequest) diff --git a/src/main/scala/code/bankconnectors/LocalConnector.scala b/src/main/scala/code/bankconnectors/LocalConnector.scala index 7c9acb948..b7a477fa6 100644 --- a/src/main/scala/code/bankconnectors/LocalConnector.scala +++ b/src/main/scala/code/bankconnectors/LocalConnector.scala @@ -8,7 +8,7 @@ import code.metadata.counterparties.{Counterparties, Metadata, MongoCounterparti import code.model._ import code.model.dataAccess._ import code.tesobe.CashTransaction -import code.transactionrequests.TransactionRequests.{TransactionRequest, TransactionRequestBody, TransactionRequestChallenge, TransactionRequestCharge} +import code.transactionrequests.TransactionRequests._ import code.util.Helper import com.mongodb.QueryBuilder import com.tesobe.model.UpdateBankAccount @@ -314,9 +314,14 @@ private object LocalConnector extends Connector with Loggable { account : BankAccount, counterparty : BankAccount, body: TransactionRequestBody, status: String, charge: TransactionRequestCharge) : Box[TransactionRequest] = ??? + override def createTransactionRequestImpl210(transactionRequestId: TransactionRequestId, transactionRequestType: TransactionRequestType, + account : BankAccount, details: String, + status: String, charge: TransactionRequestCharge) : Box[TransactionRequest210] = ??? + override def saveTransactionRequestTransactionImpl(transactionRequestId: TransactionRequestId, transactionId: TransactionId) = ??? override def saveTransactionRequestChallengeImpl(transactionRequestId: TransactionRequestId, challenge: TransactionRequestChallenge) = ??? override def getTransactionRequestsImpl(fromAccount : BankAccount) : Box[List[TransactionRequest]] = ??? + override def getTransactionRequestsImpl210(fromAccount : BankAccount) : Box[List[TransactionRequest210]] = ??? override def getTransactionRequestImpl(transactionRequestId: TransactionRequestId) : Box[TransactionRequest] = ??? override def getTransactionRequestTypesImpl(fromAccount : BankAccount) : Box[List[TransactionRequestType]] = { //TODO: write logic / data access diff --git a/src/main/scala/code/bankconnectors/LocalMappedConnector.scala b/src/main/scala/code/bankconnectors/LocalMappedConnector.scala index 93b7764c2..3d8c01a31 100644 --- a/src/main/scala/code/bankconnectors/LocalMappedConnector.scala +++ b/src/main/scala/code/bankconnectors/LocalMappedConnector.scala @@ -14,8 +14,8 @@ import code.model._ import code.model.dataAccess._ import code.tesobe.CashTransaction import code.transaction.MappedTransaction -import code.transactionrequests.MappedTransactionRequest -import code.transactionrequests.TransactionRequests.{TransactionRequest, TransactionRequestBody, TransactionRequestChallenge, TransactionRequestCharge} +import code.transactionrequests.{MappedTransactionRequest210, MappedTransactionRequest} +import code.transactionrequests.TransactionRequests._ import code.util.Helper import com.tesobe.model.UpdateBankAccount import net.liftweb.common.{Box, Failure, Full, Loggable} @@ -250,6 +250,25 @@ object LocalMappedConnector extends Connector with Loggable { Full(mappedTransactionRequest).flatMap(_.toTransactionRequest) } + override def createTransactionRequestImpl210(transactionRequestId: TransactionRequestId, transactionRequestType: TransactionRequestType, + account : BankAccount, details: String, + status: String, charge: TransactionRequestCharge) : Box[TransactionRequest210] = { + val mappedTransactionRequest = MappedTransactionRequest210.create + .mTransactionRequestId(transactionRequestId.value) + .mType(transactionRequestType.value) + .mFrom_BankId(account.bankId.value) + .mFrom_AccountId(account.accountId.value) + .mDetails(details) + .mStatus(status) + .mStartDate(now) + .mEndDate(now) + .mCharge_Summary(charge.summary) + .mCharge_Amount(charge.value.amount) + .mCharge_Currency(charge.value.currency) + .saveMe + Full(mappedTransactionRequest).flatMap(_.toTransactionRequest210) + } + override def saveTransactionRequestTransactionImpl(transactionRequestId: TransactionRequestId, transactionId: TransactionId): Box[Boolean] = { val mappedTransactionRequest = MappedTransactionRequest.find(By(MappedTransactionRequest.mTransactionRequestId, transactionRequestId.value)) mappedTransactionRequest match { @@ -286,6 +305,13 @@ object LocalMappedConnector extends Connector with Loggable { Full(transactionRequests.flatMap(_.toTransactionRequest)) } + override def getTransactionRequestsImpl210(fromAccount : BankAccount) : Box[List[TransactionRequest210]] = { + val transactionRequests = MappedTransactionRequest210.findAll(By(MappedTransactionRequest210.mFrom_AccountId, fromAccount.accountId.value), + By(MappedTransactionRequest210.mFrom_BankId, fromAccount.bankId.value)) + + Full(transactionRequests.flatMap(_.toTransactionRequest210)) + } + override def getTransactionRequestImpl(transactionRequestId: TransactionRequestId) : Box[TransactionRequest] = { val transactionRequest = MappedTransactionRequest.find(By(MappedTransactionRequest.mTransactionRequestId, transactionRequestId.value)) transactionRequest.flatMap(_.toTransactionRequest) diff --git a/src/main/scala/code/transactionrequests/MappedTransactionRequestProvider.scala b/src/main/scala/code/transactionrequests/MappedTransactionRequestProvider.scala index 5bfaae1a4..31daf7b6c 100644 --- a/src/main/scala/code/transactionrequests/MappedTransactionRequestProvider.scala +++ b/src/main/scala/code/transactionrequests/MappedTransactionRequestProvider.scala @@ -102,4 +102,68 @@ class MappedTransactionRequest extends LongKeyedMapper[MappedTransactionRequest] object MappedTransactionRequest extends MappedTransactionRequest with LongKeyedMetaMapper[MappedTransactionRequest] { override def dbIndexes = UniqueIndex(mTransactionRequestId) :: super.dbIndexes +} + +class MappedTransactionRequest210 extends LongKeyedMapper[MappedTransactionRequest210] with IdPK with CreatedUpdated { + + private val logger = Logger(classOf[MappedTransactionRequest210]) + + override def getSingleton = MappedTransactionRequest210 + + object mTransactionRequestId extends DefaultStringField(this) + object mType extends DefaultStringField(this) + object mFrom_BankId extends DefaultStringField(this) + object mFrom_AccountId extends DefaultStringField(this) + + //details fields + object mDetails extends DefaultStringField(this) + + object mTransactionIDs extends DefaultStringField(this) + object mStatus extends DefaultStringField(this) + object mStartDate extends MappedDate(this) + object mEndDate extends MappedDate(this) + object mChallenge_Id extends DefaultStringField(this) + object mChallenge_AllowedAttempts extends MappedInt(this) + object mChallenge_ChallengeType extends DefaultStringField(this) + + object mCharge_Summary extends DefaultStringField(this) + object mCharge_Amount extends DefaultStringField(this) + object mCharge_Currency extends DefaultStringField(this) + + def toTransactionRequest210 : Option[TransactionRequest210] = { + val t_from = TransactionRequestAccount ( + bank_id = mFrom_BankId.get, + account_id = mFrom_AccountId.get + ) + + val t_challenge = TransactionRequestChallenge ( + id = mChallenge_Id, + allowed_attempts = mChallenge_AllowedAttempts, + challenge_type = mChallenge_ChallengeType + ) + + val t_charge = TransactionRequestCharge ( + summary = mCharge_Summary, + value = AmountOfMoney(currency = mCharge_Currency, amount = mCharge_Amount) + ) + + Some( + TransactionRequest210( + id = TransactionRequestId(mTransactionRequestId.get), + `type`= mType.get, + from = t_from, + details = mDetails.get, + status = mStatus.get, + transaction_ids = mTransactionIDs.get, + start_date = mStartDate.get, + end_date = mEndDate.get, + challenge = t_challenge, + charge = t_charge + ) + ) + } +} + +object MappedTransactionRequest210 extends MappedTransactionRequest210 with LongKeyedMetaMapper[MappedTransactionRequest210] { + override def dbIndexes = UniqueIndex(mTransactionRequestId) :: super.dbIndexes } \ No newline at end of file diff --git a/src/main/scala/code/transactionrequests/TransactionRequests.scala b/src/main/scala/code/transactionrequests/TransactionRequests.scala index e6e096476..3749bb322 100644 --- a/src/main/scala/code/transactionrequests/TransactionRequests.scala +++ b/src/main/scala/code/transactionrequests/TransactionRequests.scala @@ -38,6 +38,19 @@ object TransactionRequests extends SimpleInjector { val charge: TransactionRequestCharge ) + case class TransactionRequest210 ( + val id: TransactionRequestId, + val `type` : String, + val from: TransactionRequestAccount, + val details: String, + val transaction_ids: String, + val status: String, + val start_date: Date, + val end_date: Date, + val challenge: TransactionRequestChallenge, + val charge: TransactionRequestCharge + ) + case class TransactionRequestChallenge ( val id: String, val allowed_attempts : Int, @@ -55,6 +68,21 @@ object TransactionRequests extends SimpleInjector { val description : String ) + trait TransactionRequestDetails { + val value: AmountOfMoney + } + + case class TransactionRequestDetailsSandBoxTan ( + val to: TransactionRequestAccount, + val value : AmountOfMoney, + val description : String + ) extends TransactionRequestDetails + + case class TransactionRequestDetailsSEPA ( + val value : AmountOfMoney, + val description : String + ) extends TransactionRequestDetails + val transactionRequestProvider = new Inject(buildOne _) {} def buildOne: TransactionRequestProvider = diff --git a/src/test/scala/code/api/v1_3_0/PhysicalCardsTest.scala b/src/test/scala/code/api/v1_3_0/PhysicalCardsTest.scala index c11b10034..afc896d3f 100644 --- a/src/test/scala/code/api/v1_3_0/PhysicalCardsTest.scala +++ b/src/test/scala/code/api/v1_3_0/PhysicalCardsTest.scala @@ -8,7 +8,7 @@ import code.bankconnectors.{Connector, OBPQueryParam} import code.management.ImporterAPI.ImporterTransaction import code.model.{PhysicalCard, Consumer => OBPConsumer, Token => OBPToken, _} import code.tesobe.CashTransaction -import code.transactionrequests.TransactionRequests.{TransactionRequest, TransactionRequestBody, TransactionRequestChallenge, TransactionRequestCharge} +import code.transactionrequests.TransactionRequests._ import net.liftweb.common.{Box, Empty, Failure, Loggable} class PhysicalCardsTest extends ServerSetup with DefaultUsers { @@ -95,11 +95,17 @@ class PhysicalCardsTest extends ServerSetup with DefaultUsers { status: String, charge: TransactionRequestCharge) : Box[TransactionRequest] = { Failure("not supported") } + override def createTransactionRequestImpl210(transactionRequestId: TransactionRequestId, transactionRequestType: TransactionRequestType, + account : BankAccount, details: String, + status: String, charge: TransactionRequestCharge) : Box[TransactionRequest210] = { + Failure("not supported") + } override def saveTransactionRequestTransactionImpl(transactionRequestId: TransactionRequestId, transactionId: TransactionId) = ??? override def saveTransactionRequestChallengeImpl(transactionRequestId: TransactionRequestId, challenge: TransactionRequestChallenge) = ??? override def saveTransactionRequestStatusImpl(transactionRequestId: TransactionRequestId, status: String): Box[Boolean] = ??? override def getTransactionRequestsImpl(fromAccount : BankAccount) : Box[List[TransactionRequest]] = ??? + override def getTransactionRequestsImpl210(fromAccount : BankAccount) : Box[List[TransactionRequest210]] = ??? override def getTransactionRequestImpl(transactionRequestId: TransactionRequestId) : Box[TransactionRequest] = ??? override def getTransactionRequestTypesImpl(fromAccount : BankAccount) : Box[List[TransactionRequestType]] = { Failure("not supported")