Merge pull request #2601 from hongwei1/feature/cardanoTest

Feature/cardano test
This commit is contained in:
Simon Redfern 2025-08-25 18:13:17 +02:00 committed by GitHub
commit 4d60b8a4da
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
27 changed files with 2103 additions and 799 deletions

7
.gitignore vendored
View File

@ -28,3 +28,10 @@ obp-api/src/main/scala/code/api/v3_0_0/custom/
# Marketing diagram generation outputs
marketing_diagram_generation/outputs/*
.bloop
.bsp
.specstory
project/project
coursier
*.code-workspace

View File

@ -35,7 +35,7 @@ import code.accountattribute.MappedAccountAttribute
import code.accountholders.MapperAccountHolders
import code.actorsystem.ObpActorSystem
import code.api.Constant._
import code.api.ResourceDocs1_4_0.ResourceDocs300.{ResourceDocs310, ResourceDocs400, ResourceDocs500, ResourceDocs510}
import code.api.ResourceDocs1_4_0.ResourceDocs300.{ResourceDocs310, ResourceDocs400, ResourceDocs500, ResourceDocs510, ResourceDocs600}
import code.api.ResourceDocs1_4_0._
import code.api._
import code.api.attributedefinition.AttributeDefinition
@ -46,7 +46,6 @@ import code.api.util.ApiRole.CanCreateEntitlementAtAnyBank
import code.api.util.ErrorMessages.MandatoryPropertyIsNotSet
import code.api.util._
import code.api.util.migration.Migration
import code.api.util.CommonsEmailWrapper
import code.api.util.migration.Migration.DbFunction
import code.apicollection.ApiCollection
import code.apicollectionendpoint.ApiCollectionEndpoint
@ -467,6 +466,7 @@ class Boot extends MdcLoggable {
enableVersionIfAllowed(ApiVersion.v4_0_0)
enableVersionIfAllowed(ApiVersion.v5_0_0)
enableVersionIfAllowed(ApiVersion.v5_1_0)
enableVersionIfAllowed(ApiVersion.v6_0_0)
enableVersionIfAllowed(ApiVersion.`dynamic-endpoint`)
enableVersionIfAllowed(ApiVersion.`dynamic-entity`)
@ -525,6 +525,7 @@ class Boot extends MdcLoggable {
LiftRules.statelessDispatch.append(ResourceDocs400)
LiftRules.statelessDispatch.append(ResourceDocs500)
LiftRules.statelessDispatch.append(ResourceDocs510)
LiftRules.statelessDispatch.append(ResourceDocs600)
////////////////////////////////////////////////////

View File

@ -27,36 +27,32 @@ TESOBE (http://www.tesobe.com/)
package code.api
import java.net.URLDecoder
import code.api.Constant._
import code.api.OAuthHandshake._
import code.api.builder.AccountInformationServiceAISApi.APIMethods_AccountInformationServiceAISApi
import code.api.util.APIUtil.{getClass, _}
import code.api.util.APIUtil._
import code.api.util.ErrorMessages.{InvalidDAuthHeaderToken, UserIsDeleted, UsernameHasBeenLocked, attemptedToOpenAnEmptyBox}
import code.api.util._
import code.api.v3_0_0.APIMethods300
import code.api.v3_1_0.APIMethods310
import code.api.v4_0_0.{APIMethods400, OBPAPI4_0_0}
import code.api.v4_0_0.OBPAPI4_0_0
import code.api.v5_0_0.OBPAPI5_0_0
import code.api.v5_1_0.OBPAPI5_1_0
import code.api.v6_0_0.OBPAPI6_0_0
import code.loginattempts.LoginAttempt
import code.model.dataAccess.AuthUser
import code.util.Helper.{MdcLoggable, ObpS}
import com.alibaba.ttl.TransmittableThreadLocal
import com.openbankproject.commons.model.ErrorMessage
import com.openbankproject.commons.util.{ApiVersion, ReflectUtils, ScannedApiVersion}
import net.liftweb.common.{Box, Full, _}
import net.liftweb.common._
import net.liftweb.http.rest.RestHelper
import net.liftweb.http.{JsonResponse, LiftResponse, LiftRules, Req, S, TransientRequestMemoize}
import net.liftweb.json.Extraction
import net.liftweb.json.JsonAST.JValue
import net.liftweb.util.{Helpers, NamedPF, Props, ThreadGlobal}
import net.liftweb.util.Helpers.tryo
import net.liftweb.util.{Helpers, NamedPF, Props, ThreadGlobal}
import java.net.URLDecoder
import java.util.{Locale, ResourceBundle}
import scala.collection.immutable.List
import scala.collection.mutable.ArrayBuffer
import scala.math.Ordering
import scala.util.control.NoStackTrace
import scala.xml.{Node, NodeSeq}
@ -642,15 +638,32 @@ trait OBPRestHelper extends RestHelper with MdcLoggable {
result
}
def isAutoValidate(doc: ResourceDoc, autoValidateAll: Boolean): Boolean = { //note: auto support v4.0.0 and later versions
doc.isValidateEnabled || (autoValidateAll && !doc.isValidateDisabled && {
// Auto support v4.0.0 and all later versions
val docVersion = doc.implementedInApiVersion
// Check if the version is v4.0.0 or later by comparing the version string
docVersion match {
case v: ScannedApiVersion =>
// Extract version numbers and compare
val versionStr = v.apiShortVersion.replace("v", "")
val parts = versionStr.split("\\.")
if (parts.length >= 2) {
val major = parts(0).toInt
val minor = parts(1).toInt
major > 4 || (major == 4 && minor >= 0)
} else {
false
}
case _ => false
}
})
}
protected def registerRoutes(routes: List[OBPEndpoint],
allResourceDocs: ArrayBuffer[ResourceDoc],
apiPrefix:OBPEndpoint => OBPEndpoint,
autoValidateAll: Boolean = false): Unit = {
def isAutoValidate(doc: ResourceDoc): Boolean = { //note: only support v5.1.0, v5.0.0 and v4.0.0 at the moment.
doc.isValidateEnabled || (autoValidateAll && !doc.isValidateDisabled && List(OBPAPI5_1_0.version,OBPAPI5_0_0.version,OBPAPI4_0_0.version).contains(doc.implementedInApiVersion))
}
for(route <- routes) {
// one endpoint can have multiple ResourceDocs, so here use filter instead of find, e.g APIMethods400.Implementations400.createTransactionRequest
val resourceDocs = allResourceDocs.filter(_.partialFunction == route)
@ -658,7 +671,7 @@ trait OBPRestHelper extends RestHelper with MdcLoggable {
if(resourceDocs.isEmpty) {
oauthServe(apiPrefix(route), None)
} else {
val (autoValidateDocs, other) = resourceDocs.partition(isAutoValidate)
val (autoValidateDocs, other) = resourceDocs.partition(isAutoValidate(_, autoValidateAll))
// autoValidateAll or doc isAutoValidate, just wrapped to auth check endpoint
autoValidateDocs.foreach { doc =>
val wrappedEndpoint = doc.wrappedWithAuthCheck(route)

View File

@ -1,8 +1,8 @@
package code.api.ResourceDocs1_4_0
import code.api.OBPRestHelper
import com.openbankproject.commons.util.{ApiVersion,ApiVersionStatus}
import code.util.Helper.MdcLoggable
import com.openbankproject.commons.util.{ApiVersion, ApiVersionStatus}
object ResourceDocs140 extends OBPRestHelper with ResourceDocsAPIMethods with MdcLoggable {
@ -136,5 +136,21 @@ object ResourceDocs300 extends OBPRestHelper with ResourceDocsAPIMethods with Md
})
})
}
object ResourceDocs600 extends OBPRestHelper with ResourceDocsAPIMethods with MdcLoggable {
val version: ApiVersion = ApiVersion.v6_0_0
val versionStatus = ApiVersionStatus.BLEEDING_EDGE.toString
val routes = List(
ImplementationsResourceDocs.getResourceDocsObpV400,
ImplementationsResourceDocs.getResourceDocsSwagger,
ImplementationsResourceDocs.getBankLevelDynamicResourceDocsObp,
// ImplementationsResourceDocs.getStaticResourceDocsObp
)
routes.foreach(route => {
oauthServe(apiPrefix {
route
})
})
}
}

View File

@ -16,6 +16,7 @@ import code.api.v3_1_0._
import code.api.v4_0_0._
import code.api.v5_0_0._
import code.api.v5_1_0._
import code.api.v6_0_0._
import code.branches.Branches.{Branch, DriveUpString, LobbyString}
import code.connectormethod.{JsonConnectorMethod, JsonConnectorMethodMethodBody}
import code.consent.ConsentStatus
@ -5697,6 +5698,47 @@ object SwaggerDefinitionsJSON {
permission_name = CAN_GRANT_ACCESS_TO_VIEWS,
extra_data = Some(List(SYSTEM_ACCOUNTANT_VIEW_ID, SYSTEM_AUDITOR_VIEW_ID))
)
lazy val cardanoPaymentJsonV600 = CardanoPaymentJsonV600(
address = "addr_test1qpv3se9ghq87ud29l0a8asy8nlqwd765e5zt4rc2z4mktqulwagn832cuzcjknfyxwzxz2p2kumx6n58tskugny6mrqs7fd12",
amount = CardanoAmountJsonV600(
quantity = 1000000,
unit = "lovelace"
),
assets = Some(List(CardanoAssetJsonV600(
policy_id = "policy1234567890abcdef",
asset_name = "4f47435241",
quantity = 10
)))
)
// Example for Send ADA with Token only (no ADA amount)
lazy val cardanoPaymentTokenOnlyJsonV510 = CardanoPaymentJsonV600(
address = "addr_test1qpv3se9ghq87ud29l0a8asy8nlqwd765e5zt4rc2z4mktqulwagn832cuzcjknfyxwzxz2p2kumx6n58tskugny6mrqs7fd12",
amount = CardanoAmountJsonV600(
quantity = 0,
unit = "lovelace"
),
assets = Some(List(CardanoAssetJsonV600(
policy_id = "policy1234567890abcdef",
asset_name = "4f47435241",
quantity = 10
)))
)
lazy val cardanoMetadataStringJsonV600 = CardanoMetadataStringJsonV600(
string = "Hello Cardano"
)
lazy val transactionRequestBodyCardanoJsonV600 = TransactionRequestBodyCardanoJsonV600(
to = cardanoPaymentJsonV600,
value = amountOfMoneyJsonV121,
passphrase = "password1234!",
description = descriptionExample.value,
metadata = Some(Map("202507022319" -> cardanoMetadataStringJsonV600))
)
//The common error or success format.
//Just some helper format to use in Json
case class NotSupportedYet()

View File

@ -71,7 +71,6 @@ import code.util.{Helper, JsonSchemaUtil}
import code.views.system.AccountAccess
import code.views.{MapperViews, Views}
import code.webuiprops.MappedWebUiPropsProvider.getWebUiPropsValue
import javassist.CannotCompileException
import com.github.dwickern.macros.NameOf.{nameOf, nameOfType}
import com.openbankproject.commons.ExecutionContext.Implicits.global
import com.openbankproject.commons.model._
@ -81,7 +80,7 @@ import com.openbankproject.commons.util.Functions.Implicits._
import com.openbankproject.commons.util._
import dispatch.url
import javassist.expr.{ExprEditor, MethodCall}
import javassist.{ClassPool, LoaderClassPath}
import javassist.{CannotCompileException, ClassPool, LoaderClassPath}
import net.liftweb.actor.LAFuture
import net.liftweb.common._
import net.liftweb.http._
@ -2747,6 +2746,7 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{
case ApiVersion.v4_0_0 => LiftRules.statelessDispatch.append(v4_0_0.OBPAPI4_0_0)
case ApiVersion.v5_0_0 => LiftRules.statelessDispatch.append(v5_0_0.OBPAPI5_0_0)
case ApiVersion.v5_1_0 => LiftRules.statelessDispatch.append(v5_1_0.OBPAPI5_1_0)
case ApiVersion.v6_0_0 => LiftRules.statelessDispatch.append(v6_0_0.OBPAPI6_0_0)
case ApiVersion.`dynamic-endpoint` => LiftRules.statelessDispatch.append(OBPAPIDynamicEndpoint)
case ApiVersion.`dynamic-entity` => LiftRules.statelessDispatch.append(OBPAPIDynamicEntity)
case version: ScannedApiVersion => LiftRules.statelessDispatch.append(ScannedApis.versionMapScannedApis(version))

View File

@ -79,7 +79,7 @@ object ErrorMessages {
// General messages (OBP-10XXX)
val InvalidJsonFormat = "OBP-10001: Incorrect json format."
val InvalidNumber = "OBP-10002: Invalid Number. Could not convert value to a number."
val InvalidISOCurrencyCode = "OBP-10003: Invalid Currency Value. It should be three letters ISO Currency Code. "
val InvalidISOCurrencyCode = """OBP-10003: Invalid Currency Value. Expected a 3-letter ISO Currency Code (e.g., 'USD', 'EUR') or 'lovelace' for Cardano transactions.""".stripMargin
val FXCurrencyCodeCombinationsNotSupported = "OBP-10004: ISO Currency code combination not supported for FX. Please modify the FROM_CURRENCY_CODE or TO_CURRENCY_CODE. "
val InvalidDateFormat = "OBP-10005: Invalid Date Format. Could not convert value to a Date."
val InvalidCurrency = "OBP-10006: Invalid Currency Value."

View File

@ -769,17 +769,37 @@ object NewStyle extends MdcLoggable{
def isEnabledTransactionRequests(callContext: Option[CallContext]): Future[Box[Unit]] = Helper.booleanToFuture(failMsg = TransactionRequestsNotEnabled, cc=callContext)(APIUtil.getPropsAsBoolValue("transactionRequests_enabled", false))
/**
* Wraps a Future("try") block around the function f and
* @param f - the block of code to evaluate
* @return <ul>
* <li>Future(result of the evaluation of f) if f doesn't throw any exception
* <li>a Failure if f throws an exception with message = failMsg and code = failCode
* </ul>
*/
def tryons[T](failMsg: String, failCode: Int = 400, callContext: Option[CallContext])(f: => T)(implicit m: Manifest[T]): Future[T]= {
* Wraps a computation `f` in a Future, capturing exceptions and returning detailed error messages.
*
* @param failMsg Base error message to return if the computation fails.
* @param failCode HTTP status code to return on failure (default: 400).
* @param callContext Optional call context for logging or metadata.
* @param f The computation to execute (call-by-name to defer evaluation).
* @param m Implicit Manifest for type `T` (handled by Scala compiler).
* @return Future[T] Success: Result of `f`; Failure: Detailed error message.
*/
def tryons[T](
failMsg: String,
failCode: Int = 400,
callContext: Option[CallContext]
)(f: => T)(implicit m: Manifest[T]): Future[T] = {
Future {
tryo {
f
try {
// Attempt to execute `f` and wrap the result in `Full` (success) or `Failure` (error)
tryo(f) match {
case Full(result) =>
Full(result) // Success: Forward the result
case Failure(msg, _, _) =>
// `tryo` encountered an exception (e.g., validation error)
Failure(s"$failMsg. Details: $msg", Empty, Empty)
case Empty =>
// Edge case: Empty result (unlikely but handled defensively)
Failure(s"$failMsg. Details: Empty result", Empty, Empty)
}
} catch {
case e: Exception =>
// Directly caught exception (e.g., JSON parsing error)
Failure(s"$failMsg. Details: ${e.getMessage}", Full(e), Empty)
}
} map {
x => unboxFullOrFail(x, callContext, failMsg, failCode)

View File

@ -86,6 +86,7 @@ object Migration extends MdcLoggable {
addFastFirehoseAccountsView(startedBeforeSchemifier)
addFastFirehoseAccountsMaterializedView(startedBeforeSchemifier)
alterUserAuthContextColumnKeyAndValueLength(startedBeforeSchemifier)
alterMappedTransactionRequestFieldsLengthMigration(startedBeforeSchemifier)
dropIndexAtColumnUsernameAtTableAuthUser(startedBeforeSchemifier)
dropIndexAtUserAuthContext()
alterWebhookColumnUrlLength()
@ -403,6 +404,19 @@ object Migration extends MdcLoggable {
}
}
}
private def alterMappedTransactionRequestFieldsLengthMigration(startedBeforeSchemifier: Boolean): Boolean = {
if(startedBeforeSchemifier == true) {
logger.warn(s"Migration.database.alterMappedTransactionRequestFieldsLengthMigration(true) cannot be run before Schemifier.")
true
} else {
val name = nameOf(alterMappedTransactionRequestFieldsLengthMigration(startedBeforeSchemifier))
runOnce(name) {
MigrationOfMappedTransactionRequestFieldsLength.alterMappedTransactionRequestFieldsLength(name)
}
}
}
private def dropIndexAtColumnUsernameAtTableAuthUser(startedBeforeSchemifier: Boolean): Boolean = {
if(startedBeforeSchemifier == true) {
logger.warn(s"Migration.database.dropIndexAtColumnUsernameAtTableAuthUser(true) cannot be run before Schemifier.")

View File

@ -0,0 +1,74 @@
package code.api.util.migration
import code.api.util.APIUtil
import code.api.util.migration.Migration.{DbFunction, saveLog}
import code.transactionrequests.MappedTransactionRequest
import net.liftweb.common.Full
import net.liftweb.mapper.Schemifier
import java.time.format.DateTimeFormatter
import java.time.{ZoneId, ZonedDateTime}
object MigrationOfMappedTransactionRequestFieldsLength {
val oneDayAgo = ZonedDateTime.now(ZoneId.of("UTC")).minusDays(1)
val oneYearInFuture = ZonedDateTime.now(ZoneId.of("UTC")).plusYears(1)
val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm'Z'")
def alterMappedTransactionRequestFieldsLength(name: String): Boolean = {
DbFunction.tableExists(MappedTransactionRequest) match {
case true =>
val startDate = System.currentTimeMillis()
val commitId: String = APIUtil.gitCommit
var isSuccessful = false
val executedSql =
DbFunction.maybeWrite(true, Schemifier.infoF _) {
APIUtil.getPropsValue("db.driver") match {
case Full(dbDriver) if dbDriver.contains("com.microsoft.sqlserver.jdbc.SQLServerDriver") =>
() =>
s"""
|-- Currency fields: support longer currency names (e.g., "lovelace")
|ALTER TABLE MappedTransactionRequest ALTER COLUMN mCharge_Currency varchar(16);
|ALTER TABLE MappedTransactionRequest ALTER COLUMN mBody_Value_Currency varchar(16);
|
|-- Account routing fields: support Cardano addresses (108 characters)
|ALTER TABLE MappedTransactionRequest ALTER COLUMN mTo_AccountId varchar(128);
|ALTER TABLE MappedTransactionRequest ALTER COLUMN mOtherAccountRoutingAddress varchar(128);
|""".stripMargin
case _ =>
() =>
"""
|-- Currency fields: support longer currency names (e.g., "lovelace")
|ALTER TABLE MappedTransactionRequest ALTER COLUMN mCharge_Currency TYPE varchar(16);
|ALTER TABLE MappedTransactionRequest ALTER COLUMN mBody_Value_Currency TYPE varchar(16);
|
|-- Account routing fields: support Cardano addresses (108 characters)
|ALTER TABLE MappedTransactionRequest ALTER COLUMN mTo_AccountId TYPE varchar(128);
|ALTER TABLE MappedTransactionRequest ALTER COLUMN mOtherAccountRoutingAddress TYPE varchar(128);
|""".stripMargin
}
}
val endDate = System.currentTimeMillis()
val comment: String =
s"""Executed SQL:
|$executedSql
|""".stripMargin
isSuccessful = true
saveLog(name, commitId, isSuccessful, startDate, endDate, comment)
isSuccessful
case false =>
val startDate = System.currentTimeMillis()
val commitId: String = APIUtil.gitCommit
val isSuccessful = false
val endDate = System.currentTimeMillis()
val comment: String =
s"""${MappedTransactionRequest._dbTableNameLC} table does not exist""".stripMargin
saveLog(name, commitId, isSuccessful, startDate, endDate, comment)
isSuccessful
}
}
}

View File

@ -28,19 +28,18 @@ import code.api.util.newstyle.UserCustomerLinkNewStyle.getUserCustomerLinks
import code.api.util.newstyle.{BalanceNewStyle, UserCustomerLinkNewStyle, ViewNewStyle}
import code.api.v1_2_1.{JSONFactory, PostTransactionTagJSON}
import code.api.v1_4_0.JSONFactory1_4_0
import code.api.v1_4_0.JSONFactory1_4_0.TransactionRequestAccountJsonV140
import code.api.v2_0_0.OBPAPI2_0_0.Implementations2_0_0
import code.api.v2_0_0.{CreateEntitlementJSON, CreateUserCustomerLinkJson, EntitlementJSONs, JSONFactory200}
import code.api.v2_1_0._
import code.api.v3_0_0.{CreateScopeJson, JSONFactory300}
import code.api.v3_1_0._
import code.api.v4_0_0.APIMethods400.{createTransactionRequest, transactionRequestGeneralText}
import code.api.v4_0_0.JSONFactory400._
import code.api.{ChargePolicy, Constant, JsonResponseException}
import code.api.{Constant, JsonResponseException}
import code.apicollection.MappedApiCollectionsProvider
import code.apicollectionendpoint.MappedApiCollectionEndpointsProvider
import code.authtypevalidation.JsonAuthTypeValidation
import code.bankconnectors.{Connector, DynamicConnector, InternalConnector}
import code.bankconnectors.LocalMappedConnectorInternal._
import code.bankconnectors.{Connector, DynamicConnector, InternalConnector, LocalMappedConnectorInternal}
import code.connectormethod.{JsonConnectorMethod, JsonConnectorMethodMethodBody}
import code.consent.{ConsentStatus, Consents}
import code.dynamicEntity.{DynamicEntityCommons, ReferenceType}
@ -48,7 +47,6 @@ import code.dynamicMessageDoc.JsonDynamicMessageDoc
import code.dynamicResourceDoc.JsonDynamicResourceDoc
import code.endpointMapping.EndpointMappingCommons
import code.entitlement.Entitlement
import code.fx.fx
import code.loginattempts.LoginAttempt
import code.metadata.counterparties.{Counterparties, MappedCounterparty}
import code.metadata.tags.Tags
@ -71,7 +69,6 @@ import com.networknt.schema.ValidationMessage
import com.openbankproject.commons.ExecutionContext.Implicits.global
import com.openbankproject.commons.dto.GetProductsParam
import com.openbankproject.commons.model._
import com.openbankproject.commons.model.enums.ChallengeType.OBP_TRANSACTION_REQUEST_CHALLENGE
import com.openbankproject.commons.model.enums.DynamicEntityOperation._
import com.openbankproject.commons.model.enums.TransactionRequestTypes._
import com.openbankproject.commons.model.enums.{TransactionRequestStatus, _}
@ -81,7 +78,6 @@ import net.liftweb.common._
import net.liftweb.http.rest.RestHelper
import net.liftweb.json.JsonAST.JValue
import net.liftweb.json.JsonDSL._
import net.liftweb.json.Serialization.write
import net.liftweb.json._
import net.liftweb.util.Helpers.{now, tryo}
import net.liftweb.util.{Helpers, StringHelpers}
@ -89,7 +85,6 @@ import org.apache.commons.lang3.StringUtils
import java.net.URLEncoder
import java.text.SimpleDateFormat
import java.time.{LocalDate, ZoneId}
import java.util
import java.util.{Calendar, Date}
import scala.collection.immutable.{List, Nil}
@ -899,7 +894,7 @@ trait APIMethods400 extends MdcLoggable {
cc =>
implicit val ec = EndpointContext(Some(cc))
val transactionRequestType = TransactionRequestType("AGENT_CASH_WITHDRAWAL")
createTransactionRequest(bankId, accountId, viewId, transactionRequestType, json)
LocalMappedConnectorInternal.createTransactionRequest(bankId, accountId, viewId, transactionRequestType, json)
}
lazy val createTransactionRequestAccount: OBPEndpoint = {
@ -907,7 +902,7 @@ trait APIMethods400 extends MdcLoggable {
"ACCOUNT" :: "transaction-requests" :: Nil JsonPost json -> _ =>
cc => implicit val ec = EndpointContext(Some(cc))
val transactionRequestType = TransactionRequestType("ACCOUNT")
createTransactionRequest(bankId, accountId, viewId , transactionRequestType, json)
LocalMappedConnectorInternal.createTransactionRequest(bankId, accountId, viewId , transactionRequestType, json)
}
lazy val createTransactionRequestAccountOtp: OBPEndpoint = {
@ -915,7 +910,7 @@ trait APIMethods400 extends MdcLoggable {
"ACCOUNT_OTP" :: "transaction-requests" :: Nil JsonPost json -> _ =>
cc => implicit val ec = EndpointContext(Some(cc))
val transactionRequestType = TransactionRequestType("ACCOUNT_OTP")
createTransactionRequest(bankId, accountId, viewId , transactionRequestType, json)
LocalMappedConnectorInternal.createTransactionRequest(bankId, accountId, viewId , transactionRequestType, json)
}
lazy val createTransactionRequestSepa: OBPEndpoint = {
@ -923,7 +918,7 @@ trait APIMethods400 extends MdcLoggable {
"SEPA" :: "transaction-requests" :: Nil JsonPost json -> _ =>
cc => implicit val ec = EndpointContext(Some(cc))
val transactionRequestType = TransactionRequestType("SEPA")
createTransactionRequest(bankId, accountId, viewId , transactionRequestType, json)
LocalMappedConnectorInternal.createTransactionRequest(bankId, accountId, viewId , transactionRequestType, json)
}
lazy val createTransactionRequestCounterparty: OBPEndpoint = {
@ -931,7 +926,7 @@ trait APIMethods400 extends MdcLoggable {
"COUNTERPARTY" :: "transaction-requests" :: Nil JsonPost json -> _ =>
cc => implicit val ec = EndpointContext(Some(cc))
val transactionRequestType = TransactionRequestType("COUNTERPARTY")
createTransactionRequest(bankId, accountId, viewId , transactionRequestType, json)
LocalMappedConnectorInternal.createTransactionRequest(bankId, accountId, viewId , transactionRequestType, json)
}
lazy val createTransactionRequestRefund: OBPEndpoint = {
@ -939,7 +934,7 @@ trait APIMethods400 extends MdcLoggable {
"REFUND" :: "transaction-requests" :: Nil JsonPost json -> _ =>
cc => implicit val ec = EndpointContext(Some(cc))
val transactionRequestType = TransactionRequestType("REFUND")
createTransactionRequest(bankId, accountId, viewId , transactionRequestType, json)
LocalMappedConnectorInternal.createTransactionRequest(bankId, accountId, viewId , transactionRequestType, json)
}
lazy val createTransactionRequestFreeForm: OBPEndpoint = {
@ -947,7 +942,7 @@ trait APIMethods400 extends MdcLoggable {
"FREE_FORM" :: "transaction-requests" :: Nil JsonPost json -> _ =>
cc => implicit val ec = EndpointContext(Some(cc))
val transactionRequestType = TransactionRequestType("FREE_FORM")
createTransactionRequest(bankId, accountId, viewId , transactionRequestType, json)
LocalMappedConnectorInternal.createTransactionRequest(bankId, accountId, viewId , transactionRequestType, json)
}
lazy val createTransactionRequestSimple: OBPEndpoint = {
@ -955,7 +950,7 @@ trait APIMethods400 extends MdcLoggable {
"SIMPLE" :: "transaction-requests" :: Nil JsonPost json -> _ =>
cc => implicit val ec = EndpointContext(Some(cc))
val transactionRequestType = TransactionRequestType("SIMPLE")
createTransactionRequest(bankId, accountId, viewId , transactionRequestType, json)
LocalMappedConnectorInternal.createTransactionRequest(bankId, accountId, viewId , transactionRequestType, json)
}
@ -1001,7 +996,7 @@ trait APIMethods400 extends MdcLoggable {
case "transaction-request-types" :: "CARD" :: "transaction-requests" :: Nil JsonPost json -> _ =>
cc => implicit val ec = EndpointContext(Some(cc))
val transactionRequestType = TransactionRequestType("CARD")
createTransactionRequest(BankId(""), AccountId(""), ViewId(Constant.SYSTEM_OWNER_VIEW_ID), transactionRequestType, json)
LocalMappedConnectorInternal.createTransactionRequest(BankId(""), AccountId(""), ViewId(Constant.SYSTEM_OWNER_VIEW_ID), transactionRequestType, json)
}
@ -12216,677 +12211,6 @@ object APIMethods400 extends RestHelper with APIMethods400 {
lazy val newStyleEndpoints: List[(String, String)] = Implementations4_0_0.resourceDocs.map {
rd => (rd.partialFunctionName, rd.implementedInApiVersion.toString())
}.toList
// This text is used in the various Create Transaction Request resource docs
val transactionRequestGeneralText =
s"""
|
|For an introduction to Transaction Requests, see: ${Glossary.getGlossaryItemLink("Transaction-Request-Introduction")}
|
|""".stripMargin
val lowAmount = AmountOfMoneyJsonV121("EUR", "12.50")
val sharedChargePolicy = ChargePolicy.withName("SHARED")
def createTransactionRequest(bankId: BankId, accountId: AccountId, viewId: ViewId, transactionRequestType: TransactionRequestType, json: JValue): Future[(TransactionRequestWithChargeJSON400, Option[CallContext])] = {
for {
(Full(u), callContext) <- SS.user
transactionRequestTypeValue <- NewStyle.function.tryons(s"$InvalidTransactionRequestType: '${transactionRequestType.value}'. OBP does not support it.", 400, callContext) {
TransactionRequestTypes.withName(transactionRequestType.value)
}
(fromAccount, callContext) <- transactionRequestTypeValue match {
case CARD =>
for{
transactionRequestBodyCard <- NewStyle.function.tryons(s"${InvalidJsonFormat}, it should be $CARD json format", 400, callContext) {
json.extract[TransactionRequestBodyCardJsonV400]
}
// 1.1 get Card from card_number
(cardFromCbs,callContext) <- NewStyle.function.getPhysicalCardByCardNumber(transactionRequestBodyCard.card.card_number, callContext)
// 1.2 check card name/expire month. year.
calendar = Calendar.getInstance
_ = calendar.setTime(cardFromCbs.expires)
yearFromCbs = calendar.get(Calendar.YEAR).toString
monthFromCbs = calendar.get(Calendar.MONTH).toString
nameOnCardFromCbs= cardFromCbs.nameOnCard
cvvFromCbs= cardFromCbs.cvv.getOrElse("")
brandFromCbs= cardFromCbs.brand.getOrElse("")
_ <- Helper.booleanToFuture(s"$InvalidJsonValue brand is not matched", cc=callContext) {
transactionRequestBodyCard.card.brand.equalsIgnoreCase(brandFromCbs)
}
dateFromJsonBody <- NewStyle.function.tryons(s"$InvalidDateFormat year should be 'yyyy', " +
s"eg: 2023, but current expiry_year(${transactionRequestBodyCard.card.expiry_year}), " +
s"month should be 'xx', eg: 02, but current expiry_month(${transactionRequestBodyCard.card.expiry_month})", 400, callContext) {
DateWithMonthFormat.parse(s"${transactionRequestBodyCard.card.expiry_year}-${transactionRequestBodyCard.card.expiry_month}")
}
_ <- Helper.booleanToFuture(s"$InvalidJsonValue your credit card is expired.", cc=callContext) {
org.apache.commons.lang3.time.DateUtils.addMonths(new Date(), 1).before(dateFromJsonBody)
}
_ <- Helper.booleanToFuture(s"$InvalidJsonValue expiry_year is not matched", cc=callContext) {
transactionRequestBodyCard.card.expiry_year.equalsIgnoreCase(yearFromCbs)
}
_ <- Helper.booleanToFuture(s"$InvalidJsonValue expiry_month is not matched", cc=callContext) {
transactionRequestBodyCard.card.expiry_month.toInt.equals(monthFromCbs.toInt+1)
}
_ <- Helper.booleanToFuture(s"$InvalidJsonValue name_on_card is not matched", cc=callContext) {
transactionRequestBodyCard.card.name_on_card.equalsIgnoreCase(nameOnCardFromCbs)
}
_ <- Helper.booleanToFuture(s"$InvalidJsonValue cvv is not matched", cc=callContext) {
HashUtil.Sha256Hash(transactionRequestBodyCard.card.cvv).equals(cvvFromCbs)
}
} yield{
(cardFromCbs.account, callContext)
}
case _ => NewStyle.function.getBankAccount(bankId,accountId, callContext)
}
_ <- NewStyle.function.isEnabledTransactionRequests(callContext)
_ <- Helper.booleanToFuture(InvalidAccountIdFormat, cc=callContext) {
isValidID(fromAccount.accountId.value)
}
_ <- Helper.booleanToFuture(InvalidBankIdFormat, cc=callContext) {
isValidID(fromAccount.bankId.value)
}
_ <- NewStyle.function.checkAuthorisationToCreateTransactionRequest(viewId, BankIdAccountId(fromAccount.bankId, fromAccount.accountId), u, callContext)
_ <- Helper.booleanToFuture(s"${InvalidTransactionRequestType}: '${transactionRequestType.value}'. Current Sandbox does not support it. ", cc=callContext) {
APIUtil.getPropsValue("transactionRequests_supported_types", "").split(",").contains(transactionRequestType.value)
}
// Check the input JSON format, here is just check the common parts of all four types
transDetailsJson <- NewStyle.function.tryons(s"$InvalidJsonFormat The Json body should be the $TransactionRequestBodyCommonJSON ", 400, callContext) {
json.extract[TransactionRequestBodyCommonJSON]
}
transactionAmountNumber <- NewStyle.function.tryons(s"$InvalidNumber Current input is ${transDetailsJson.value.amount} ", 400, callContext) {
BigDecimal(transDetailsJson.value.amount)
}
_ <- Helper.booleanToFuture(s"${NotPositiveAmount} Current input is: '${transactionAmountNumber}'", cc=callContext) {
transactionAmountNumber > BigDecimal("0")
}
_ <- Helper.booleanToFuture(s"${InvalidISOCurrencyCode} Current input is: '${transDetailsJson.value.currency}'", cc=callContext) {
APIUtil.isValidCurrencyISOCode(transDetailsJson.value.currency)
}
(createdTransactionRequest, callContext) <- transactionRequestTypeValue match {
case REFUND => {
for {
transactionRequestBodyRefundJson <- NewStyle.function.tryons(s"${InvalidJsonFormat}, it should be $ACCOUNT json format", 400, callContext) {
json.extract[TransactionRequestBodyRefundJsonV400]
}
transactionId = TransactionId(transactionRequestBodyRefundJson.refund.transaction_id)
(fromAccount, toAccount, transaction, callContext) <- transactionRequestBodyRefundJson.to match {
case Some(refundRequestTo) if refundRequestTo.account_id.isDefined && refundRequestTo.bank_id.isDefined =>
val toBankId = BankId(refundRequestTo.bank_id.get)
val toAccountId = AccountId(refundRequestTo.account_id.get)
for {
(transaction, callContext) <- NewStyle.function.getTransaction(fromAccount.bankId, fromAccount.accountId, transactionId, callContext)
(toAccount, callContext) <- NewStyle.function.checkBankAccountExists(toBankId, toAccountId, callContext)
} yield (fromAccount, toAccount, transaction, callContext)
case Some(refundRequestTo) if refundRequestTo.counterparty_id.isDefined =>
val toCounterpartyId = CounterpartyId(refundRequestTo.counterparty_id.get)
for {
(toCounterparty, callContext) <- NewStyle.function.getCounterpartyByCounterpartyId(toCounterpartyId, callContext)
(toAccount, callContext) <- NewStyle.function.getBankAccountFromCounterparty(toCounterparty, isOutgoingAccount = true, callContext)
_ <- Helper.booleanToFuture(s"$CounterpartyBeneficiaryPermit", cc=callContext) {
toCounterparty.isBeneficiary
}
(transaction, callContext) <- NewStyle.function.getTransaction(fromAccount.bankId, fromAccount.accountId, transactionId, callContext)
} yield (fromAccount, toAccount, transaction, callContext)
case None if transactionRequestBodyRefundJson.from.isDefined =>
val fromCounterpartyId = CounterpartyId(transactionRequestBodyRefundJson.from.get.counterparty_id)
val toAccount = fromAccount
for {
(fromCounterparty, callContext) <- NewStyle.function.getCounterpartyByCounterpartyId(fromCounterpartyId, callContext)
(fromAccount, callContext) <- NewStyle.function.getBankAccountFromCounterparty(fromCounterparty, isOutgoingAccount = false, callContext)
_ <- Helper.booleanToFuture(s"$CounterpartyBeneficiaryPermit", cc=callContext) {
fromCounterparty.isBeneficiary
}
(transaction, callContext) <- NewStyle.function.getTransaction(toAccount.bankId, toAccount.accountId, transactionId, callContext)
} yield (fromAccount, toAccount, transaction, callContext)
}
transDetailsSerialized <- NewStyle.function.tryons(UnknownError, 400, callContext) {
write(transactionRequestBodyRefundJson)(Serialization.formats(NoTypeHints))
}
_ <- Helper.booleanToFuture(s"${RefundedTransaction} Current input amount is: '${transDetailsJson.value.amount}'. It can not be more than the original amount(${(transaction.amount).abs})", cc=callContext) {
(transaction.amount).abs >= transactionAmountNumber
}
//TODO, we need additional field to guarantee the transaction is refunded...
// _ <- Helper.booleanToFuture(s"${RefundedTransaction}") {
// !((transaction.description.toString contains(" Refund to ")) && (transaction.description.toString contains(" and transaction_id(")))
// }
//we add the extra info (counterparty name + transaction_id) for this special Refund endpoint.
newDescription = s"${transactionRequestBodyRefundJson.description} - Refund for transaction_id: (${transactionId.value}) to ${transaction.otherAccount.counterpartyName}"
//This is the refund endpoint, the original fromAccount is the `toAccount` which will receive money.
refundToAccount = fromAccount
//This is the refund endpoint, the original toAccount is the `fromAccount` which will lose money.
refundFromAccount = toAccount
(createdTransactionRequest, callContext) <- NewStyle.function.createTransactionRequestv400(u,
viewId,
refundFromAccount,
refundToAccount,
transactionRequestType,
transactionRequestBodyRefundJson.copy(description = newDescription),
transDetailsSerialized,
sharedChargePolicy.toString,
Some(OBP_TRANSACTION_REQUEST_CHALLENGE),
getScaMethodAtInstance(transactionRequestType.value).toOption,
None,
callContext) //in ACCOUNT, ChargePolicy set default "SHARED"
_ <- NewStyle.function.createOrUpdateTransactionRequestAttribute(
bankId = bankId,
transactionRequestId = createdTransactionRequest.id,
transactionRequestAttributeId = None,
name = "original_transaction_id",
attributeType = TransactionRequestAttributeType.withName("STRING"),
value = transactionId.value,
callContext = callContext
)
refundReasonCode = transactionRequestBodyRefundJson.refund.reason_code
_ <- if (refundReasonCode.nonEmpty) {
NewStyle.function.createOrUpdateTransactionRequestAttribute(
bankId = bankId,
transactionRequestId = createdTransactionRequest.id,
transactionRequestAttributeId = None,
name = "refund_reason_code",
attributeType = TransactionRequestAttributeType.withName("STRING"),
value = refundReasonCode,
callContext = callContext)
} else Future.successful()
(newTransactionRequestStatus, callContext) <- NewStyle.function.notifyTransactionRequest(refundFromAccount, refundToAccount, createdTransactionRequest, callContext)
_ <- NewStyle.function.saveTransactionRequestStatusImpl(createdTransactionRequest.id, newTransactionRequestStatus.toString, callContext)
createdTransactionRequest <- Future(createdTransactionRequest.copy(status = newTransactionRequestStatus.toString))
} yield (createdTransactionRequest, callContext)
}
case ACCOUNT | SANDBOX_TAN => {
for {
transactionRequestBodySandboxTan <- NewStyle.function.tryons(s"${InvalidJsonFormat}, it should be $ACCOUNT json format", 400, callContext) {
json.extract[TransactionRequestBodySandBoxTanJSON]
}
toBankId = BankId(transactionRequestBodySandboxTan.to.bank_id)
toAccountId = AccountId(transactionRequestBodySandboxTan.to.account_id)
(toAccount, callContext) <- NewStyle.function.checkBankAccountExists(toBankId, toAccountId, callContext)
transDetailsSerialized <- NewStyle.function.tryons(UnknownError, 400, callContext) {
write(transactionRequestBodySandboxTan)(Serialization.formats(NoTypeHints))
}
(createdTransactionRequest, callContext) <- NewStyle.function.createTransactionRequestv400(u,
viewId,
fromAccount,
toAccount,
transactionRequestType,
transactionRequestBodySandboxTan,
transDetailsSerialized,
sharedChargePolicy.toString,
Some(OBP_TRANSACTION_REQUEST_CHALLENGE),
getScaMethodAtInstance(transactionRequestType.value).toOption,
None,
callContext) //in ACCOUNT, ChargePolicy set default "SHARED"
} yield (createdTransactionRequest, callContext)
}
case ACCOUNT_OTP => {
for {
transactionRequestBodySandboxTan <- NewStyle.function.tryons(s"${InvalidJsonFormat}, it should be $ACCOUNT json format", 400, callContext) {
json.extract[TransactionRequestBodySandBoxTanJSON]
}
toBankId = BankId(transactionRequestBodySandboxTan.to.bank_id)
toAccountId = AccountId(transactionRequestBodySandboxTan.to.account_id)
(toAccount, callContext) <- NewStyle.function.checkBankAccountExists(toBankId, toAccountId, callContext)
transDetailsSerialized <- NewStyle.function.tryons(UnknownError, 400, callContext) {
write(transactionRequestBodySandboxTan)(Serialization.formats(NoTypeHints))
}
(createdTransactionRequest, callContext) <- NewStyle.function.createTransactionRequestv400(u,
viewId,
fromAccount,
toAccount,
transactionRequestType,
transactionRequestBodySandboxTan,
transDetailsSerialized,
sharedChargePolicy.toString,
Some(OBP_TRANSACTION_REQUEST_CHALLENGE),
getScaMethodAtInstance(transactionRequestType.value).toOption,
None,
callContext) //in ACCOUNT, ChargePolicy set default "SHARED"
} yield (createdTransactionRequest, callContext)
}
case COUNTERPARTY => {
for {
_ <- Future { logger.debug(s"Before extracting counterparty id") }
//For COUNTERPARTY, Use the counterpartyId to find the toCounterparty and set up the toAccount
transactionRequestBodyCounterparty <- NewStyle.function.tryons(s"${InvalidJsonFormat}, it should be $COUNTERPARTY json format", 400, callContext) {
json.extract[TransactionRequestBodyCounterpartyJSON]
}
toCounterpartyId = transactionRequestBodyCounterparty.to.counterparty_id
_ <- Future { logger.debug(s"After extracting counterparty id: $toCounterpartyId") }
(toCounterparty, callContext) <- NewStyle.function.getCounterpartyByCounterpartyId(CounterpartyId(toCounterpartyId), callContext)
transactionRequestAttributes <- if(transactionRequestBodyCounterparty.attributes.isDefined && transactionRequestBodyCounterparty.attributes.head.length > 0 ) {
val attributes = transactionRequestBodyCounterparty.attributes.head
val failMsg = s"$InvalidJsonFormat The attribute `type` field can only accept the following field: " +
s"${TransactionRequestAttributeType.DOUBLE}(12.1234)," +
s" ${TransactionRequestAttributeType.STRING}(TAX_NUMBER), " +
s"${TransactionRequestAttributeType.INTEGER}(123) and " +
s"${TransactionRequestAttributeType.DATE_WITH_DAY}(2012-04-23)"
for{
_ <- NewStyle.function.tryons(failMsg, 400, callContext) {
attributes.map(attribute => TransactionRequestAttributeType.withName(attribute.attribute_type))
}
}yield{
attributes
}
} else {
Future.successful(List.empty[TransactionRequestAttributeJsonV400])
}
(counterpartyLimitBox, callContext) <- Connector.connector.vend.getCounterpartyLimit(
bankId.value,
accountId.value,
viewId.value,
toCounterpartyId,
callContext
)
_<- if(counterpartyLimitBox.isDefined){
for{
counterpartyLimit <- Future.successful(counterpartyLimitBox.head)
maxSingleAmount = counterpartyLimit.maxSingleAmount
maxMonthlyAmount = counterpartyLimit.maxMonthlyAmount
maxNumberOfMonthlyTransactions = counterpartyLimit.maxNumberOfMonthlyTransactions
maxYearlyAmount = counterpartyLimit.maxYearlyAmount
maxNumberOfYearlyTransactions = counterpartyLimit.maxNumberOfYearlyTransactions
maxTotalAmount = counterpartyLimit.maxTotalAmount
maxNumberOfTransactions = counterpartyLimit.maxNumberOfTransactions
// Get the first day of the current month
firstDayOfMonth: LocalDate = LocalDate.now().withDayOfMonth(1)
// Get the last day of the current month
lastDayOfMonth: LocalDate = LocalDate.now().withDayOfMonth(
LocalDate.now().lengthOfMonth()
)
// Get the first day of the current year
firstDayOfYear: LocalDate = LocalDate.now().withDayOfYear(1)
// Get the last day of the current year
lastDayOfYear: LocalDate = LocalDate.now().withDayOfYear(
LocalDate.now().lengthOfYear()
)
// Convert LocalDate to Date
zoneId: ZoneId = ZoneId.systemDefault()
firstCurrentMonthDate: Date = Date.from(firstDayOfMonth.atStartOfDay(zoneId).toInstant)
// Adjust to include 23:59:59.999
lastCurrentMonthDate: Date = Date.from(
lastDayOfMonth
.atTime(23, 59, 59, 999000000)
.atZone(zoneId)
.toInstant
)
firstCurrentYearDate: Date = Date.from(firstDayOfYear.atStartOfDay(zoneId).toInstant)
// Adjust to include 23:59:59.999
lastCurrentYearDate: Date = Date.from(
lastDayOfYear
.atTime(23, 59, 59, 999000000)
.atZone(zoneId)
.toInstant
)
defaultFromDate: Date = theEpochTime
defaultToDate: Date = APIUtil.ToDateInFuture
(sumOfTransactionsFromAccountToCounterpartyMonthly, callContext) <- NewStyle.function.getSumOfTransactionsFromAccountToCounterparty(
fromAccount.bankId: BankId,
fromAccount.accountId: AccountId,
CounterpartyId(toCounterpartyId): CounterpartyId,
firstCurrentMonthDate: Date,
lastCurrentMonthDate: Date,
callContext: Option[CallContext]
)
(countOfTransactionsFromAccountToCounterpartyMonthly, callContext) <- NewStyle.function.getCountOfTransactionsFromAccountToCounterparty(
fromAccount.bankId: BankId,
fromAccount.accountId: AccountId,
CounterpartyId(toCounterpartyId): CounterpartyId,
firstCurrentMonthDate: Date,
lastCurrentMonthDate: Date,
callContext: Option[CallContext]
)
(sumOfTransactionsFromAccountToCounterpartyYearly, callContext) <- NewStyle.function.getSumOfTransactionsFromAccountToCounterparty(
fromAccount.bankId: BankId,
fromAccount.accountId: AccountId,
CounterpartyId(toCounterpartyId): CounterpartyId,
firstCurrentYearDate: Date,
lastCurrentYearDate: Date,
callContext: Option[CallContext]
)
(countOfTransactionsFromAccountToCounterpartyYearly, callContext) <- NewStyle.function.getCountOfTransactionsFromAccountToCounterparty(
fromAccount.bankId: BankId,
fromAccount.accountId: AccountId,
CounterpartyId(toCounterpartyId): CounterpartyId,
firstCurrentYearDate: Date,
lastCurrentYearDate: Date,
callContext: Option[CallContext]
)
(sumOfAllTransactionsFromAccountToCounterparty, callContext) <- NewStyle.function.getSumOfTransactionsFromAccountToCounterparty(
fromAccount.bankId: BankId,
fromAccount.accountId: AccountId,
CounterpartyId(toCounterpartyId): CounterpartyId,
defaultFromDate: Date,
defaultToDate: Date,
callContext: Option[CallContext]
)
(countOfAllTransactionsFromAccountToCounterparty, callContext) <- NewStyle.function.getCountOfTransactionsFromAccountToCounterparty(
fromAccount.bankId: BankId,
fromAccount.accountId: AccountId,
CounterpartyId(toCounterpartyId): CounterpartyId,
defaultFromDate: Date,
defaultToDate: Date,
callContext: Option[CallContext]
)
currentTransactionAmountWithFxApplied <- NewStyle.function.tryons(s"${InvalidJsonFormat}, it should be $COUNTERPARTY json format", 400, callContext) {
val fromAccountCurrency = fromAccount.currency //eg: if from account currency is EUR
val transferCurrency = transactionRequestBodyCounterparty.value.currency //eg: if the payment json body currency is GBP.
val transferAmount = BigDecimal(transactionRequestBodyCounterparty.value.amount) //eg: if the payment json body amount is 1.
val debitRate = fx.exchangeRate(transferCurrency, fromAccountCurrency, Some(fromAccount.bankId.value), callContext) //eg: the rate here is 1.16278.
fx.convert(transferAmount, debitRate) // 1.16278 Euro
}
_ <- Helper.booleanToFuture(s"$CounterpartyLimitValidationError max_single_amount is $maxSingleAmount ${fromAccount.currency}, " +
s"but current transaction body amount is ${transactionRequestBodyCounterparty.value.amount} ${transactionRequestBodyCounterparty.value.currency}, " +
s"which is $currentTransactionAmountWithFxApplied ${fromAccount.currency}. ", cc = callContext) {
maxSingleAmount >= currentTransactionAmountWithFxApplied
}
_ <- Helper.booleanToFuture(s"$CounterpartyLimitValidationError max_monthly_amount is $maxMonthlyAmount, but current monthly amount is ${BigDecimal(sumOfTransactionsFromAccountToCounterpartyMonthly.amount)+currentTransactionAmountWithFxApplied}", cc = callContext) {
maxMonthlyAmount >= BigDecimal(sumOfTransactionsFromAccountToCounterpartyMonthly.amount)+currentTransactionAmountWithFxApplied
}
_ <- Helper.booleanToFuture(s"$CounterpartyLimitValidationError max_number_of_monthly_transactions is $maxNumberOfMonthlyTransactions, but current count of monthly transactions is ${countOfTransactionsFromAccountToCounterpartyMonthly+1}", cc = callContext) {
maxNumberOfMonthlyTransactions >= countOfTransactionsFromAccountToCounterpartyMonthly+1
}
_ <- Helper.booleanToFuture(s"$CounterpartyLimitValidationError max_yearly_amount is $maxYearlyAmount, but current yearly amount is ${BigDecimal(sumOfTransactionsFromAccountToCounterpartyYearly.amount)+currentTransactionAmountWithFxApplied}", cc = callContext) {
maxYearlyAmount >= BigDecimal(sumOfTransactionsFromAccountToCounterpartyYearly.amount)+currentTransactionAmountWithFxApplied
}
result <- Helper.booleanToFuture(s"$CounterpartyLimitValidationError max_number_of_yearly_transactions is $maxNumberOfYearlyTransactions, but current count of yearly transaction is ${countOfTransactionsFromAccountToCounterpartyYearly+1}", cc = callContext) {
maxNumberOfYearlyTransactions >= countOfTransactionsFromAccountToCounterpartyYearly+1
}
_ <- Helper.booleanToFuture(s"$CounterpartyLimitValidationError max_total_amount is $maxTotalAmount, but current amount is ${BigDecimal(sumOfAllTransactionsFromAccountToCounterparty.amount)+currentTransactionAmountWithFxApplied}", cc = callContext) {
maxTotalAmount >= BigDecimal(sumOfAllTransactionsFromAccountToCounterparty.amount)+currentTransactionAmountWithFxApplied
}
result <- Helper.booleanToFuture(s"$CounterpartyLimitValidationError max_number_of_transactions is $maxNumberOfTransactions, but current count of all transactions is ${countOfAllTransactionsFromAccountToCounterparty+1}", cc = callContext) {
maxNumberOfTransactions >= countOfAllTransactionsFromAccountToCounterparty+1
}
}yield{
result
}
}
else {
Future.successful(true)
}
(toAccount, callContext) <- NewStyle.function.getBankAccountFromCounterparty(toCounterparty, true, callContext)
// Check we can send money to it.
_ <- Helper.booleanToFuture(s"$CounterpartyBeneficiaryPermit", cc=callContext) {
toCounterparty.isBeneficiary
}
chargePolicy = transactionRequestBodyCounterparty.charge_policy
_ <- Helper.booleanToFuture(s"$InvalidChargePolicy", cc=callContext) {
ChargePolicy.values.contains(ChargePolicy.withName(chargePolicy))
}
transDetailsSerialized <- NewStyle.function.tryons(UnknownError, 400, callContext) {
write(transactionRequestBodyCounterparty)(Serialization.formats(NoTypeHints))
}
(createdTransactionRequest, callContext) <- NewStyle.function.createTransactionRequestv400(u,
viewId,
fromAccount,
toAccount,
transactionRequestType,
transactionRequestBodyCounterparty,
transDetailsSerialized,
chargePolicy,
Some(OBP_TRANSACTION_REQUEST_CHALLENGE),
getScaMethodAtInstance(transactionRequestType.value).toOption,
None,
callContext)
_ <- NewStyle.function.createTransactionRequestAttributes(
bankId: BankId,
createdTransactionRequest.id,
transactionRequestAttributes,
true,
callContext: Option[CallContext]
)
} yield (createdTransactionRequest, callContext)
}
case AGENT_CASH_WITHDRAWAL => {
for {
//For Agent, Use the agentId to find the agent and set up the toAccount
transactionRequestBodyAgent <- NewStyle.function.tryons(s"${InvalidJsonFormat}, it should be $AGENT_CASH_WITHDRAWAL json format", 400, callContext) {
json.extract[TransactionRequestBodyAgentJsonV400]
}
(agent, callContext) <- NewStyle.function.getAgentByAgentNumber(BankId(transactionRequestBodyAgent.to.bank_id),transactionRequestBodyAgent.to.agent_number, callContext)
(agentAccountLinks, callContext) <- NewStyle.function.getAgentAccountLinksByAgentId(agent.agentId, callContext)
agentAccountLink <- NewStyle.function.tryons(AgentAccountLinkNotFound, 400, callContext) {
agentAccountLinks.head
}
// Check we can send money to it.
_ <- Helper.booleanToFuture(s"$AgentBeneficiaryPermit", cc=callContext) {
!agent.isPendingAgent && agent.isConfirmedAgent
}
(toAccount, callContext) <- NewStyle.function.getBankAccount(BankId(agentAccountLink.bankId), AccountId(agentAccountLink.accountId), callContext)
chargePolicy = transactionRequestBodyAgent.charge_policy
_ <- Helper.booleanToFuture(s"$InvalidChargePolicy", cc=callContext) {
ChargePolicy.values.contains(ChargePolicy.withName(chargePolicy))
}
transDetailsSerialized <- NewStyle.function.tryons(UnknownError, 400, callContext) {
write(transactionRequestBodyAgent)(Serialization.formats(NoTypeHints))
}
(createdTransactionRequest, callContext) <- NewStyle.function.createTransactionRequestv400(u,
viewId,
fromAccount,
toAccount,
transactionRequestType,
transactionRequestBodyAgent,
transDetailsSerialized,
chargePolicy,
Some(OBP_TRANSACTION_REQUEST_CHALLENGE),
getScaMethodAtInstance(transactionRequestType.value).toOption,
None,
callContext)
} yield (createdTransactionRequest, callContext)
}
case CARD => {
for {
//2rd: get toAccount from counterpartyId
transactionRequestBodyCard <- NewStyle.function.tryons(s"${InvalidJsonFormat}, it should be $CARD json format", 400, callContext) {
json.extract[TransactionRequestBodyCardJsonV400]
}
toCounterpartyId = transactionRequestBodyCard.to.counterparty_id
(toCounterparty, callContext) <- NewStyle.function.getCounterpartyByCounterpartyId(CounterpartyId(toCounterpartyId), callContext)
(toAccount, callContext) <- NewStyle.function.getBankAccountFromCounterparty(toCounterparty, true, callContext)
// Check we can send money to it.
_ <- Helper.booleanToFuture(s"$CounterpartyBeneficiaryPermit", cc=callContext) {
toCounterparty.isBeneficiary
}
chargePolicy = ChargePolicy.RECEIVER.toString
transDetailsSerialized <- NewStyle.function.tryons(UnknownError, 400, callContext) {
write(transactionRequestBodyCard)(Serialization.formats(NoTypeHints))
}
(createdTransactionRequest, callContext) <- NewStyle.function.createTransactionRequestv400(u,
viewId,
fromAccount,
toAccount,
transactionRequestType,
transactionRequestBodyCard,
transDetailsSerialized,
chargePolicy,
Some(OBP_TRANSACTION_REQUEST_CHALLENGE),
getScaMethodAtInstance(transactionRequestType.value).toOption,
None,
callContext)
} yield (createdTransactionRequest, callContext)
}
case SIMPLE => {
for {
//For SAMPLE, we will create/get toCounterparty on site and set up the toAccount
transactionRequestBodySimple <- NewStyle.function.tryons(s"${InvalidJsonFormat}, it should be $SIMPLE json format", 400, callContext) {
json.extract[TransactionRequestBodySimpleJsonV400]
}
(toCounterparty, callContext) <- NewStyle.function.getOrCreateCounterparty(
name = transactionRequestBodySimple.to.name,
description = transactionRequestBodySimple.to.description,
currency = transactionRequestBodySimple.value.currency,
createdByUserId = u.userId,
thisBankId = bankId.value,
thisAccountId = accountId.value,
thisViewId = viewId.value,
otherBankRoutingScheme = StringHelpers.snakify(transactionRequestBodySimple.to.other_bank_routing_scheme).toUpperCase,
otherBankRoutingAddress = transactionRequestBodySimple.to.other_bank_routing_address,
otherBranchRoutingScheme = StringHelpers.snakify(transactionRequestBodySimple.to.other_branch_routing_scheme).toUpperCase,
otherBranchRoutingAddress = transactionRequestBodySimple.to.other_branch_routing_address,
otherAccountRoutingScheme = StringHelpers.snakify(transactionRequestBodySimple.to.other_account_routing_scheme).toUpperCase,
otherAccountRoutingAddress = transactionRequestBodySimple.to.other_account_routing_address,
otherAccountSecondaryRoutingScheme = StringHelpers.snakify(transactionRequestBodySimple.to.other_account_secondary_routing_scheme).toUpperCase,
otherAccountSecondaryRoutingAddress = transactionRequestBodySimple.to.other_account_secondary_routing_address,
callContext: Option[CallContext],
)
(toAccount, callContext) <- NewStyle.function.getBankAccountFromCounterparty(toCounterparty, true, callContext)
// Check we can send money to it.
_ <- Helper.booleanToFuture(s"$CounterpartyBeneficiaryPermit", cc=callContext) {
toCounterparty.isBeneficiary
}
chargePolicy = transactionRequestBodySimple.charge_policy
_ <- Helper.booleanToFuture(s"$InvalidChargePolicy", cc=callContext) {
ChargePolicy.values.contains(ChargePolicy.withName(chargePolicy))
}
transDetailsSerialized <- NewStyle.function.tryons(UnknownError, 400, callContext) {
write(transactionRequestBodySimple)(Serialization.formats(NoTypeHints))
}
(createdTransactionRequest, callContext) <- NewStyle.function.createTransactionRequestv400(u,
viewId,
fromAccount,
toAccount,
transactionRequestType,
transactionRequestBodySimple,
transDetailsSerialized,
chargePolicy,
Some(OBP_TRANSACTION_REQUEST_CHALLENGE),
getScaMethodAtInstance(transactionRequestType.value).toOption,
None,
callContext)
} yield (createdTransactionRequest, callContext)
}
case SEPA => {
for {
//For SEPA, Use the IBAN to find the toCounterparty and set up the toAccount
transDetailsSEPAJson <- NewStyle.function.tryons(s"${InvalidJsonFormat}, it should be $SEPA json format", 400, callContext) {
json.extract[TransactionRequestBodySEPAJsonV400]
}
toIban = transDetailsSEPAJson.to.iban
(toCounterparty, callContext) <- NewStyle.function.getCounterpartyByIbanAndBankAccountId(toIban, fromAccount.bankId, fromAccount.accountId, callContext)
(toAccount, callContext) <- NewStyle.function.getBankAccountFromCounterparty(toCounterparty, true, callContext)
_ <- Helper.booleanToFuture(s"$CounterpartyBeneficiaryPermit", cc=callContext) {
toCounterparty.isBeneficiary
}
chargePolicy = transDetailsSEPAJson.charge_policy
_ <- Helper.booleanToFuture(s"$InvalidChargePolicy", cc=callContext) {
ChargePolicy.values.contains(ChargePolicy.withName(chargePolicy))
}
transDetailsSerialized <- NewStyle.function.tryons(UnknownError, 400, callContext) {
write(transDetailsSEPAJson)(Serialization.formats(NoTypeHints))
}
(createdTransactionRequest, callContext) <- NewStyle.function.createTransactionRequestv400(u,
viewId,
fromAccount,
toAccount,
transactionRequestType,
transDetailsSEPAJson,
transDetailsSerialized,
chargePolicy,
Some(OBP_TRANSACTION_REQUEST_CHALLENGE),
getScaMethodAtInstance(transactionRequestType.value).toOption,
transDetailsSEPAJson.reasons.map(_.map(_.transform)),
callContext)
} yield (createdTransactionRequest, callContext)
}
case FREE_FORM => {
for {
transactionRequestBodyFreeForm <- NewStyle.function.tryons(s"${InvalidJsonFormat}, it should be $FREE_FORM json format", 400, callContext) {
json.extract[TransactionRequestBodyFreeFormJSON]
}
// Following lines: just transfer the details body, add Bank_Id and Account_Id in the Detail part. This is for persistence and 'answerTransactionRequestChallenge'
transactionRequestAccountJSON = TransactionRequestAccountJsonV140(bankId.value, accountId.value)
transDetailsSerialized <- NewStyle.function.tryons(UnknownError, 400, callContext) {
write(transactionRequestBodyFreeForm)(Serialization.formats(NoTypeHints))
}
(createdTransactionRequest, callContext) <- NewStyle.function.createTransactionRequestv400(u,
viewId,
fromAccount,
fromAccount,
transactionRequestType,
transactionRequestBodyFreeForm,
transDetailsSerialized,
sharedChargePolicy.toString,
Some(OBP_TRANSACTION_REQUEST_CHALLENGE),
getScaMethodAtInstance(transactionRequestType.value).toOption,
None,
callContext)
} yield
(createdTransactionRequest, callContext)
}
}
(challenges, callContext) <- NewStyle.function.getChallengesByTransactionRequestId(createdTransactionRequest.id.value, callContext)
(transactionRequestAttributes, callContext) <- NewStyle.function.getTransactionRequestAttributes(
bankId,
createdTransactionRequest.id,
callContext
)
} yield {
(JSONFactory400.createTransactionRequestWithChargeJSON(createdTransactionRequest, challenges, transactionRequestAttributes), HttpCode.`201`(callContext))
}
}
}

View File

@ -52,7 +52,7 @@ import com.openbankproject.commons.model._
import com.openbankproject.commons.util.ApiVersion
import net.liftweb.common.{Box, Full}
import net.liftweb.json
import net.liftweb.json.{JString, JValue, MappingException, parse, parseOpt}
import net.liftweb.json.{Meta, _}
import java.text.SimpleDateFormat
import java.util.Date

View File

@ -0,0 +1,84 @@
package code.api.v6_0_0
import code.api.ResourceDocs1_4_0.SwaggerDefinitionsJSON._
import code.api.util.APIUtil._
import code.api.util.ApiTag._
import code.api.util.ErrorMessages.{$UserNotLoggedIn, InvalidJsonFormat, UnknownError, _}
import code.api.util.FutureUtil.EndpointContext
import code.bankconnectors.LocalMappedConnectorInternal
import code.bankconnectors.LocalMappedConnectorInternal._
import com.github.dwickern.macros.NameOf.nameOf
import com.openbankproject.commons.model._
import com.openbankproject.commons.util.{ApiVersion, ScannedApiVersion}
import net.liftweb.http.rest.RestHelper
import scala.collection.immutable.{List, Nil}
import scala.collection.mutable.ArrayBuffer
trait APIMethods600 {
self: RestHelper =>
val Implementations6_0_0 = new Implementations600()
class Implementations600 {
val implementedInApiVersion: ScannedApiVersion = ApiVersion.v6_0_0
private val staticResourceDocs = ArrayBuffer[ResourceDoc]()
def resourceDocs = staticResourceDocs
val apiRelations = ArrayBuffer[ApiRelation]()
val codeContext = CodeContext(staticResourceDocs, apiRelations)
staticResourceDocs += ResourceDoc(
createTransactionRequestCardano,
implementedInApiVersion,
nameOf(createTransactionRequestCardano),
"POST",
"/banks/BANK_ID/accounts/ACCOUNT_ID/owner/transaction-request-types/CARDANO/transaction-requests",
"Create Transaction Request (CARDANO)",
s"""
|
|For sandbox mode, it will use the Cardano Preprod Network.
|The accountId can be the wallet_id for now, as it uses cardano-wallet in the backend.
|
|${transactionRequestGeneralText}
|
""".stripMargin,
transactionRequestBodyCardanoJsonV600,
transactionRequestWithChargeJSON400,
List(
$UserNotLoggedIn,
$BankNotFound,
$BankAccountNotFound,
InsufficientAuthorisationToCreateTransactionRequest,
InvalidTransactionRequestType,
InvalidJsonFormat,
NotPositiveAmount,
InvalidTransactionRequestCurrency,
TransactionDisabled,
UnknownError
),
List(apiTagTransactionRequest, apiTagPSD2PIS, apiTagPsd2)
)
lazy val createTransactionRequestCardano: OBPEndpoint = {
case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: ViewId(viewId) :: "transaction-request-types" ::
"CARDANO" :: "transaction-requests" :: Nil JsonPost json -> _ =>
cc => implicit val ec = EndpointContext(Some(cc))
val transactionRequestType = TransactionRequestType("CARDANO")
LocalMappedConnectorInternal.createTransactionRequest(bankId, accountId, viewId , transactionRequestType, json)
}
}
}
object APIMethods600 extends RestHelper with APIMethods600 {
lazy val newStyleEndpoints: List[(String, String)] = Implementations6_0_0.resourceDocs.map {
rd => (rd.partialFunctionName, rd.implementedInApiVersion.toString())
}.toList
}

View File

@ -0,0 +1,64 @@
/**
* Open Bank Project - API
* Copyright (C) 2011-2019, TESOBE GmbH
* *
* This program 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 of the License, or
* (at your option) any later version.
* *
* This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
* *
* Email: contact@tesobe.com
* TESOBE GmbH
* Osloerstrasse 16/17
* Berlin 13359, Germany
* *
* This product includes software developed at
* TESOBE (http://www.tesobe.com/)
*
*/
package code.api.v6_0_0
import code.api.util._
import code.util.Helper.MdcLoggable
import com.openbankproject.commons.model._
case class CardanoPaymentJsonV600(
address: String,
amount: CardanoAmountJsonV600,
assets: Option[List[CardanoAssetJsonV600]] = None
)
case class CardanoAmountJsonV600(
quantity: Long,
unit: String // "lovelace"
)
case class CardanoAssetJsonV600(
policy_id: String,
asset_name: String,
quantity: Long
)
case class CardanoMetadataStringJsonV600(
string: String
)
case class TransactionRequestBodyCardanoJsonV600(
to: CardanoPaymentJsonV600,
value: AmountOfMoneyJsonV121,
passphrase: String,
description: String,
metadata: Option[Map[String, CardanoMetadataStringJsonV600]] = None
) extends TransactionRequestCommonBodyJSON
object JSONFactory600 extends CustomJsonFormats with MdcLoggable{
}

View File

@ -0,0 +1,122 @@
/**
Open Bank Project - API
Copyright (C) 2011-2019, TESOBE GmbH.
This program 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 of the License, or
(at your option) any later version.
This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
Email: contact@tesobe.com
TESOBE GmbH.
Osloer Strasse 16/17
Berlin 13359, Germany
This product includes software developed at
TESOBE (http://www.tesobe.com/)
*/
package code.api.v6_0_0
import code.api.OBPRestHelper
import code.api.util.APIUtil.{OBPEndpoint, getAllowedEndpoints}
import code.api.util.VersionedOBPApis
import code.api.v1_3_0.APIMethods130
import code.api.v1_4_0.APIMethods140
import code.api.v2_0_0.APIMethods200
import code.api.v2_1_0.APIMethods210
import code.api.v2_2_0.APIMethods220
import code.api.v3_0_0.APIMethods300
import code.api.v3_0_0.custom.CustomAPIMethods300
import code.api.v3_1_0.APIMethods310
import code.api.v4_0_0.APIMethods400
import code.api.v5_0_0.APIMethods500
import code.api.v5_1_0.{APIMethods510, OBPAPI5_1_0}
import code.util.Helper.MdcLoggable
import com.github.dwickern.macros.NameOf.nameOf
import com.openbankproject.commons.util.{ApiVersion, ApiVersionStatus}
import net.liftweb.common.{Box, Full}
import net.liftweb.http.{LiftResponse, PlainTextResponse}
import org.apache.http.HttpStatus
/*
This file defines which endpoints from all the versions are available in v5.0.0
*/
object OBPAPI6_0_0 extends OBPRestHelper
with APIMethods130
with APIMethods140
with APIMethods200
with APIMethods210
with APIMethods220
with APIMethods300
with CustomAPIMethods300
with APIMethods310
with APIMethods400
with APIMethods500
with APIMethods510
with APIMethods600
with MdcLoggable
with VersionedOBPApis{
val version : ApiVersion = ApiVersion.v6_0_0
val versionStatus = ApiVersionStatus.BLEEDING_EDGE.toString
// Possible Endpoints from 5.1.0, exclude one endpoint use - method,exclude multiple endpoints use -- method,
// e.g getEndpoints(Implementations5_0_0) -- List(Implementations5_0_0.genericEndpoint, Implementations5_0_0.root)
lazy val endpointsOf6_0_0 = getEndpoints(Implementations6_0_0)
lazy val excludeEndpoints =
nameOf(Implementations3_0_0.getUserByUsername) :: // following 4 endpoints miss Provider parameter in the URL, we introduce new ones in V600.
nameOf(Implementations3_1_0.getBadLoginStatus) ::
nameOf(Implementations3_1_0.unlockUser) ::
nameOf(Implementations4_0_0.lockUser) ::
nameOf(Implementations4_0_0.createUserWithAccountAccess) :: // following 3 endpoints miss ViewId parameter in the URL, we introduce new ones in V600.
nameOf(Implementations4_0_0.grantUserAccessToView) ::
nameOf(Implementations4_0_0.revokeUserAccessToView) ::
nameOf(Implementations4_0_0.revokeGrantUserAccessToViews) ::// this endpoint is forbidden in V600, we do not support multi views in one endpoint from V600.
Nil
// if old version ResourceDoc objects have the same name endpoint with new version, omit old version ResourceDoc.
def allResourceDocs = collectResourceDocs(
OBPAPI5_1_0.allResourceDocs,
Implementations6_0_0.resourceDocs
).filterNot(it => it.partialFunctionName.matches(excludeEndpoints.mkString("|")))
// all endpoints
private val endpoints: List[OBPEndpoint] = OBPAPI5_1_0.routes ++ endpointsOf6_0_0
// Filter the possible endpoints by the disabled / enabled Props settings and add them together
val routes : List[OBPEndpoint] = getAllowedEndpoints(endpoints, allResourceDocs)
registerRoutes(routes, allResourceDocs, apiPrefix, true)
logger.info(s"version $version has been run! There are ${routes.length} routes, ${allResourceDocs.length} allResourceDocs.")
// specified response for OPTIONS request.
private val corsResponse: Box[LiftResponse] = Full{
val corsHeaders = List(
"Access-Control-Allow-Origin" -> "*",
"Access-Control-Allow-Methods" -> "GET, POST, OPTIONS, PUT, PATCH, DELETE",
"Access-Control-Allow-Headers" -> "*",
"Access-Control-Allow-Credentials" -> "true",
"Access-Control-Max-Age" -> "1728000" //Tell client that this pre-flight info is valid for 20 days
)
PlainTextResponse("", corsHeaders, HttpStatus.SC_NO_CONTENT)
}
/*
* process OPTIONS http request, just return no content and status is 204
*/
this.serve({
case req if req.requestType.method == "OPTIONS" => corsResponse
})
}

View File

@ -78,8 +78,8 @@ import net.liftweb.common._
import net.liftweb.json
import net.liftweb.json.{JArray, JBool, JObject, JValue}
import net.liftweb.mapper._
import net.liftweb.util.Helpers.{hours, now, time, tryo}
import net.liftweb.util.Helpers
import net.liftweb.util.Helpers.{hours, now, time, tryo}
import org.mindrot.jbcrypt.BCrypt
import scalikejdbc.DB.CPContext
import scalikejdbc.{ConnectionPool, ConnectionPoolSettings, MultipleConnectionPoolContext, DB => scalikeDB, _}
@ -163,6 +163,8 @@ object LocalMappedConnector extends Connector with MdcLoggable {
val thresholdCurrency: String = APIUtil.getPropsValue("transactionRequests_challenge_currency", "EUR")
logger.debug(s"thresholdCurrency is $thresholdCurrency")
isValidCurrencyISOCode(thresholdCurrency) match {
case true if((currency.toLowerCase.equals("lovelace")||(currency.toLowerCase.equals("ada")))) =>
(Full(AmountOfMoney(currency, "10000000000000")), callContext)
case true =>
fx.exchangeRate(thresholdCurrency, currency, Some(bankId), callContext) match {
case rate@Some(_) =>

View File

@ -1,14 +1,21 @@
package code.bankconnectors
import code.api.ChargePolicy
import code.api.Constant._
import code.api.berlin.group.ConstantsBG
import code.api.berlin.group.v1_3.model.TransactionStatus.mapTransactionStatus
import code.api.cache.Caching
import code.api.util.APIUtil._
import code.api.util.ErrorMessages._
import code.api.util.NewStyle.HttpCode
import code.api.util._
import code.api.util.newstyle.ViewNewStyle
import code.api.v1_4_0.JSONFactory1_4_0.TransactionRequestAccountJsonV140
import code.api.v2_1_0._
import code.api.v4_0_0._
import code.api.v6_0_0.TransactionRequestBodyCardanoJsonV600
import code.branches.MappedBranch
import code.fx.fx
import code.fx.fx.TTL
import code.management.ImporterAPI.ImporterTransaction
import code.model.dataAccess.{BankAccountRouting, MappedBank, MappedBankAccount}
@ -16,27 +23,32 @@ import code.model.toBankAccountExtended
import code.transaction.MappedTransaction
import code.transactionrequests._
import code.util.Helper
import code.util.Helper._
import code.util.Helper.MdcLoggable
import com.github.dwickern.macros.NameOf.nameOf
import com.openbankproject.commons.ExecutionContext.Implicits.global
import com.openbankproject.commons.model._
import com.openbankproject.commons.model.enums.{AccountRoutingScheme, PaymentServiceTypes, TransactionRequestStatus, TransactionRequestTypes}
import com.openbankproject.commons.model.enums.ChallengeType.OBP_TRANSACTION_REQUEST_CHALLENGE
import com.openbankproject.commons.model.enums.TransactionRequestTypes._
import com.openbankproject.commons.model.enums.{TransactionRequestStatus, _}
import com.tesobe.CacheKeyFromArguments
import net.liftweb.common._
import net.liftweb.json.JsonAST.JValue
import net.liftweb.json.Serialization.write
import net.liftweb.json.{NoTypeHints, Serialization}
import net.liftweb.mapper._
import net.liftweb.util.Helpers.{now, tryo}
import net.liftweb.util.StringHelpers
import java.util.Date
import java.time.{LocalDate, ZoneId}
import java.util.UUID.randomUUID
import scala.concurrent._
import java.util.{Calendar, Date}
import scala.collection.immutable.{List, Nil}
import scala.concurrent.Future
import scala.concurrent.duration.DurationInt
import scala.language.postfixOps
import scala.util.Random
//Try to keep LocalMappedConnector smaller, so put OBP internal code here. these methods will not be exposed to CBS side.
object LocalMappedConnectorInternal extends MdcLoggable {
@ -672,4 +684,776 @@ object LocalMappedConnectorInternal extends MdcLoggable {
def getTransactionRequestStatuses() : Box[TransactionRequestStatus] = Failure(NotImplemented + nameOf(getTransactionRequestStatuses _))
// This text is used in the various Create Transaction Request resource docs
val transactionRequestGeneralText =
s"""
|
|For an introduction to Transaction Requests, see: ${Glossary.getGlossaryItemLink("Transaction-Request-Introduction")}
|
|""".stripMargin
val lowAmount = AmountOfMoneyJsonV121("EUR", "12.50")
val sharedChargePolicy = ChargePolicy.withName("SHARED")
def createTransactionRequest(bankId: BankId, accountId: AccountId, viewId: ViewId, transactionRequestType: TransactionRequestType, json: JValue): Future[(TransactionRequestWithChargeJSON400, Option[CallContext])] = {
for {
(Full(u), callContext) <- SS.user
transactionRequestTypeValue <- NewStyle.function.tryons(s"$InvalidTransactionRequestType: '${transactionRequestType.value}'. OBP does not support it.", 400, callContext) {
TransactionRequestTypes.withName(transactionRequestType.value)
}
(fromAccount, callContext) <- transactionRequestTypeValue match {
case CARD =>
for{
transactionRequestBodyCard <- NewStyle.function.tryons(s"${InvalidJsonFormat}, it should be $CARD json format", 400, callContext) {
json.extract[TransactionRequestBodyCardJsonV400]
}
// 1.1 get Card from card_number
(cardFromCbs,callContext) <- NewStyle.function.getPhysicalCardByCardNumber(transactionRequestBodyCard.card.card_number, callContext)
// 1.2 check card name/expire month. year.
calendar = Calendar.getInstance
_ = calendar.setTime(cardFromCbs.expires)
yearFromCbs = calendar.get(Calendar.YEAR).toString
monthFromCbs = calendar.get(Calendar.MONTH).toString
nameOnCardFromCbs= cardFromCbs.nameOnCard
cvvFromCbs= cardFromCbs.cvv.getOrElse("")
brandFromCbs= cardFromCbs.brand.getOrElse("")
_ <- Helper.booleanToFuture(s"$InvalidJsonValue brand is not matched", cc=callContext) {
transactionRequestBodyCard.card.brand.equalsIgnoreCase(brandFromCbs)
}
dateFromJsonBody <- NewStyle.function.tryons(s"$InvalidDateFormat year should be 'yyyy', " +
s"eg: 2023, but current expiry_year(${transactionRequestBodyCard.card.expiry_year}), " +
s"month should be 'xx', eg: 02, but current expiry_month(${transactionRequestBodyCard.card.expiry_month})", 400, callContext) {
DateWithMonthFormat.parse(s"${transactionRequestBodyCard.card.expiry_year}-${transactionRequestBodyCard.card.expiry_month}")
}
_ <- Helper.booleanToFuture(s"$InvalidJsonValue your credit card is expired.", cc=callContext) {
org.apache.commons.lang3.time.DateUtils.addMonths(new Date(), 1).before(dateFromJsonBody)
}
_ <- Helper.booleanToFuture(s"$InvalidJsonValue expiry_year is not matched", cc=callContext) {
transactionRequestBodyCard.card.expiry_year.equalsIgnoreCase(yearFromCbs)
}
_ <- Helper.booleanToFuture(s"$InvalidJsonValue expiry_month is not matched", cc=callContext) {
transactionRequestBodyCard.card.expiry_month.toInt.equals(monthFromCbs.toInt+1)
}
_ <- Helper.booleanToFuture(s"$InvalidJsonValue name_on_card is not matched", cc=callContext) {
transactionRequestBodyCard.card.name_on_card.equalsIgnoreCase(nameOnCardFromCbs)
}
_ <- Helper.booleanToFuture(s"$InvalidJsonValue cvv is not matched", cc=callContext) {
HashUtil.Sha256Hash(transactionRequestBodyCard.card.cvv).equals(cvvFromCbs)
}
} yield{
(cardFromCbs.account, callContext)
}
case _ => NewStyle.function.getBankAccount(bankId,accountId, callContext)
}
_ <- NewStyle.function.isEnabledTransactionRequests(callContext)
_ <- Helper.booleanToFuture(InvalidAccountIdFormat, cc=callContext) {
isValidID(fromAccount.accountId.value)
}
_ <- Helper.booleanToFuture(InvalidBankIdFormat, cc=callContext) {
isValidID(fromAccount.bankId.value)
}
_ <- NewStyle.function.checkAuthorisationToCreateTransactionRequest(viewId, BankIdAccountId(fromAccount.bankId, fromAccount.accountId), u, callContext)
_ <- Helper.booleanToFuture(s"${InvalidTransactionRequestType}: '${transactionRequestType.value}'. Current Sandbox does not support it. ", cc=callContext) {
APIUtil.getPropsValue("transactionRequests_supported_types", "").split(",").contains(transactionRequestType.value)
}
// Check the input JSON format, here is just check the common parts of all four types
transDetailsJson <- NewStyle.function.tryons(s"$InvalidJsonFormat The Json body should be the $TransactionRequestBodyCommonJSON ", 400, callContext) {
json.extract[TransactionRequestBodyCommonJSON]
}
transactionAmountNumber <- NewStyle.function.tryons(s"$InvalidNumber Current input is ${transDetailsJson.value.amount} ", 400, callContext) {
BigDecimal(transDetailsJson.value.amount)
}
_ <- Helper.booleanToFuture(s"${NotPositiveAmount} Current input is: '${transactionAmountNumber}'", cc=callContext) {
transactionAmountNumber > BigDecimal("0")
}
_ <- Helper.booleanToFuture(s"${InvalidISOCurrencyCode} Current input is: '${transDetailsJson.value.currency}'", cc=callContext) {
APIUtil.isValidCurrencyISOCode(transDetailsJson.value.currency)
}
(createdTransactionRequest, callContext) <- transactionRequestTypeValue match {
case REFUND => {
for {
transactionRequestBodyRefundJson <- NewStyle.function.tryons(s"${InvalidJsonFormat}, it should be $ACCOUNT json format", 400, callContext) {
json.extract[TransactionRequestBodyRefundJsonV400]
}
transactionId = TransactionId(transactionRequestBodyRefundJson.refund.transaction_id)
(fromAccount, toAccount, transaction, callContext) <- transactionRequestBodyRefundJson.to match {
case Some(refundRequestTo) if refundRequestTo.account_id.isDefined && refundRequestTo.bank_id.isDefined =>
val toBankId = BankId(refundRequestTo.bank_id.get)
val toAccountId = AccountId(refundRequestTo.account_id.get)
for {
(transaction, callContext) <- NewStyle.function.getTransaction(fromAccount.bankId, fromAccount.accountId, transactionId, callContext)
(toAccount, callContext) <- NewStyle.function.checkBankAccountExists(toBankId, toAccountId, callContext)
} yield (fromAccount, toAccount, transaction, callContext)
case Some(refundRequestTo) if refundRequestTo.counterparty_id.isDefined =>
val toCounterpartyId = CounterpartyId(refundRequestTo.counterparty_id.get)
for {
(toCounterparty, callContext) <- NewStyle.function.getCounterpartyByCounterpartyId(toCounterpartyId, callContext)
(toAccount, callContext) <- NewStyle.function.getBankAccountFromCounterparty(toCounterparty, isOutgoingAccount = true, callContext)
_ <- Helper.booleanToFuture(s"$CounterpartyBeneficiaryPermit", cc=callContext) {
toCounterparty.isBeneficiary
}
(transaction, callContext) <- NewStyle.function.getTransaction(fromAccount.bankId, fromAccount.accountId, transactionId, callContext)
} yield (fromAccount, toAccount, transaction, callContext)
case None if transactionRequestBodyRefundJson.from.isDefined =>
val fromCounterpartyId = CounterpartyId(transactionRequestBodyRefundJson.from.get.counterparty_id)
val toAccount = fromAccount
for {
(fromCounterparty, callContext) <- NewStyle.function.getCounterpartyByCounterpartyId(fromCounterpartyId, callContext)
(fromAccount, callContext) <- NewStyle.function.getBankAccountFromCounterparty(fromCounterparty, isOutgoingAccount = false, callContext)
_ <- Helper.booleanToFuture(s"$CounterpartyBeneficiaryPermit", cc=callContext) {
fromCounterparty.isBeneficiary
}
(transaction, callContext) <- NewStyle.function.getTransaction(toAccount.bankId, toAccount.accountId, transactionId, callContext)
} yield (fromAccount, toAccount, transaction, callContext)
}
transDetailsSerialized <- NewStyle.function.tryons(UnknownError, 400, callContext) {
write(transactionRequestBodyRefundJson)(Serialization.formats(NoTypeHints))
}
_ <- Helper.booleanToFuture(s"${RefundedTransaction} Current input amount is: '${transDetailsJson.value.amount}'. It can not be more than the original amount(${(transaction.amount).abs})", cc=callContext) {
(transaction.amount).abs >= transactionAmountNumber
}
//TODO, we need additional field to guarantee the transaction is refunded...
// _ <- Helper.booleanToFuture(s"${RefundedTransaction}") {
// !((transaction.description.toString contains(" Refund to ")) && (transaction.description.toString contains(" and transaction_id(")))
// }
//we add the extra info (counterparty name + transaction_id) for this special Refund endpoint.
newDescription = s"${transactionRequestBodyRefundJson.description} - Refund for transaction_id: (${transactionId.value}) to ${transaction.otherAccount.counterpartyName}"
//This is the refund endpoint, the original fromAccount is the `toAccount` which will receive money.
refundToAccount = fromAccount
//This is the refund endpoint, the original toAccount is the `fromAccount` which will lose money.
refundFromAccount = toAccount
(createdTransactionRequest, callContext) <- NewStyle.function.createTransactionRequestv400(u,
viewId,
refundFromAccount,
refundToAccount,
transactionRequestType,
transactionRequestBodyRefundJson.copy(description = newDescription),
transDetailsSerialized,
sharedChargePolicy.toString,
Some(OBP_TRANSACTION_REQUEST_CHALLENGE),
getScaMethodAtInstance(transactionRequestType.value).toOption,
None,
callContext) //in ACCOUNT, ChargePolicy set default "SHARED"
_ <- NewStyle.function.createOrUpdateTransactionRequestAttribute(
bankId = bankId,
transactionRequestId = createdTransactionRequest.id,
transactionRequestAttributeId = None,
name = "original_transaction_id",
attributeType = TransactionRequestAttributeType.withName("STRING"),
value = transactionId.value,
callContext = callContext
)
refundReasonCode = transactionRequestBodyRefundJson.refund.reason_code
_ <- if (refundReasonCode.nonEmpty) {
NewStyle.function.createOrUpdateTransactionRequestAttribute(
bankId = bankId,
transactionRequestId = createdTransactionRequest.id,
transactionRequestAttributeId = None,
name = "refund_reason_code",
attributeType = TransactionRequestAttributeType.withName("STRING"),
value = refundReasonCode,
callContext = callContext)
} else Future.successful()
(newTransactionRequestStatus, callContext) <- NewStyle.function.notifyTransactionRequest(refundFromAccount, refundToAccount, createdTransactionRequest, callContext)
_ <- NewStyle.function.saveTransactionRequestStatusImpl(createdTransactionRequest.id, newTransactionRequestStatus.toString, callContext)
createdTransactionRequest <- Future(createdTransactionRequest.copy(status = newTransactionRequestStatus.toString))
} yield (createdTransactionRequest, callContext)
}
case ACCOUNT | SANDBOX_TAN => {
for {
transactionRequestBodySandboxTan <- NewStyle.function.tryons(s"${InvalidJsonFormat}, it should be $ACCOUNT json format", 400, callContext) {
json.extract[TransactionRequestBodySandBoxTanJSON]
}
toBankId = BankId(transactionRequestBodySandboxTan.to.bank_id)
toAccountId = AccountId(transactionRequestBodySandboxTan.to.account_id)
(toAccount, callContext) <- NewStyle.function.checkBankAccountExists(toBankId, toAccountId, callContext)
transDetailsSerialized <- NewStyle.function.tryons(UnknownError, 400, callContext) {
write(transactionRequestBodySandboxTan)(Serialization.formats(NoTypeHints))
}
(createdTransactionRequest, callContext) <- NewStyle.function.createTransactionRequestv400(u,
viewId,
fromAccount,
toAccount,
transactionRequestType,
transactionRequestBodySandboxTan,
transDetailsSerialized,
sharedChargePolicy.toString,
Some(OBP_TRANSACTION_REQUEST_CHALLENGE),
getScaMethodAtInstance(transactionRequestType.value).toOption,
None,
callContext) //in ACCOUNT, ChargePolicy set default "SHARED"
} yield (createdTransactionRequest, callContext)
}
case ACCOUNT_OTP => {
for {
transactionRequestBodySandboxTan <- NewStyle.function.tryons(s"${InvalidJsonFormat}, it should be $ACCOUNT json format", 400, callContext) {
json.extract[TransactionRequestBodySandBoxTanJSON]
}
toBankId = BankId(transactionRequestBodySandboxTan.to.bank_id)
toAccountId = AccountId(transactionRequestBodySandboxTan.to.account_id)
(toAccount, callContext) <- NewStyle.function.checkBankAccountExists(toBankId, toAccountId, callContext)
transDetailsSerialized <- NewStyle.function.tryons(UnknownError, 400, callContext) {
write(transactionRequestBodySandboxTan)(Serialization.formats(NoTypeHints))
}
(createdTransactionRequest, callContext) <- NewStyle.function.createTransactionRequestv400(u,
viewId,
fromAccount,
toAccount,
transactionRequestType,
transactionRequestBodySandboxTan,
transDetailsSerialized,
sharedChargePolicy.toString,
Some(OBP_TRANSACTION_REQUEST_CHALLENGE),
getScaMethodAtInstance(transactionRequestType.value).toOption,
None,
callContext) //in ACCOUNT, ChargePolicy set default "SHARED"
} yield (createdTransactionRequest, callContext)
}
case COUNTERPARTY => {
for {
_ <- Future { logger.debug(s"Before extracting counterparty id") }
//For COUNTERPARTY, Use the counterpartyId to find the toCounterparty and set up the toAccount
transactionRequestBodyCounterparty <- NewStyle.function.tryons(s"${InvalidJsonFormat}, it should be $COUNTERPARTY json format", 400, callContext) {
json.extract[TransactionRequestBodyCounterpartyJSON]
}
toCounterpartyId = transactionRequestBodyCounterparty.to.counterparty_id
_ <- Future { logger.debug(s"After extracting counterparty id: $toCounterpartyId") }
(toCounterparty, callContext) <- NewStyle.function.getCounterpartyByCounterpartyId(CounterpartyId(toCounterpartyId), callContext)
transactionRequestAttributes <- if(transactionRequestBodyCounterparty.attributes.isDefined && transactionRequestBodyCounterparty.attributes.head.length > 0 ) {
val attributes = transactionRequestBodyCounterparty.attributes.head
val failMsg = s"$InvalidJsonFormat The attribute `type` field can only accept the following field: " +
s"${TransactionRequestAttributeType.DOUBLE}(12.1234)," +
s" ${TransactionRequestAttributeType.STRING}(TAX_NUMBER), " +
s"${TransactionRequestAttributeType.INTEGER}(123) and " +
s"${TransactionRequestAttributeType.DATE_WITH_DAY}(2012-04-23)"
for{
_ <- NewStyle.function.tryons(failMsg, 400, callContext) {
attributes.map(attribute => TransactionRequestAttributeType.withName(attribute.attribute_type))
}
}yield{
attributes
}
} else {
Future.successful(List.empty[TransactionRequestAttributeJsonV400])
}
(counterpartyLimitBox, callContext) <- Connector.connector.vend.getCounterpartyLimit(
bankId.value,
accountId.value,
viewId.value,
toCounterpartyId,
callContext
)
_<- if(counterpartyLimitBox.isDefined){
for{
counterpartyLimit <- Future.successful(counterpartyLimitBox.head)
maxSingleAmount = counterpartyLimit.maxSingleAmount
maxMonthlyAmount = counterpartyLimit.maxMonthlyAmount
maxNumberOfMonthlyTransactions = counterpartyLimit.maxNumberOfMonthlyTransactions
maxYearlyAmount = counterpartyLimit.maxYearlyAmount
maxNumberOfYearlyTransactions = counterpartyLimit.maxNumberOfYearlyTransactions
maxTotalAmount = counterpartyLimit.maxTotalAmount
maxNumberOfTransactions = counterpartyLimit.maxNumberOfTransactions
// Get the first day of the current month
firstDayOfMonth: LocalDate = LocalDate.now().withDayOfMonth(1)
// Get the last day of the current month
lastDayOfMonth: LocalDate = LocalDate.now().withDayOfMonth(
LocalDate.now().lengthOfMonth()
)
// Get the first day of the current year
firstDayOfYear: LocalDate = LocalDate.now().withDayOfYear(1)
// Get the last day of the current year
lastDayOfYear: LocalDate = LocalDate.now().withDayOfYear(
LocalDate.now().lengthOfYear()
)
// Convert LocalDate to Date
zoneId: ZoneId = ZoneId.systemDefault()
firstCurrentMonthDate: Date = Date.from(firstDayOfMonth.atStartOfDay(zoneId).toInstant)
// Adjust to include 23:59:59.999
lastCurrentMonthDate: Date = Date.from(
lastDayOfMonth
.atTime(23, 59, 59, 999000000)
.atZone(zoneId)
.toInstant
)
firstCurrentYearDate: Date = Date.from(firstDayOfYear.atStartOfDay(zoneId).toInstant)
// Adjust to include 23:59:59.999
lastCurrentYearDate: Date = Date.from(
lastDayOfYear
.atTime(23, 59, 59, 999000000)
.atZone(zoneId)
.toInstant
)
defaultFromDate: Date = theEpochTime
defaultToDate: Date = APIUtil.ToDateInFuture
(sumOfTransactionsFromAccountToCounterpartyMonthly, callContext) <- NewStyle.function.getSumOfTransactionsFromAccountToCounterparty(
fromAccount.bankId: BankId,
fromAccount.accountId: AccountId,
CounterpartyId(toCounterpartyId): CounterpartyId,
firstCurrentMonthDate: Date,
lastCurrentMonthDate: Date,
callContext: Option[CallContext]
)
(countOfTransactionsFromAccountToCounterpartyMonthly, callContext) <- NewStyle.function.getCountOfTransactionsFromAccountToCounterparty(
fromAccount.bankId: BankId,
fromAccount.accountId: AccountId,
CounterpartyId(toCounterpartyId): CounterpartyId,
firstCurrentMonthDate: Date,
lastCurrentMonthDate: Date,
callContext: Option[CallContext]
)
(sumOfTransactionsFromAccountToCounterpartyYearly, callContext) <- NewStyle.function.getSumOfTransactionsFromAccountToCounterparty(
fromAccount.bankId: BankId,
fromAccount.accountId: AccountId,
CounterpartyId(toCounterpartyId): CounterpartyId,
firstCurrentYearDate: Date,
lastCurrentYearDate: Date,
callContext: Option[CallContext]
)
(countOfTransactionsFromAccountToCounterpartyYearly, callContext) <- NewStyle.function.getCountOfTransactionsFromAccountToCounterparty(
fromAccount.bankId: BankId,
fromAccount.accountId: AccountId,
CounterpartyId(toCounterpartyId): CounterpartyId,
firstCurrentYearDate: Date,
lastCurrentYearDate: Date,
callContext: Option[CallContext]
)
(sumOfAllTransactionsFromAccountToCounterparty, callContext) <- NewStyle.function.getSumOfTransactionsFromAccountToCounterparty(
fromAccount.bankId: BankId,
fromAccount.accountId: AccountId,
CounterpartyId(toCounterpartyId): CounterpartyId,
defaultFromDate: Date,
defaultToDate: Date,
callContext: Option[CallContext]
)
(countOfAllTransactionsFromAccountToCounterparty, callContext) <- NewStyle.function.getCountOfTransactionsFromAccountToCounterparty(
fromAccount.bankId: BankId,
fromAccount.accountId: AccountId,
CounterpartyId(toCounterpartyId): CounterpartyId,
defaultFromDate: Date,
defaultToDate: Date,
callContext: Option[CallContext]
)
currentTransactionAmountWithFxApplied <- NewStyle.function.tryons(s"${InvalidJsonFormat}, it should be $COUNTERPARTY json format", 400, callContext) {
val fromAccountCurrency = fromAccount.currency //eg: if from account currency is EUR
val transferCurrency = transactionRequestBodyCounterparty.value.currency //eg: if the payment json body currency is GBP.
val transferAmount = BigDecimal(transactionRequestBodyCounterparty.value.amount) //eg: if the payment json body amount is 1.
val debitRate = fx.exchangeRate(transferCurrency, fromAccountCurrency, Some(fromAccount.bankId.value), callContext) //eg: the rate here is 1.16278.
fx.convert(transferAmount, debitRate) // 1.16278 Euro
}
_ <- Helper.booleanToFuture(s"$CounterpartyLimitValidationError max_single_amount is $maxSingleAmount ${fromAccount.currency}, " +
s"but current transaction body amount is ${transactionRequestBodyCounterparty.value.amount} ${transactionRequestBodyCounterparty.value.currency}, " +
s"which is $currentTransactionAmountWithFxApplied ${fromAccount.currency}. ", cc = callContext) {
maxSingleAmount >= currentTransactionAmountWithFxApplied
}
_ <- Helper.booleanToFuture(s"$CounterpartyLimitValidationError max_monthly_amount is $maxMonthlyAmount, but current monthly amount is ${BigDecimal(sumOfTransactionsFromAccountToCounterpartyMonthly.amount)+currentTransactionAmountWithFxApplied}", cc = callContext) {
maxMonthlyAmount >= BigDecimal(sumOfTransactionsFromAccountToCounterpartyMonthly.amount)+currentTransactionAmountWithFxApplied
}
_ <- Helper.booleanToFuture(s"$CounterpartyLimitValidationError max_number_of_monthly_transactions is $maxNumberOfMonthlyTransactions, but current count of monthly transactions is ${countOfTransactionsFromAccountToCounterpartyMonthly+1}", cc = callContext) {
maxNumberOfMonthlyTransactions >= countOfTransactionsFromAccountToCounterpartyMonthly+1
}
_ <- Helper.booleanToFuture(s"$CounterpartyLimitValidationError max_yearly_amount is $maxYearlyAmount, but current yearly amount is ${BigDecimal(sumOfTransactionsFromAccountToCounterpartyYearly.amount)+currentTransactionAmountWithFxApplied}", cc = callContext) {
maxYearlyAmount >= BigDecimal(sumOfTransactionsFromAccountToCounterpartyYearly.amount)+currentTransactionAmountWithFxApplied
}
result <- Helper.booleanToFuture(s"$CounterpartyLimitValidationError max_number_of_yearly_transactions is $maxNumberOfYearlyTransactions, but current count of yearly transaction is ${countOfTransactionsFromAccountToCounterpartyYearly+1}", cc = callContext) {
maxNumberOfYearlyTransactions >= countOfTransactionsFromAccountToCounterpartyYearly+1
}
_ <- Helper.booleanToFuture(s"$CounterpartyLimitValidationError max_total_amount is $maxTotalAmount, but current amount is ${BigDecimal(sumOfAllTransactionsFromAccountToCounterparty.amount)+currentTransactionAmountWithFxApplied}", cc = callContext) {
maxTotalAmount >= BigDecimal(sumOfAllTransactionsFromAccountToCounterparty.amount)+currentTransactionAmountWithFxApplied
}
result <- Helper.booleanToFuture(s"$CounterpartyLimitValidationError max_number_of_transactions is $maxNumberOfTransactions, but current count of all transactions is ${countOfAllTransactionsFromAccountToCounterparty+1}", cc = callContext) {
maxNumberOfTransactions >= countOfAllTransactionsFromAccountToCounterparty+1
}
}yield{
result
}
}
else {
Future.successful(true)
}
(toAccount, callContext) <- NewStyle.function.getBankAccountFromCounterparty(toCounterparty, true, callContext)
// Check we can send money to it.
_ <- Helper.booleanToFuture(s"$CounterpartyBeneficiaryPermit", cc=callContext) {
toCounterparty.isBeneficiary
}
chargePolicy = transactionRequestBodyCounterparty.charge_policy
_ <- Helper.booleanToFuture(s"$InvalidChargePolicy", cc=callContext) {
ChargePolicy.values.contains(ChargePolicy.withName(chargePolicy))
}
transDetailsSerialized <- NewStyle.function.tryons(UnknownError, 400, callContext) {
write(transactionRequestBodyCounterparty)(Serialization.formats(NoTypeHints))
}
(createdTransactionRequest, callContext) <- NewStyle.function.createTransactionRequestv400(u,
viewId,
fromAccount,
toAccount,
transactionRequestType,
transactionRequestBodyCounterparty,
transDetailsSerialized,
chargePolicy,
Some(OBP_TRANSACTION_REQUEST_CHALLENGE),
getScaMethodAtInstance(transactionRequestType.value).toOption,
None,
callContext)
_ <- NewStyle.function.createTransactionRequestAttributes(
bankId: BankId,
createdTransactionRequest.id,
transactionRequestAttributes,
true,
callContext: Option[CallContext]
)
} yield (createdTransactionRequest, callContext)
}
case AGENT_CASH_WITHDRAWAL => {
for {
//For Agent, Use the agentId to find the agent and set up the toAccount
transactionRequestBodyAgent <- NewStyle.function.tryons(s"${InvalidJsonFormat}, it should be $AGENT_CASH_WITHDRAWAL json format", 400, callContext) {
json.extract[TransactionRequestBodyAgentJsonV400]
}
(agent, callContext) <- NewStyle.function.getAgentByAgentNumber(BankId(transactionRequestBodyAgent.to.bank_id),transactionRequestBodyAgent.to.agent_number, callContext)
(agentAccountLinks, callContext) <- NewStyle.function.getAgentAccountLinksByAgentId(agent.agentId, callContext)
agentAccountLink <- NewStyle.function.tryons(AgentAccountLinkNotFound, 400, callContext) {
agentAccountLinks.head
}
// Check we can send money to it.
_ <- Helper.booleanToFuture(s"$AgentBeneficiaryPermit", cc=callContext) {
!agent.isPendingAgent && agent.isConfirmedAgent
}
(toAccount, callContext) <- NewStyle.function.getBankAccount(BankId(agentAccountLink.bankId), AccountId(agentAccountLink.accountId), callContext)
chargePolicy = transactionRequestBodyAgent.charge_policy
_ <- Helper.booleanToFuture(s"$InvalidChargePolicy", cc=callContext) {
ChargePolicy.values.contains(ChargePolicy.withName(chargePolicy))
}
transDetailsSerialized <- NewStyle.function.tryons(UnknownError, 400, callContext) {
write(transactionRequestBodyAgent)(Serialization.formats(NoTypeHints))
}
(createdTransactionRequest, callContext) <- NewStyle.function.createTransactionRequestv400(u,
viewId,
fromAccount,
toAccount,
transactionRequestType,
transactionRequestBodyAgent,
transDetailsSerialized,
chargePolicy,
Some(OBP_TRANSACTION_REQUEST_CHALLENGE),
getScaMethodAtInstance(transactionRequestType.value).toOption,
None,
callContext)
} yield (createdTransactionRequest, callContext)
}
case CARD => {
for {
//2rd: get toAccount from counterpartyId
transactionRequestBodyCard <- NewStyle.function.tryons(s"${InvalidJsonFormat}, it should be $CARD json format", 400, callContext) {
json.extract[TransactionRequestBodyCardJsonV400]
}
toCounterpartyId = transactionRequestBodyCard.to.counterparty_id
(toCounterparty, callContext) <- NewStyle.function.getCounterpartyByCounterpartyId(CounterpartyId(toCounterpartyId), callContext)
(toAccount, callContext) <- NewStyle.function.getBankAccountFromCounterparty(toCounterparty, true, callContext)
// Check we can send money to it.
_ <- Helper.booleanToFuture(s"$CounterpartyBeneficiaryPermit", cc=callContext) {
toCounterparty.isBeneficiary
}
chargePolicy = ChargePolicy.RECEIVER.toString
transDetailsSerialized <- NewStyle.function.tryons(UnknownError, 400, callContext) {
write(transactionRequestBodyCard)(Serialization.formats(NoTypeHints))
}
(createdTransactionRequest, callContext) <- NewStyle.function.createTransactionRequestv400(u,
viewId,
fromAccount,
toAccount,
transactionRequestType,
transactionRequestBodyCard,
transDetailsSerialized,
chargePolicy,
Some(OBP_TRANSACTION_REQUEST_CHALLENGE),
getScaMethodAtInstance(transactionRequestType.value).toOption,
None,
callContext)
} yield (createdTransactionRequest, callContext)
}
case SIMPLE => {
for {
//For SAMPLE, we will create/get toCounterparty on site and set up the toAccount
transactionRequestBodySimple <- NewStyle.function.tryons(s"${InvalidJsonFormat}, it should be $SIMPLE json format", 400, callContext) {
json.extract[TransactionRequestBodySimpleJsonV400]
}
(toCounterparty, callContext) <- NewStyle.function.getOrCreateCounterparty(
name = transactionRequestBodySimple.to.name,
description = transactionRequestBodySimple.to.description,
currency = transactionRequestBodySimple.value.currency,
createdByUserId = u.userId,
thisBankId = bankId.value,
thisAccountId = accountId.value,
thisViewId = viewId.value,
otherBankRoutingScheme = StringHelpers.snakify(transactionRequestBodySimple.to.other_bank_routing_scheme).toUpperCase,
otherBankRoutingAddress = transactionRequestBodySimple.to.other_bank_routing_address,
otherBranchRoutingScheme = StringHelpers.snakify(transactionRequestBodySimple.to.other_branch_routing_scheme).toUpperCase,
otherBranchRoutingAddress = transactionRequestBodySimple.to.other_branch_routing_address,
otherAccountRoutingScheme = StringHelpers.snakify(transactionRequestBodySimple.to.other_account_routing_scheme).toUpperCase,
otherAccountRoutingAddress = transactionRequestBodySimple.to.other_account_routing_address,
otherAccountSecondaryRoutingScheme = StringHelpers.snakify(transactionRequestBodySimple.to.other_account_secondary_routing_scheme).toUpperCase,
otherAccountSecondaryRoutingAddress = transactionRequestBodySimple.to.other_account_secondary_routing_address,
callContext: Option[CallContext],
)
(toAccount, callContext) <- NewStyle.function.getBankAccountFromCounterparty(toCounterparty, true, callContext)
// Check we can send money to it.
_ <- Helper.booleanToFuture(s"$CounterpartyBeneficiaryPermit", cc=callContext) {
toCounterparty.isBeneficiary
}
chargePolicy = transactionRequestBodySimple.charge_policy
_ <- Helper.booleanToFuture(s"$InvalidChargePolicy", cc=callContext) {
ChargePolicy.values.contains(ChargePolicy.withName(chargePolicy))
}
transDetailsSerialized <- NewStyle.function.tryons(UnknownError, 400, callContext) {
write(transactionRequestBodySimple)(Serialization.formats(NoTypeHints))
}
(createdTransactionRequest, callContext) <- NewStyle.function.createTransactionRequestv400(u,
viewId,
fromAccount,
toAccount,
transactionRequestType,
transactionRequestBodySimple,
transDetailsSerialized,
chargePolicy,
Some(OBP_TRANSACTION_REQUEST_CHALLENGE),
getScaMethodAtInstance(transactionRequestType.value).toOption,
None,
callContext)
} yield (createdTransactionRequest, callContext)
}
case SEPA => {
for {
//For SEPA, Use the IBAN to find the toCounterparty and set up the toAccount
transDetailsSEPAJson <- NewStyle.function.tryons(s"${InvalidJsonFormat}, it should be $SEPA json format", 400, callContext) {
json.extract[TransactionRequestBodySEPAJsonV400]
}
toIban = transDetailsSEPAJson.to.iban
(toCounterparty, callContext) <- NewStyle.function.getCounterpartyByIbanAndBankAccountId(toIban, fromAccount.bankId, fromAccount.accountId, callContext)
(toAccount, callContext) <- NewStyle.function.getBankAccountFromCounterparty(toCounterparty, true, callContext)
_ <- Helper.booleanToFuture(s"$CounterpartyBeneficiaryPermit", cc=callContext) {
toCounterparty.isBeneficiary
}
chargePolicy = transDetailsSEPAJson.charge_policy
_ <- Helper.booleanToFuture(s"$InvalidChargePolicy", cc=callContext) {
ChargePolicy.values.contains(ChargePolicy.withName(chargePolicy))
}
transDetailsSerialized <- NewStyle.function.tryons(UnknownError, 400, callContext) {
write(transDetailsSEPAJson)(Serialization.formats(NoTypeHints))
}
(createdTransactionRequest, callContext) <- NewStyle.function.createTransactionRequestv400(u,
viewId,
fromAccount,
toAccount,
transactionRequestType,
transDetailsSEPAJson,
transDetailsSerialized,
chargePolicy,
Some(OBP_TRANSACTION_REQUEST_CHALLENGE),
getScaMethodAtInstance(transactionRequestType.value).toOption,
transDetailsSEPAJson.reasons.map(_.map(_.transform)),
callContext)
} yield (createdTransactionRequest, callContext)
}
case FREE_FORM => {
for {
transactionRequestBodyFreeForm <- NewStyle.function.tryons(s"${InvalidJsonFormat}, it should be $FREE_FORM json format", 400, callContext) {
json.extract[TransactionRequestBodyFreeFormJSON]
}
// Following lines: just transfer the details body, add Bank_Id and Account_Id in the Detail part. This is for persistence and 'answerTransactionRequestChallenge'
transactionRequestAccountJSON = TransactionRequestAccountJsonV140(bankId.value, accountId.value)
transDetailsSerialized <- NewStyle.function.tryons(UnknownError, 400, callContext) {
write(transactionRequestBodyFreeForm)(Serialization.formats(NoTypeHints))
}
(createdTransactionRequest, callContext) <- NewStyle.function.createTransactionRequestv400(u,
viewId,
fromAccount,
fromAccount,
transactionRequestType,
transactionRequestBodyFreeForm,
transDetailsSerialized,
sharedChargePolicy.toString,
Some(OBP_TRANSACTION_REQUEST_CHALLENGE),
getScaMethodAtInstance(transactionRequestType.value).toOption,
None,
callContext)
} yield
(createdTransactionRequest, callContext)
}
case CARDANO => {
for {
//For CARDANO, we will create/get toCounterparty on site and set up the toAccount, fromAccount we need to prepare before .
transactionRequestBodyCardano <- NewStyle.function.tryons(s"${InvalidJsonFormat} It should be $TransactionRequestBodyCardanoJsonV600 json format", 400, callContext) {
json.extract[TransactionRequestBodyCardanoJsonV600]
}
// Validate Cardano specific fields
_ <- Helper.booleanToFuture(s"$InvalidJsonValue Cardano payment address is required", cc=callContext) {
transactionRequestBodyCardano.to.address.nonEmpty
}
// Validate Cardano address format (basic validation)
_ <- Helper.booleanToFuture(s"$InvalidJsonValue Cardano address format is invalid", cc=callContext) {
transactionRequestBodyCardano.to.address.startsWith("addr_") ||
transactionRequestBodyCardano.to.address.startsWith("addr_test") ||
transactionRequestBodyCardano.to.address.startsWith("addr_main")
}
// Validate amount quantity is non-negative
_ <- Helper.booleanToFuture(s"$InvalidJsonValue Cardano amount quantity must be non-negative", cc=callContext) {
transactionRequestBodyCardano.to.amount.quantity >= 0
}
// Validate amount unit must be 'lovelace' (case insensitive)
_ <- Helper.booleanToFuture(s"$InvalidJsonValue Cardano amount unit must be 'lovelace'", cc=callContext) {
transactionRequestBodyCardano.to.amount.unit.toLowerCase == "lovelace"
}
// Validate assets if provided
_ <- transactionRequestBodyCardano.to.assets match {
case Some(assets) => Helper.booleanToFuture(s"$InvalidJsonValue Cardano assets must have valid policy_id and asset_name", cc=callContext) {
assets.forall(asset => asset.policy_id.nonEmpty && asset.asset_name.nonEmpty && asset.quantity > 0)
}
case None => Future.successful(true)
}
// Validate that if amount is 0, there must be assets (token-only transfer)
_ <- (transactionRequestBodyCardano.to.amount, transactionRequestBodyCardano.to.assets) match {
case (amount, Some(assets)) if amount.quantity == 0 => Helper.booleanToFuture(s"$InvalidJsonValue Cardano token-only transfer must have assets", cc=callContext) {
assets.nonEmpty
}
case (amount, None) if amount.quantity == 0 => Helper.booleanToFuture(s"$InvalidJsonValue Cardano transfer with zero amount must include assets", cc=callContext) {
false
}
case _ => Future.successful(true)
}
// Validate metadata if provided
_ <- transactionRequestBodyCardano.metadata match {
case Some(metadata) => Helper.booleanToFuture(s"$InvalidJsonValue Cardano metadata must have valid structure", cc=callContext) {
metadata.forall { case (label, metadataObj) =>
label.nonEmpty && metadataObj.string.nonEmpty
}
}
case None => Future.successful(true)
}
(toCounterparty, callContext) <- NewStyle.function.getOrCreateCounterparty(
name = "cardano-"+transactionRequestBodyCardano.to.address.take(27),
description = transactionRequestBodyCardano.description,
currency = transactionRequestBodyCardano.value.currency,
createdByUserId = u.userId,
thisBankId = bankId.value,
thisAccountId = accountId.value,
thisViewId = viewId.value,
otherBankRoutingScheme = "",
otherBankRoutingAddress = "",
otherBranchRoutingScheme = "",
otherBranchRoutingAddress = "",
otherAccountRoutingScheme = "",
otherAccountRoutingAddress = "",
otherAccountSecondaryRoutingScheme = "cardano",
otherAccountSecondaryRoutingAddress = transactionRequestBodyCardano.to.address,
callContext: Option[CallContext],
)
(toAccount, callContext) <- NewStyle.function.getBankAccountFromCounterparty(toCounterparty, true, callContext)
// Check we can send money to it.
_ <- Helper.booleanToFuture(s"$CounterpartyBeneficiaryPermit", cc=callContext) {
toCounterparty.isBeneficiary
}
chargePolicy = sharedChargePolicy.toString
transDetailsSerialized <- NewStyle.function.tryons(UnknownError, 400, callContext) {
write(transactionRequestBodyCardano)(Serialization.formats(NoTypeHints))
}
(createdTransactionRequest, callContext) <- NewStyle.function.createTransactionRequestv400(u,
viewId,
fromAccount,
toAccount,
transactionRequestType,
transactionRequestBodyCardano,
transDetailsSerialized,
chargePolicy,
Some(OBP_TRANSACTION_REQUEST_CHALLENGE),
getScaMethodAtInstance(transactionRequestType.value).toOption,
None,
callContext)
} yield (createdTransactionRequest, callContext)
}
}
(challenges, callContext) <- NewStyle.function.getChallengesByTransactionRequestId(createdTransactionRequest.id.value, callContext)
(transactionRequestAttributes, callContext) <- NewStyle.function.getTransactionRequestAttributes(
bankId,
createdTransactionRequest.id,
callContext
)
} yield {
(JSONFactory400.createTransactionRequestWithChargeJSON(createdTransactionRequest, challenges, transactionRequestAttributes), HttpCode.`201`(callContext))
}
}
}

View File

@ -24,14 +24,17 @@ Berlin 13359, Germany
*/
import code.api.util.APIUtil._
import code.api.util.CallContext
import code.api.util.{CallContext, ErrorMessages, NewStyle}
import code.api.v6_0_0.TransactionRequestBodyCardanoJsonV600
import code.bankconnectors._
import code.util.AkkaHttpClient._
import code.util.Helper
import code.util.Helper.MdcLoggable
import com.openbankproject.commons.model._
import net.liftweb.common._
import net.liftweb.json
import net.liftweb.json.JValue
import java.util.UUID.randomUUID
import scala.collection.mutable.ArrayBuffer
import scala.concurrent.Future
import scala.language.postfixOps
@ -41,13 +44,13 @@ trait CardanoConnector_vJun2025 extends Connector with MdcLoggable {
//this one import is for implicit convert, don't delete
implicit override val nameOfConnector = CardanoConnector_vJun2025.toString
val messageFormat: String = "Jun2025"
override val messageDocs = ArrayBuffer[MessageDoc]()
override def makePaymentv210(fromAccount: BankAccount,
override def makePaymentv210(
fromAccount: BankAccount,
toAccount: BankAccount,
transactionRequestId: TransactionRequestId,
transactionRequestCommonBody: TransactionRequestCommonBodyJSON,
@ -56,21 +59,158 @@ trait CardanoConnector_vJun2025 extends Connector with MdcLoggable {
transactionRequestType: TransactionRequestType,
chargePolicy: String,
callContext: Option[CallContext]): OBPReturnType[Box[TransactionId]] = {
for {
transactionData <- Future.successful("123|100.50|EUR|2025-03-16 12:30:00")
transactionHash <- Future {
code.cardano.CardanoMetadataWriter.generateHash(transactionData)
failMsg <- Future.successful(s"${ErrorMessages.InvalidJsonFormat} The transaction request body should be $TransactionRequestBodyCardanoJsonV600")
transactionRequestBodyCardano <- NewStyle.function.tryons(failMsg, 400, callContext) {
transactionRequestCommonBody.asInstanceOf[TransactionRequestBodyCardanoJsonV600]
}
txIn <- Future.successful("8c293647e5cb51c4d29e57e162a0bb4a0500096560ce6899a4b801f2b69f2813:0")
txOut <- Future.successful("addr_test1qruvtthh7mndxu2ncykn47tksar9yqr3u97dlkq2h2dhzwnf3d755n99t92kp4rydpzgv7wmx4nx2j0zzz0g802qvadqtczjhn:1234")
signingKey <- Future.successful("payment.skey")
network <- Future.successful("--testnet-magic")
_ <- Future {
code.cardano.CardanoMetadataWriter.submitHashToCardano(transactionHash, txIn, txOut, signingKey, network)
walletId = fromAccount.accountId.value
paramUrl = s"http://localhost:8090/v2/wallets/${walletId}/transactions"
// Build payments array based on the transaction request body
paymentsArray = buildPaymentsArray(transactionRequestBodyCardano)
// Build metadata if present
metadataJson = buildMetadataJson(transactionRequestBodyCardano)
jsonToSend = s"""{
| "payments": [
| $paymentsArray
| ],
| "passphrase": "${transactionRequestBodyCardano.passphrase}"
| $metadataJson
|}""".stripMargin
request = prepareHttpRequest(paramUrl, _root_.akka.http.scaladsl.model.HttpMethods.POST, _root_.akka.http.scaladsl.model.HttpProtocol("HTTP/1.1"), jsonToSend)
_ = logger.debug(s"CardanoConnector_vJun2025.makePaymentv210 request is : $request")
response <- NewStyle.function.tryons(s"${ErrorMessages.UnknownError} Failed to make HTTP request to Cardano API", 500, callContext) {
makeHttpRequest(request)
}.flatten
responseBody <- NewStyle.function.tryons(s"${ErrorMessages.UnknownError} Failed to extract response body", 500, callContext) {
response.entity.dataBytes.runFold(_root_.akka.util.ByteString(""))(_ ++ _).map(_.utf8String)
}.flatten
_ <- Helper.booleanToFuture(s"${ErrorMessages.UnknownError} Cardano API returned error: ${response.status.value}", 500, callContext) {
logger.debug(s"CardanoConnector_vJun2025.makePaymentv210 response jsonString is : $responseBody")
response.status.isSuccess()
}
transactionId <- Future.successful(TransactionId(randomUUID().toString))
} yield (Full(transactionId), callContext)
transactionId <- NewStyle.function.tryons(s"${ErrorMessages.InvalidJsonFormat} Failed to parse Cardano API response", 500, callContext) {
val jValue: JValue = json.parse(responseBody)
val id = (jValue \ "id").values.toString
if (id.nonEmpty && id != "null") {
TransactionId(id)
} else {
throw new RuntimeException(s"${ErrorMessages.UnknownError} Transaction ID not found in response")
}
}
} yield {
(Full(transactionId), callContext)
}
}
/**
* Build payments array for Cardano API
* Amount is always required in Cardano transactions
* Supports different payment types: ADA only, Token only, ADA + Token
*/
private def buildPaymentsArray(transactionRequestBodyCardano: TransactionRequestBodyCardanoJsonV600): String = {
val address = transactionRequestBodyCardano.to.address
// Amount is always required in Cardano
val amount = transactionRequestBodyCardano.to.amount
val amountJson = s"""
| "amount": {
| "quantity": ${amount.quantity},
| "unit": "${amount.unit}"
| }""".stripMargin
val assetsJson = transactionRequestBodyCardano.to.assets match {
case Some(assets) if assets.nonEmpty => {
val assetsArray = assets.map { asset =>
// Convert asset_name to hex format
// "4f47435241" -> "OGCRA"
// "4f47435242" -> "OGCRB"
// "4f47435243" -> "OGCRC"
// "4f47435244" -> "OGCRD"
val hexAssetName = asset.asset_name.getBytes("UTF-8").map("%02x".format(_)).mkString
s""" {
| "policy_id": "${asset.policy_id}",
| "asset_name": "$hexAssetName",
| "quantity": ${asset.quantity}
| }""".stripMargin
}.mkString(",\n")
s""",
| "assets": [
|$assetsArray
| ]""".stripMargin
}
case _ => ""
}
// Always include amount, optionally include assets
val jsonContent = if (assetsJson.isEmpty) {
s""" "address": "$address",$amountJson"""
} else {
s""" "address": "$address",$amountJson$assetsJson"""
}
s""" {
|$jsonContent
| }""".stripMargin
}
/**
* Build metadata JSON for Cardano API
* Supports simple string metadata format
*/
private def buildMetadataJson(transactionRequestBodyCardano: TransactionRequestBodyCardanoJsonV600): String = {
transactionRequestBodyCardano.metadata match {
case Some(metadata) if metadata.nonEmpty => {
val metadataEntries = metadata.map { case (label, metadataObj) =>
s""" "$label": {
| "string": "${metadataObj.string}"
| }""".stripMargin
}.mkString(",\n")
s""",
| "metadata": {
|$metadataEntries
| }""".stripMargin
}
case _ => ""
}
}
// override def makePaymentv210(fromAccount: BankAccount,
// toAccount: BankAccount,
// transactionRequestId: TransactionRequestId,
// transactionRequestCommonBody: TransactionRequestCommonBodyJSON,
// amount: BigDecimal,
// description: String,
// transactionRequestType: TransactionRequestType,
// chargePolicy: String,
// callContext: Option[CallContext]): OBPReturnType[Box[TransactionId]] = {
// for {
// transactionData <- Future.successful("123|100.50|EUR|2025-03-16 12:30:00")
// transactionHash <- Future {
// code.cardano.CardanoMetadataWriter.generateHash(transactionData)
// }
// txIn <- Future.successful("8c293647e5cb51c4d29e57e162a0bb4a0500096560ce6899a4b801f2b69f2813:0")
// txOut <- Future.successful("addr_test1qruvtthh7mndxu2ncykn47tksar9yqr3u97dlkq2h2dhzwnf3d755n99t92kp4rydpzgv7wmx4nx2j0zzz0g802qvadqtczjhn:1234")
// signingKey <- Future.successful("payment.skey")
// network <- Future.successful("--testnet-magic")
// _ <- Future {
// code.cardano.CardanoMetadataWriter.submitHashToCardano(transactionHash, txIn, txOut, signingKey, network)
// }
// transactionId <- Future.successful(TransactionId(randomUUID().toString))
// } yield (Full(transactionId), callContext)
// }
}
object CardanoConnector_vJun2025 extends CardanoConnector_vJun2025
object CardanoConnector_vJun2025 extends CardanoConnector_vJun2025

View File

@ -1,24 +1,20 @@
package code.metadata.counterparties
import java.util.UUID.randomUUID
import java.util.{Date, UUID}
import code.api.cache.Caching
import code.api.util.{APIUtil, CallContext}
import code.api.util.APIUtil.getSecondsCache
import code.model._
import code.api.util.APIUtil
import code.model.dataAccess.ResourceUser
import code.users.Users
import code.util.Helper.MdcLoggable
import code.util._
import com.google.common.cache.CacheBuilder
import com.openbankproject.commons.model._
import com.tesobe.CacheKeyFromArguments
import net.liftweb.common.{Box, Full}
import net.liftweb.mapper.{By, MappedString, _}
import net.liftweb.mapper._
import net.liftweb.util.Helpers.tryo
import net.liftweb.util.StringHelpers
import java.util.UUID.randomUUID
import java.util.{Date, UUID}
import scala.concurrent.duration._
// For now, there are two Counterparties: one is used for CreateCounterParty.Counterparty, the other is for getTransactions.Counterparty.
@ -210,34 +206,34 @@ object MapperCounterparties extends Counterparties with MdcLoggable {
currency: String,
bespoke: List[CounterpartyBespoke]
): Box[CounterpartyTrait] = {
tryo{
val mappedCounterparty = MappedCounterparty.create
.mCounterPartyId(APIUtil.createExplicitCounterpartyId) //We create the Counterparty_Id here, it means, it will be created in each connector.
.mName(name)
.mCreatedByUserId(createdByUserId)
.mThisBankId(thisBankId)
.mThisAccountId(thisAccountId)
.mThisViewId(thisViewId)
.mOtherAccountRoutingScheme(StringHelpers.snakify(otherAccountRoutingScheme).toUpperCase)
.mOtherAccountRoutingAddress(otherAccountRoutingAddress)
.mOtherBankRoutingScheme(StringHelpers.snakify(otherBankRoutingScheme).toUpperCase)
.mOtherBankRoutingAddress(otherBankRoutingAddress)
.mOtherBranchRoutingAddress(otherBranchRoutingAddress)
.mOtherBranchRoutingScheme(StringHelpers.snakify(otherBranchRoutingScheme).toUpperCase)
.mIsBeneficiary(isBeneficiary)
.mDescription(description)
.mCurrency(currency)
.mOtherAccountSecondaryRoutingScheme(otherAccountSecondaryRoutingScheme)
.mOtherAccountSecondaryRoutingAddress(otherAccountSecondaryRoutingAddress)
.saveMe()
val mappedCounterparty = MappedCounterparty.create
.mCounterPartyId(APIUtil.createExplicitCounterpartyId) //We create the Counterparty_Id here, it means, it will be create in each connector.
.mName(name)
.mCreatedByUserId(createdByUserId)
.mThisBankId(thisBankId)
.mThisAccountId(thisAccountId)
.mThisViewId(thisViewId)
.mOtherAccountRoutingScheme(StringHelpers.snakify(otherAccountRoutingScheme).toUpperCase)
.mOtherAccountRoutingAddress(otherAccountRoutingAddress)
.mOtherBankRoutingScheme(StringHelpers.snakify(otherBankRoutingScheme).toUpperCase)
.mOtherBankRoutingAddress(otherBankRoutingAddress)
.mOtherBranchRoutingAddress(otherBranchRoutingAddress)
.mOtherBranchRoutingScheme(StringHelpers.snakify(otherBranchRoutingScheme).toUpperCase)
.mIsBeneficiary(isBeneficiary)
.mDescription(description)
.mCurrency(currency)
.mOtherAccountSecondaryRoutingScheme(otherAccountSecondaryRoutingScheme)
.mOtherAccountSecondaryRoutingAddress(otherAccountSecondaryRoutingAddress)
.saveMe()
// This is especially for OneToMany table, to save a List to database.
CounterpartyBespokes.counterpartyBespokers.vend
.createCounterpartyBespokes(mappedCounterparty.id.get, bespoke)
.map(mappedBespoke =>mappedCounterparty.mBespoke += mappedBespoke)
Some(mappedCounterparty)
// This is especially for OneToMany table, to save a List to database.
CounterpartyBespokes.counterpartyBespokers.vend
.createCounterpartyBespokes(mappedCounterparty.id.get, bespoke)
.map(mappedBespoke =>mappedCounterparty.mBespoke += mappedBespoke)
mappedCounterparty
}
}
override def checkCounterpartyExists(

View File

@ -1,25 +1,22 @@
package code.transactionrequests
import code.api.util.APIUtil.{DateWithMsFormat}
import code.api.util.{APIUtil, CallContext, CustomJsonFormats}
import code.api.util.APIUtil.DateWithMsFormat
import code.api.util.ErrorMessages._
import code.api.util.{APIUtil, CallContext, CustomJsonFormats}
import code.api.v2_1_0.TransactionRequestBodyCounterpartyJSON
import code.bankconnectors.LocalMappedConnectorInternal
import code.consent.Consents
import code.model._
import code.util.{AccountIdString, UUIDString}
import com.openbankproject.commons.model._
import com.openbankproject.commons.model.enums.{AccountRoutingScheme, TransactionRequestStatus}
import com.openbankproject.commons.model.enums.TransactionRequestTypes
import com.openbankproject.commons.model.enums.TransactionRequestTypes.{COUNTERPARTY, SEPA}
import com.openbankproject.commons.model.enums.{AccountRoutingScheme, TransactionRequestStatus, TransactionRequestTypes}
import net.liftweb.common.{Box, Failure, Full, Logger}
import net.liftweb.json
import net.liftweb.json.JsonAST.{JField, JObject, JString}
import net.liftweb.mapper._
import net.liftweb.util.Helpers._
import java.text.SimpleDateFormat
object MappedTransactionRequestProvider extends TransactionRequestProvider {
private val logger = Logger(classOf[TransactionRequestProvider])
@ -249,11 +246,11 @@ class MappedTransactionRequest extends LongKeyedMapper[MappedTransactionRequest]
object mChallenge_ChallengeType extends MappedString(this, 100)
object mCharge_Summary extends MappedString(this, 64)
object mCharge_Amount extends MappedString(this, 32)
object mCharge_Currency extends MappedString(this, 3)
object mCharge_Currency extends MappedString(this, 16)
object mcharge_Policy extends MappedString(this, 32)
//Body from http request: SANDBOX_TAN, FREE_FORM, SEPA and COUNTERPARTY should have the same following fields:
object mBody_Value_Currency extends MappedString(this, 3)
object mBody_Value_Currency extends MappedString(this, 16)
object mBody_Value_Amount extends MappedString(this, 32)
object mBody_Description extends MappedString(this, 2000)
// This is the details / body of the request (contains all fields in the body)
@ -268,7 +265,7 @@ class MappedTransactionRequest extends LongKeyedMapper[MappedTransactionRequest]
@deprecated("use mOtherBankRoutingAddress instead","2017-12-25")
object mTo_BankId extends UUIDString(this)
@deprecated("use mOtherAccountRoutingAddress instead","2017-12-25")
object mTo_AccountId extends AccountIdString(this)
object mTo_AccountId extends MappedString(this, 128)
//toCounterparty fields
object mName extends MappedString(this, 64)
@ -277,7 +274,7 @@ class MappedTransactionRequest extends LongKeyedMapper[MappedTransactionRequest]
object mThisViewId extends UUIDString(this)
object mCounterpartyId extends UUIDString(this)
object mOtherAccountRoutingScheme extends MappedString(this, 32) // TODO Add class for Scheme and Address
object mOtherAccountRoutingAddress extends MappedString(this, 64)
object mOtherAccountRoutingAddress extends MappedString(this, 128)
object mOtherBankRoutingScheme extends MappedString(this, 32)
object mOtherBankRoutingAddress extends MappedString(this, 64)
object mIsBeneficiary extends MappedBoolean(this)

View File

@ -1945,5 +1945,21 @@
<CcyNbr>961</CcyNbr>
<CcyMnrUnts>N.A.</CcyMnrUnts>
</CcyNtry>
<!-- Cardano (ADA) -->
<CcyNtry>
<CtryNm>ZZ12_Cardano</CtryNm>
<CcyNm>Cardano</CcyNm>
<Ccy>ada</Ccy>
<CcyNbr>null</CcyNbr>
<CcyMnrUnts>6</CcyMnrUnts> <!-- 1 ADA = 10^6 Lovelace -->
</CcyNtry>
<!-- Lovelace (The smallest unit of ADA) -->
<CcyNtry>
<CtryNm>ZZ12_Cardano_Lovelace</CtryNm>
<CcyNm>Lovelace</CcyNm>
<Ccy>lovelace</Ccy>
<CcyNbr>null</CcyNbr>
<CcyMnrUnts>0</CcyMnrUnts> <!-- Lovelace is the basic unit, no smaller subdivision -->
</CcyNtry>
</CcyTbl>
</ISO_4217>

View File

@ -0,0 +1,132 @@
package code.api
import code.api.util.APIUtil.{ResourceDoc, EmptyBody}
import code.api.OBPRestHelper
import com.openbankproject.commons.util.{ApiVersion, ScannedApiVersion}
import org.scalatest.{FlatSpec, Matchers, Tag}
/**
* Unit tests for OBPRestHelper.isAutoValidate method
*
* This test suite covers basic scenarios for the isAutoValidate function:
* - When doc.isValidateEnabled is true
* - When autoValidateAll is false
* - When doc.isValidateDisabled is true
* - When doc.implementedInApiVersion is not ScannedApiVersion
* - Basic version comparison logic
*/
class OBPRestHelperTest extends FlatSpec with Matchers {
object tag extends Tag("OBPRestHelper")
// Create a test instance of OBPRestHelper
private val testHelper = new OBPRestHelper {
val version: com.openbankproject.commons.util.ApiVersion = ScannedApiVersion("obp", "OBP", "v4.0.0")
val versionStatus: String = "stable"
}
// Helper method to create a ResourceDoc with specific validation settings
private def createResourceDoc(
version: ScannedApiVersion,
isValidateEnabled: Boolean = false,
isValidateDisabled: Boolean = false
): ResourceDoc = {
// Create a minimal ResourceDoc for testing
val doc = new ResourceDoc(
partialFunction = null, // Not used in our tests
implementedInApiVersion = version,
partialFunctionName = "testFunction",
requestVerb = "GET",
requestUrl = "/test",
summary = "Test endpoint",
description = "Test description",
exampleRequestBody = EmptyBody,
successResponseBody = EmptyBody,
errorResponseBodies = List(),
tags = List()
)
// Set validation flags using reflection or direct method calls
if (isValidateEnabled) {
doc.enableAutoValidate()
}
if (isValidateDisabled) {
doc.disableAutoValidate()
}
doc
}
"isAutoValidate" should "return true when doc.isValidateEnabled is true" taggedAs tag in {
val v4_0_0 = ScannedApiVersion("obp", "OBP", "v4.0.0")
val doc = createResourceDoc(v4_0_0, isValidateEnabled = true)
val result = testHelper.isAutoValidate(doc, autoValidateAll = false)
result shouldBe true
}
it should "return false when autoValidateAll is false and doc.isValidateEnabled is false" taggedAs tag in {
val v4_0_0 = ScannedApiVersion("obp", "OBP", "v4.0.0")
val doc = createResourceDoc(v4_0_0, isValidateEnabled = false)
val result = testHelper.isAutoValidate(doc, autoValidateAll = false)
result shouldBe false
}
it should "return false when doc.isValidateDisabled is true" taggedAs tag in {
val v4_0_0 = ScannedApiVersion("obp", "OBP", "v4.0.0")
val doc = createResourceDoc(v4_0_0, isValidateDisabled = true)
val result = testHelper.isAutoValidate(doc, autoValidateAll = true)
result shouldBe false
}
it should "return false for versions before v4.0.0" taggedAs tag in {
val v3_1_0 = ScannedApiVersion("obp", "OBP", "v3.1.0")
val doc = createResourceDoc(v3_1_0)
val result = testHelper.isAutoValidate(doc, autoValidateAll = true)
result shouldBe false
}
it should "return true for v4.0.0" taggedAs tag in {
val v4_0_0 = ScannedApiVersion("obp", "OBP", "v4.0.0")
val doc = createResourceDoc(v4_0_0)
val result = testHelper.isAutoValidate(doc, autoValidateAll = true)
result shouldBe true
}
it should "return true for versions after v4.0.0" taggedAs tag in {
val v5_0_0 = ScannedApiVersion("obp", "OBP", "v5.0.0")
val doc = createResourceDoc(v5_0_0)
val result = testHelper.isAutoValidate(doc, autoValidateAll = true)
result shouldBe true
}
it should "return true for v4.1.0 (major=4, minor=1)" taggedAs tag in {
val v4_1_0 = ScannedApiVersion("obp", "OBP", "v4.1.0")
val doc = createResourceDoc(v4_1_0)
val result = testHelper.isAutoValidate(doc, autoValidateAll = true)
result shouldBe true
}
it should "return false for malformed version strings" taggedAs tag in {
val malformedVersion = ScannedApiVersion("obp", "OBP", "v4") // Missing minor version
val doc = createResourceDoc(malformedVersion)
val result = testHelper.isAutoValidate(doc, autoValidateAll = true)
result shouldBe false
}
it should "prioritize isValidateEnabled over autoValidateAll" taggedAs tag in {
val v3_1_0 = ScannedApiVersion("obp", "OBP", "v3.1.0") // v3.1.0 normally wouldn't auto-validate
val doc = createResourceDoc(v3_1_0, isValidateEnabled = true)
val result = testHelper.isAutoValidate(doc, autoValidateAll = true)
result shouldBe true // Should be true because isValidateEnabled is true
}
it should "prioritize isValidateDisabled over autoValidateAll" taggedAs tag in {
val v4_0_0 = ScannedApiVersion("obp", "OBP", "v4.0.0") // v4.0.0 normally would auto-validate
val doc = createResourceDoc(v4_0_0, isValidateDisabled = true)
val result = testHelper.isAutoValidate(doc, autoValidateAll = true)
result shouldBe false // Should be false because isValidateDisabled is true
}
}

View File

@ -0,0 +1,442 @@
/**
Open Bank Project - API
Copyright (C) 2011-2019, TESOBE GmbH
This program 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 of the License, or
(at your option) any later version.
This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
Email: contact@tesobe.com
TESOBE GmbH
Osloerstrasse 16/17
Berlin 13359, Germany
This product includes software developed at
TESOBE (http://www.tesobe.com/)
*/
package code.api.v6_0_0
import code.api.Constant
import code.api.ResourceDocs1_4_0.SwaggerDefinitionsJSON
import code.api.util.ApiRole
import code.api.util.ApiRole._
import code.api.util.ErrorMessages._
import code.api.v4_0_0.TransactionRequestWithChargeJSON400
import code.entitlement.Entitlement
import code.methodrouting.MethodRoutingCommons
import com.github.dwickern.macros.NameOf.nameOf
import com.openbankproject.commons.model.{AmountOfMoneyJsonV121, ErrorMessage}
import com.openbankproject.commons.util.ApiVersion
import net.liftweb.json.Serialization.write
import org.scalatest.Tag
import code.api.util.APIUtil.OAuth._
import code.api.util.ApiRole
import code.api.v6_0_0.OBPAPI6_0_0.Implementations6_0_0
import code.entitlement.Entitlement
class CardanoTransactionRequestTest extends V600ServerSetup {
/**
* Test tags
* Example: To run tests with tag "getPermissions":
* mvn test -D tagsToInclude
*
* This is made possible by the scalatest maven plugin
*/
object VersionOfApi extends Tag(ApiVersion.v6_0_0.toString)
object CreateTransactionRequestCardano extends Tag(nameOf(Implementations6_0_0.createTransactionRequestCardano))
val testBankId = testBankId1.value
// This is a test account for Cardano transaction request tests, testAccountId0 is the walletId, passphrase is the passphrase for the wallet
val testAccountId = "62b27359c25d4f2a5f97acee521ac1df7ac5a606"
val passphrase = "StrongPassword123!"
val putCreateAccountJSONV310 = SwaggerDefinitionsJSON.createAccountRequestJsonV310.copy(
user_id = resourceUser1.userId,
balance = AmountOfMoneyJsonV121("lovelace", "0"),
)
feature("Create Cardano Transaction Request - v6.0.0") {
scenario("We will create Cardano transaction request - user is NOT logged in", CreateTransactionRequestCardano, VersionOfApi) {
When("We make a request v6.0.0")
val request600 = (v6_0_0_Request / "banks" / testBankId / "accounts" / testAccountId / Constant.SYSTEM_OWNER_VIEW_ID / "transaction-request-types" / "CARDANO" / "transaction-requests").POST
val cardanoTransactionRequestBody = TransactionRequestBodyCardanoJsonV600(
to = CardanoPaymentJsonV600(
address = "addr_test1qpv3se9ghq87ud29l0a8asy8nlqwd765e5zt4rc2z4mktqulwagn832cuzcjknfyxwzxz2p2kumx6n58tskugny6mrqs7fd23z",
amount = CardanoAmountJsonV600(
quantity = 1000000,
unit = "lovelace"
)
),
value = AmountOfMoneyJsonV121("lovelace", "1000000"),
passphrase = passphrase,
description = "Basic ADA transfer"
)
val response600 = makePostRequest(request600, write(cardanoTransactionRequestBody))
Then("We should get a 401")
response600.code should equal(401)
And("error should be " + UserNotLoggedIn)
response600.body.extract[ErrorMessage].message should equal(UserNotLoggedIn)
}
// scenario("We will create Cardano transaction request - user is logged in", CreateTransactionRequestCardano, VersionOfApi) {
// Entitlement.entitlement.vend.addEntitlement(testBankId, resourceUser1.userId, ApiRole.canCreateAccount.toString())
// val request = (v6_0_0_Request / "banks" / testBankId / "accounts" / testAccountId ).PUT <@(user1)
// val response = makePutRequest(request, write(putCreateAccountJSONV310))
// Then("We should get a 201")
// response.code should equal(201)
//
// When("We create a method routing for makePaymentv210 to cardano_vJun2025")
// val cardanoMethodRouting = MethodRoutingCommons(
// methodName = "makePaymentv210",
// connectorName = "cardano_vJun2025",
// isBankIdExactMatch = false,
// bankIdPattern = Some("*"),
// parameters = List()
// )
// val request310 = (v6_0_0_Request / "management" / "method_routings").POST <@(user1)
// Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanCreateMethodRouting.toString)
// val response310 = makePostRequest(request310, write(cardanoMethodRouting))
// response310.code should equal(201)
//
// When("We make a request v6.0.0")
// val request600 = (v6_0_0_Request / "banks" / testBankId / "accounts" / testAccountId / Constant.SYSTEM_OWNER_VIEW_ID / "transaction-request-types" / "CARDANO" / "transaction-requests").POST <@(user1)
// val cardanoTransactionRequestBody = TransactionRequestBodyCardanoJsonV600(
// to = CardanoPaymentJsonV600(
// address = "addr_test1qpv3se9ghq87ud29l0a8asy8nlqwd765e5zt4rc2z4mktqulwagn832cuzcjknfyxwzxz2p2kumx6n58tskugny6mrqs7fd23z",
// amount = CardanoAmountJsonV600(
// quantity = 1000000,
// unit = "lovelace"
// )
// ),
// value = AmountOfMoneyJsonV121("lovelace", "1000000"),
// passphrase = passphrase,
// description = "Basic ADA transfer"
// )
// val response600 = makePostRequest(request600, write(cardanoTransactionRequestBody))
// Then("We should get a 201")
// response600.code should equal(201)
// And("response should contain transaction request")
// val transactionRequest = response600.body.extract[TransactionRequestWithChargeJSON400]
// transactionRequest.status should not be empty
// }
//
// scenario("We will create Cardano transaction request with metadata - user is logged in", CreateTransactionRequestCardano, VersionOfApi) {
// Entitlement.entitlement.vend.addEntitlement(testBankId, resourceUser1.userId, ApiRole.canCreateAccount.toString())
// val request = (v6_0_0_Request / "banks" / testBankId / "accounts" / testAccountId ).PUT <@(user1)
// val response = makePutRequest(request, write(putCreateAccountJSONV310))
// Then("We should get a 201")
// response.code should equal(201)
//
// When("We create a method routing for makePaymentv210 to cardano_vJun2025")
// val cardanoMethodRouting = MethodRoutingCommons(
// methodName = "makePaymentv210",
// connectorName = "cardano_vJun2025",
// isBankIdExactMatch = false,
// bankIdPattern = Some("*"),
// parameters = List()
// )
// val request310 = (v6_0_0_Request / "management" / "method_routings").POST <@(user1)
// Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanCreateMethodRouting.toString)
// val response310 = makePostRequest(request310, write(cardanoMethodRouting))
// response310.code should equal(201)
//
// When("We make a request v6.0.0 with metadata")
// val request600 = (v6_0_0_Request / "banks" / testBankId / "accounts" / testAccountId / Constant.SYSTEM_OWNER_VIEW_ID / "transaction-request-types" / "CARDANO" / "transaction-requests").POST <@(user1)
// val cardanoTransactionRequestBody = TransactionRequestBodyCardanoJsonV600(
// to = CardanoPaymentJsonV600(
// address = "addr_test1qpv3se9ghq87ud29l0a8asy8nlqwd765e5zt4rc2z4mktqulwagn832cuzcjknfyxwzxz2p2kumx6n58tskugny6mrqs7fd23z",
// amount = CardanoAmountJsonV600(
// quantity = 1000000,
// unit = "lovelace"
// )
// ),
// value = AmountOfMoneyJsonV121("lovelace", "1000000"),
// passphrase = passphrase,
// description = "ADA transfer with metadata",
// metadata = Some(Map("202507022319" -> CardanoMetadataStringJsonV600("Hello Cardano")))
// )
// val response600 = makePostRequest(request600, write(cardanoTransactionRequestBody))
// Then("We should get a 201")
// response600.code should equal(201)
// And("response should contain transaction request")
// val transactionRequest = response600.body.extract[TransactionRequestWithChargeJSON400]
// transactionRequest.status should not be empty
// }
//
// scenario("We will create Cardano transaction request with token - user is logged in", CreateTransactionRequestCardano, VersionOfApi) {
// Entitlement.entitlement.vend.addEntitlement(testBankId, resourceUser1.userId, ApiRole.canCreateAccount.toString())
// val request = (v6_0_0_Request / "banks" / testBankId / "accounts" / testAccountId ).PUT <@(user1)
// val response = makePutRequest(request, write(putCreateAccountJSONV310))
// Then("We should get a 201")
// response.code should equal(201)
//
// When("We create a method routing for makePaymentv210 to cardano_vJun2025")
// val cardanoMethodRouting = MethodRoutingCommons(
// methodName = "makePaymentv210",
// connectorName = "cardano_vJun2025",
// isBankIdExactMatch = false,
// bankIdPattern = Some("*"),
// parameters = List()
// )
// val request310 = (v6_0_0_Request / "management" / "method_routings").POST <@(user1)
// Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanCreateMethodRouting.toString)
// val response310 = makePostRequest(request310, write(cardanoMethodRouting))
// response310.code should equal(201)
//
// When("We make a request v6.0.0 with token")
// val request600 = (v6_0_0_Request / "banks" / testBankId / "accounts" / testAccountId / Constant.SYSTEM_OWNER_VIEW_ID / "transaction-request-types" / "CARDANO" / "transaction-requests").POST <@(user1)
// val cardanoTransactionRequestBody = TransactionRequestBodyCardanoJsonV600(
// to = CardanoPaymentJsonV600(
// address = "addr_test1qpv3se9ghq87ud29l0a8asy8nlqwd765e5zt4rc2z4mktqulwagn832cuzcjknfyxwzxz2p2kumx6n58tskugny6mrqs7fd23z",
// amount = CardanoAmountJsonV600(
// quantity = 1000000,
// unit = "lovelace"
// ),
// assets = Some(List(CardanoAssetJsonV600(
// policy_id = "ef1954d3a058a96d89d959939aeb5b2948a3df2eb40f9a78d61e3d4f",
// asset_name = "OGCRA",
// quantity = 10
// )))
// ),
// value = AmountOfMoneyJsonV121("lovelace", "1000000"),
// passphrase = passphrase,
// description = "Token-only transfer"
// )
// val response600 = makePostRequest(request600, write(cardanoTransactionRequestBody))
// Then("We should get a 201")
// response600.code should equal(201)
// And("response should contain transaction request")
// val transactionRequest = response600.body.extract[TransactionRequestWithChargeJSON400]
// transactionRequest.status should not be empty
// }
//
// scenario("We will create Cardano transaction request with token and metadata - user is logged in", CreateTransactionRequestCardano, VersionOfApi) {
// When("We make a request v6.0.0 with token and metadata")
// val request600 = (v6_0_0_Request / "banks" / testBankId / "accounts" / testAccountId / Constant.SYSTEM_OWNER_VIEW_ID / "transaction-request-types" / "CARDANO" / "transaction-requests").POST <@(user1)
// val cardanoTransactionRequestBody = TransactionRequestBodyCardanoJsonV600(
// to = CardanoPaymentJsonV600(
// address = "addr_test1qpv3se9ghq87ud29l0a8asy8nlqwd765e5zt4rc2z4mktqulwagn832cuzcjknfyxwzxz2p2kumx6n58tskugny6mrqs7fd23z",
// amount = CardanoAmountJsonV600(
// quantity = 5000000,
// unit = "lovelace"
// ),
// assets = Some(List(CardanoAssetJsonV600(
// policy_id = "ef1954d3a058a96d89d959939aeb5b2948a3df2eb40f9a78d61e3d4f",
// asset_name = "OGCRA",
// quantity = 10
// )))
// ),
// value = AmountOfMoneyJsonV121("lovelace", "1000000"),
// passphrase = passphrase,
// description = "ADA transfer with token and metadata",
// metadata = Some(Map("202507022319" -> CardanoMetadataStringJsonV600("Hello Cardano with Token")))
// )
// val response600 = makePostRequest(request600, write(cardanoTransactionRequestBody))
// Then("We should get a 201")
// response600.code should equal(201)
// And("response should contain transaction request")
// val transactionRequest = response600.body.extract[TransactionRequestWithChargeJSON400]
// transactionRequest.status should not be empty
// }
//
// scenario("We will try to create Cardano transaction request for someone else account - user is logged in", CreateTransactionRequestCardano, VersionOfApi) {
// When("We make a request v6.0.0")
// val request600 = (v6_0_0_Request / "banks" / testBankId / "accounts" / testAccountId / Constant.SYSTEM_OWNER_VIEW_ID / "transaction-request-types" / "CARDANO" / "transaction-requests").POST <@(user2)
// val cardanoTransactionRequestBody = TransactionRequestBodyCardanoJsonV600(
// to = CardanoPaymentJsonV600(
// address = "addr_test1qpv3se9ghq87ud29l0a8asy8nlqwd765e5zt4rc2z4mktqulwagn832cuzcjknfyxwzxz2p2kumx6n58tskugny6mrqs7fd23z",
// amount = CardanoAmountJsonV600(
// quantity = 1000000,
// unit = "lovelace"
// )
// ),
// value = AmountOfMoneyJsonV121("lovelace", "1000000"),
// passphrase = passphrase,
// description = "Basic ADA transfer"
// )
// val response600 = makePostRequest(request600, write(cardanoTransactionRequestBody))
// Then("We should get a 403")
// response600.code should equal(403)
// And("error should be " + UserNoPermissionAccessView)
// response600.body.extract[ErrorMessage].message contains (UserNoPermissionAccessView) shouldBe (true)
// }
//
// scenario("We will try to create Cardano transaction request with invalid address format", CreateTransactionRequestCardano, VersionOfApi) {
// When("We make a request v6.0.0 with invalid address")
// val request600 = (v6_0_0_Request / "banks" / testBankId / "accounts" / testAccountId / Constant.SYSTEM_OWNER_VIEW_ID / "transaction-request-types" / "CARDANO" / "transaction-requests").POST <@(user1)
// val cardanoTransactionRequestBody = TransactionRequestBodyCardanoJsonV600(
// to = CardanoPaymentJsonV600(
// address = "invalid_address_format",
// amount = CardanoAmountJsonV600(
// quantity = 1000000,
// unit = "lovelace"
// )
// ),
// value = AmountOfMoneyJsonV121("lovelace", "1000000"),
// passphrase = passphrase,
// description = "Basic ADA transfer"
// )
// val response600 = makePostRequest(request600, write(cardanoTransactionRequestBody))
// Then("We should get a 400")
// response600.code should equal(400)
// And("error should contain invalid address message")
// response600.body.extract[ErrorMessage].message should include("Cardano address format is invalid")
// }
//
// scenario("We will try to create Cardano transaction request with missing amount", CreateTransactionRequestCardano, VersionOfApi) {
// When("We make a request v6.0.0 with missing amount")
// val request600 = (v6_0_0_Request / "banks" / testBankId / "accounts" / testAccountId / Constant.SYSTEM_OWNER_VIEW_ID / "transaction-request-types" / "CARDANO" / "transaction-requests").POST <@(user1)
// val invalidJson = """
// {
// "to": {
// "address": "addr_test1qpv3se9ghq87ud29l0a8asy8nlqwd765e5zt4rc2z4mktqulwagn832cuzcjknfyxwzxz2p2kumx6n58tskugny6mrqs7fd23z"
// },
// "value": {
// "currency": "lovelace",
// "amount": "1000000"
// },
// "passphrase": "StrongPassword123!",
// "description": "Basic ADA transfer"
// }
// """
// val response600 = makePostRequest(request600, invalidJson)
// Then("We should get a 400")
// response600.code should equal(400)
// And("error should contain invalid json format message")
// response600.body.extract[ErrorMessage].message should include("InvalidJsonFormat")
// }
//
// scenario("We will try to create Cardano transaction request with negative amount", CreateTransactionRequestCardano, VersionOfApi) {
// When("We make a request v6.0.0 with negative amount")
// val request600 = (v6_0_0_Request / "banks" / testBankId / "accounts" / testAccountId / Constant.SYSTEM_OWNER_VIEW_ID / "transaction-request-types" / "CARDANO" / "transaction-requests").POST <@(user1)
// val cardanoTransactionRequestBody = TransactionRequestBodyCardanoJsonV600(
// to = CardanoPaymentJsonV600(
// address = "addr_test1qpv3se9ghq87ud29l0a8asy8nlqwd765e5zt4rc2z4mktqulwagn832cuzcjknfyxwzxz2p2kumx6n58tskugny6mrqs7fd23z",
// amount = CardanoAmountJsonV600(
// quantity = -1000000,
// unit = "lovelace"
// )
// ),
// value = AmountOfMoneyJsonV121("lovelace", "1000000"),
// passphrase = passphrase,
// description = "Basic ADA transfer"
// )
// val response600 = makePostRequest(request600, write(cardanoTransactionRequestBody))
// Then("We should get a 400")
// response600.code should equal(400)
// And("error should contain invalid amount message")
// response600.body.extract[ErrorMessage].message should include("Cardano amount quantity must be non-negative")
// }
//
// scenario("We will try to create Cardano transaction request with invalid amount unit", CreateTransactionRequestCardano, VersionOfApi) {
// When("We make a request v6.0.0 with invalid amount unit")
// val request600 = (v6_0_0_Request / "banks" / testBankId / "accounts" / testAccountId / Constant.SYSTEM_OWNER_VIEW_ID / "transaction-request-types" / "CARDANO" / "transaction-requests").POST <@(user1)
// val cardanoTransactionRequestBody = TransactionRequestBodyCardanoJsonV600(
// to = CardanoPaymentJsonV600(
// address = "addr_test1qpv3se9ghq87ud29l0a8asy8nlqwd765e5zt4rc2z4mktqulwagn832cuzcjknfyxwzxz2p2kumx6n58tskugny6mrqs7fd23z",
// amount = CardanoAmountJsonV600(
// quantity = 1000000,
// unit = "abc" // Invalid unit, should be "lovelace"
// )
// ),
// value = AmountOfMoneyJsonV121("lovelace", "1000000"),
// passphrase = passphrase,
// description = "Basic ADA transfer"
// )
// val response600 = makePostRequest(request600, write(cardanoTransactionRequestBody))
// Then("We should get a 400")
// response600.code should equal(400)
// And("error should contain invalid unit message")
// response600.body.extract[ErrorMessage].message should include("Cardano amount unit must be 'lovelace'")
// }
//
// scenario("We will try to create Cardano transaction request with zero amount but no assets", CreateTransactionRequestCardano, VersionOfApi) {
// When("We make a request v6.0.0 with zero amount but no assets")
// val request600 = (v6_0_0_Request / "banks" / testBankId / "accounts" / testAccountId / Constant.SYSTEM_OWNER_VIEW_ID / "transaction-request-types" / "CARDANO" / "transaction-requests").POST <@(user1)
// val cardanoTransactionRequestBody = TransactionRequestBodyCardanoJsonV600(
// to = CardanoPaymentJsonV600(
// address = "addr_test1qpv3se9ghq87ud29l0a8asy8nlqwd765e5zt4rc2z4mktqulwagn832cuzcjknfyxwzxz2p2kumx6n58tskugny6mrqs7fd23z",
// amount = CardanoAmountJsonV600(
// quantity = 0,
// unit = "lovelace"
// )
// ),
// value = AmountOfMoneyJsonV121("lovelace", "0.0"),
// passphrase = passphrase,
// description = "Zero amount without assets"
// )
// val response600 = makePostRequest(request600, write(cardanoTransactionRequestBody))
// Then("We should get a 400")
// response600.code should equal(400)
// And("error should contain invalid amount message")
// response600.body.extract[ErrorMessage].message should include("Cardano transfer with zero amount must include assets")
// }
//
// scenario("We will try to create Cardano transaction request with invalid assets", CreateTransactionRequestCardano, VersionOfApi) {
// When("We make a request v6.0.0 with invalid assets")
// val request600 = (v6_0_0_Request / "banks" / testBankId / "accounts" / testAccountId / Constant.SYSTEM_OWNER_VIEW_ID / "transaction-request-types" / "CARDANO" / "transaction-requests").POST <@(user1)
// val cardanoTransactionRequestBody = TransactionRequestBodyCardanoJsonV600(
// to = CardanoPaymentJsonV600(
// address = "addr_test1qpv3se9ghq87ud29l0a8asy8nlqwd765e5zt4rc2z4mktqulwagn832cuzcjknfyxwzxz2p2kumx6n58tskugny6mrqs7fd23z",
// amount = CardanoAmountJsonV600(
// quantity = 0,
// unit = "lovelace"
// ),
// assets = Some(List(CardanoAssetJsonV600(
// policy_id = "",
// asset_name = "",
// quantity = 0
// )))
// ),
// value = AmountOfMoneyJsonV121("lovelace", "0.0"),
// passphrase = passphrase,
// description = "Invalid assets"
// )
// val response600 = makePostRequest(request600, write(cardanoTransactionRequestBody))
// Then("We should get a 400")
// response600.code should equal(400)
// And("error should contain invalid assets message")
// response600.body.extract[ErrorMessage].message should include("Cardano assets must have valid policy_id and asset_name")
// }
//
// scenario("We will try to create Cardano transaction request with invalid metadata", CreateTransactionRequestCardano, VersionOfApi) {
// When("We make a request v6.0.0 with invalid metadata")
// val request600 = (v6_0_0_Request / "banks" / testBankId / "accounts" / testAccountId / Constant.SYSTEM_OWNER_VIEW_ID / "transaction-request-types" / "CARDANO" / "transaction-requests").POST <@(user1)
// val cardanoTransactionRequestBody = TransactionRequestBodyCardanoJsonV600(
// to = CardanoPaymentJsonV600(
// address = "addr_test1qpv3se9ghq87ud29l0a8asy8nlqwd765e5zt4rc2z4mktqulwagn832cuzcjknfyxwzxz2p2kumx6n58tskugny6mrqs7fd23z",
// amount = CardanoAmountJsonV600(
// quantity = 1000000,
// unit = "lovelace"
// )
// ),
// value = AmountOfMoneyJsonV121("lovelace", "1000000"),
// passphrase = passphrase,
// description = "ADA transfer with invalid metadata",
// metadata = Some(Map("" -> CardanoMetadataStringJsonV600("")))
// )
// val response600 = makePostRequest(request600, write(cardanoTransactionRequestBody))
// Then("We should get a 400")
// response600.code should equal(400)
// And("error should contain invalid metadata message")
// response600.body.extract[ErrorMessage].message should include("Cardano metadata must have valid structure")
// }
}
}

View File

@ -0,0 +1,13 @@
package code.api.v6_0_0
import code.setup.{DefaultUsers, ServerSetupWithTestData}
import dispatch.Req
trait V600ServerSetup extends ServerSetupWithTestData with DefaultUsers {
def v4_0_0_Request: Req = baseRequest / "obp" / "v4.0.0"
def v5_0_0_Request: Req = baseRequest / "obp" / "v5.0.0"
def v5_1_0_Request: Req = baseRequest / "obp" / "v5.1.0"
def v6_0_0_Request: Req = baseRequest / "obp" / "v6.0.0"
}

View File

@ -27,8 +27,6 @@ TESOBE (http://www.tesobe.com/)
package code.setup
import java.net.URI
import _root_.net.liftweb.json.JsonAST.JObject
import code.TestServer
import code.api.util.APIUtil._
@ -51,11 +49,11 @@ trait ServerSetup extends FeatureSpec with SendServerRequests
setPropsValues("dauth.host" -> "127.0.0.1")
setPropsValues("jwt_token_secret"->"your-at-least-256-bit-secret-token")
setPropsValues("jwt.public_key_rsa" -> "src/test/resources/cert/public_dauth.pem")
setPropsValues("transactionRequests_supported_types" -> "SEPA,SANDBOX_TAN,FREE_FORM,COUNTERPARTY,ACCOUNT,ACCOUNT_OTP,SIMPLE,CARD,AGENT_CASH_WITHDRAWAL")
setPropsValues("transactionRequests_supported_types" -> "SEPA,SANDBOX_TAN,FREE_FORM,COUNTERPARTY,ACCOUNT,ACCOUNT_OTP,SIMPLE,CARD,AGENT_CASH_WITHDRAWAL,CARDANO")
setPropsValues("CARD_OTP_INSTRUCTION_TRANSPORT" -> "DUMMY")
setPropsValues("AGENT_CASH_WITHDRAWAL_OTP_INSTRUCTION_TRANSPORT" -> "DUMMY")
setPropsValues("api_instance_id" -> "1_final")
setPropsValues("starConnector_supported_types" -> "mapped,internal")
setPropsValues("starConnector_supported_types" -> "mapped,internal,cardano_vJun2025")
setPropsValues("connector" -> "star")
// Berlin Group

View File

@ -1,11 +1,11 @@
package com.openbankproject.commons.model.enums
import java.time.format.DateTimeFormatter
import com.openbankproject.commons.util.{EnumValue, JsonAble, OBPEnumeration}
import net.liftweb.common.Box
import net.liftweb.json.JsonAST.{JNothing, JString}
import net.liftweb.json.{Formats, JBool, JDouble, JInt, JValue}
import net.liftweb.json._
import java.time.format.DateTimeFormatter
sealed trait UserAttributeType extends EnumValue
@ -113,6 +113,7 @@ object TransactionRequestTypes extends OBPEnumeration[TransactionRequestTypes]{
object CROSS_BORDER_CREDIT_TRANSFERS extends Value
object REFUND extends Value
object AGENT_CASH_WITHDRAWAL extends Value
object CARDANO extends Value
}
sealed trait StrongCustomerAuthentication extends EnumValue

View File

@ -1,9 +1,8 @@
package com.openbankproject.commons.util
import com.openbankproject.commons.util.ApiShortVersions.Value
import net.liftweb.json._
import java.util.concurrent.ConcurrentHashMap
import net.liftweb.json.{Formats, JField, JObject, JString, JsonAST}
object ApiStandards extends Enumeration {
type ApiStandards = Value
@ -23,6 +22,7 @@ object ApiShortVersions extends Enumeration {
val `v4.0.0` = Value("v4.0.0")
val `v5.0.0` = Value("v5.0.0")
val `v5.1.0` = Value("v5.1.0")
val `v6.0.0` = Value("v6.0.0")
val `dynamic-endpoint` = Value("dynamic-endpoint")
val `dynamic-entity` = Value("dynamic-entity")
}
@ -113,6 +113,7 @@ object ApiVersion {
val v4_0_0 = ScannedApiVersion(urlPrefix,ApiStandards.obp.toString,ApiShortVersions.`v4.0.0`.toString)
val v5_0_0 = ScannedApiVersion(urlPrefix,ApiStandards.obp.toString,ApiShortVersions.`v5.0.0`.toString)
val v5_1_0 = ScannedApiVersion(urlPrefix,ApiStandards.obp.toString,ApiShortVersions.`v5.1.0`.toString)
val v6_0_0 = ScannedApiVersion(urlPrefix,ApiStandards.obp.toString,ApiShortVersions.`v6.0.0`.toString)
val `dynamic-endpoint` = ScannedApiVersion(urlPrefix,ApiStandards.obp.toString,ApiShortVersions.`dynamic-endpoint`.toString)
val `dynamic-entity` = ScannedApiVersion(urlPrefix,ApiStandards.obp.toString,ApiShortVersions.`dynamic-entity`.toString)
@ -129,6 +130,7 @@ object ApiVersion {
v4_0_0 ::
v5_0_0 ::
v5_1_0 ::
v6_0_0 ::
`dynamic-endpoint` ::
`dynamic-entity`::
Nil