diff --git a/.gitignore b/.gitignore
index 270764ea1..8d4567230 100644
--- a/.gitignore
+++ b/.gitignore
@@ -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
\ No newline at end of file
diff --git a/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala b/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala
index 9ae42172e..525b20a74 100644
--- a/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala
+++ b/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala
@@ -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)
////////////////////////////////////////////////////
diff --git a/obp-api/src/main/scala/code/api/OBPRestHelper.scala b/obp-api/src/main/scala/code/api/OBPRestHelper.scala
index f707e265b..d78be8924 100644
--- a/obp-api/src/main/scala/code/api/OBPRestHelper.scala
+++ b/obp-api/src/main/scala/code/api/OBPRestHelper.scala
@@ -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)
diff --git a/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/ResourceDocs140.scala b/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/ResourceDocs140.scala
index c53650ef7..d7d3cc31a 100644
--- a/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/ResourceDocs140.scala
+++ b/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/ResourceDocs140.scala
@@ -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
+ })
+ })
+ }
}
\ No newline at end of file
diff --git a/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala b/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala
index cf159555f..34c42dd30 100644
--- a/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala
+++ b/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala
@@ -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()
diff --git a/obp-api/src/main/scala/code/api/util/APIUtil.scala b/obp-api/src/main/scala/code/api/util/APIUtil.scala
index 5def035d3..700d5e742 100644
--- a/obp-api/src/main/scala/code/api/util/APIUtil.scala
+++ b/obp-api/src/main/scala/code/api/util/APIUtil.scala
@@ -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))
diff --git a/obp-api/src/main/scala/code/api/util/ErrorMessages.scala b/obp-api/src/main/scala/code/api/util/ErrorMessages.scala
index c2a49fa27..07da1e107 100644
--- a/obp-api/src/main/scala/code/api/util/ErrorMessages.scala
+++ b/obp-api/src/main/scala/code/api/util/ErrorMessages.scala
@@ -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."
diff --git a/obp-api/src/main/scala/code/api/util/NewStyle.scala b/obp-api/src/main/scala/code/api/util/NewStyle.scala
index 24c9bc80e..8a297199c 100644
--- a/obp-api/src/main/scala/code/api/util/NewStyle.scala
+++ b/obp-api/src/main/scala/code/api/util/NewStyle.scala
@@ -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
- * - Future(result of the evaluation of f) if f doesn't throw any exception
- *
- a Failure if f throws an exception with message = failMsg and code = failCode
- *
- */
- 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)
diff --git a/obp-api/src/main/scala/code/api/util/migration/Migration.scala b/obp-api/src/main/scala/code/api/util/migration/Migration.scala
index 7da81d82c..166a54342 100644
--- a/obp-api/src/main/scala/code/api/util/migration/Migration.scala
+++ b/obp-api/src/main/scala/code/api/util/migration/Migration.scala
@@ -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.")
diff --git a/obp-api/src/main/scala/code/api/util/migration/MigrationOfMappedTransactionRequestFieldsLength.scala b/obp-api/src/main/scala/code/api/util/migration/MigrationOfMappedTransactionRequestFieldsLength.scala
new file mode 100644
index 000000000..252f066fb
--- /dev/null
+++ b/obp-api/src/main/scala/code/api/util/migration/MigrationOfMappedTransactionRequestFieldsLength.scala
@@ -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
+ }
+ }
+}
+
diff --git a/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala b/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala
index a1d6f4ccd..db729eb4a 100644
--- a/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala
+++ b/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala
@@ -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))
- }
- }
-
+
}
diff --git a/obp-api/src/main/scala/code/api/v5_1_0/JSONFactory5.1.0.scala b/obp-api/src/main/scala/code/api/v5_1_0/JSONFactory5.1.0.scala
index b05898569..0d15f3cd8 100644
--- a/obp-api/src/main/scala/code/api/v5_1_0/JSONFactory5.1.0.scala
+++ b/obp-api/src/main/scala/code/api/v5_1_0/JSONFactory5.1.0.scala
@@ -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
diff --git a/obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala b/obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala
new file mode 100644
index 000000000..e251b9430
--- /dev/null
+++ b/obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala
@@ -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
+}
+
diff --git a/obp-api/src/main/scala/code/api/v6_0_0/JSONFactory6.0.0.scala b/obp-api/src/main/scala/code/api/v6_0_0/JSONFactory6.0.0.scala
new file mode 100644
index 000000000..7f4dd441d
--- /dev/null
+++ b/obp-api/src/main/scala/code/api/v6_0_0/JSONFactory6.0.0.scala
@@ -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 .
+ * *
+ * 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{
+
+}
\ No newline at end of file
diff --git a/obp-api/src/main/scala/code/api/v6_0_0/OBPAPI6_0_0.scala b/obp-api/src/main/scala/code/api/v6_0_0/OBPAPI6_0_0.scala
new file mode 100644
index 000000000..f5b7f6b43
--- /dev/null
+++ b/obp-api/src/main/scala/code/api/v6_0_0/OBPAPI6_0_0.scala
@@ -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 .
+
+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
+ })
+}
diff --git a/obp-api/src/main/scala/code/bankconnectors/LocalMappedConnector.scala b/obp-api/src/main/scala/code/bankconnectors/LocalMappedConnector.scala
index 0bfe96e38..7784852dd 100644
--- a/obp-api/src/main/scala/code/bankconnectors/LocalMappedConnector.scala
+++ b/obp-api/src/main/scala/code/bankconnectors/LocalMappedConnector.scala
@@ -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(_) =>
diff --git a/obp-api/src/main/scala/code/bankconnectors/LocalMappedConnectorInternal.scala b/obp-api/src/main/scala/code/bankconnectors/LocalMappedConnectorInternal.scala
index 56e5c4ac3..e25d12d75 100644
--- a/obp-api/src/main/scala/code/bankconnectors/LocalMappedConnectorInternal.scala
+++ b/obp-api/src/main/scala/code/bankconnectors/LocalMappedConnectorInternal.scala
@@ -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))
+ }
+ }
+
+
}
diff --git a/obp-api/src/main/scala/code/bankconnectors/cardano/CardanoConnector_vJun2025.scala b/obp-api/src/main/scala/code/bankconnectors/cardano/CardanoConnector_vJun2025.scala
index 7dce2dd03..01dbcc262 100644
--- a/obp-api/src/main/scala/code/bankconnectors/cardano/CardanoConnector_vJun2025.scala
+++ b/obp-api/src/main/scala/code/bankconnectors/cardano/CardanoConnector_vJun2025.scala
@@ -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
\ No newline at end of file
diff --git a/obp-api/src/main/scala/code/metadata/counterparties/MapperCounterparties.scala b/obp-api/src/main/scala/code/metadata/counterparties/MapperCounterparties.scala
index 1066aece8..08c244a22 100644
--- a/obp-api/src/main/scala/code/metadata/counterparties/MapperCounterparties.scala
+++ b/obp-api/src/main/scala/code/metadata/counterparties/MapperCounterparties.scala
@@ -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(
diff --git a/obp-api/src/main/scala/code/transactionrequests/MappedTransactionRequestProvider.scala b/obp-api/src/main/scala/code/transactionrequests/MappedTransactionRequestProvider.scala
index b413c2ea1..824b1376a 100644
--- a/obp-api/src/main/scala/code/transactionrequests/MappedTransactionRequestProvider.scala
+++ b/obp-api/src/main/scala/code/transactionrequests/MappedTransactionRequestProvider.scala
@@ -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)
diff --git a/obp-api/src/main/webapp/media/xml/ISOCurrencyCodes.xml b/obp-api/src/main/webapp/media/xml/ISOCurrencyCodes.xml
index dac734d6b..eec889297 100644
--- a/obp-api/src/main/webapp/media/xml/ISOCurrencyCodes.xml
+++ b/obp-api/src/main/webapp/media/xml/ISOCurrencyCodes.xml
@@ -1945,5 +1945,21 @@
961
N.A.
+
+
+ Cardano
+ Cardano
+ ada
+ null
+ 6
+
+
+
+ Cardano_Lovelace
+ Lovelace
+ lovelace
+ null
+ 0
+
\ No newline at end of file
diff --git a/obp-api/src/test/scala/code/api/OBPRestHelperTest.scala b/obp-api/src/test/scala/code/api/OBPRestHelperTest.scala
new file mode 100644
index 000000000..a8bfc06c1
--- /dev/null
+++ b/obp-api/src/test/scala/code/api/OBPRestHelperTest.scala
@@ -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
+ }
+}
+
diff --git a/obp-api/src/test/scala/code/api/v6_0_0/CardanoTransactionRequestTest.scala b/obp-api/src/test/scala/code/api/v6_0_0/CardanoTransactionRequestTest.scala
new file mode 100644
index 000000000..7348a3821
--- /dev/null
+++ b/obp-api/src/test/scala/code/api/v6_0_0/CardanoTransactionRequestTest.scala
@@ -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 .
+
+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")
+// }
+ }
+}
\ No newline at end of file
diff --git a/obp-api/src/test/scala/code/api/v6_0_0/V600ServerSetup.scala b/obp-api/src/test/scala/code/api/v6_0_0/V600ServerSetup.scala
new file mode 100644
index 000000000..ae9e71ced
--- /dev/null
+++ b/obp-api/src/test/scala/code/api/v6_0_0/V600ServerSetup.scala
@@ -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"
+
+}
\ No newline at end of file
diff --git a/obp-api/src/test/scala/code/setup/ServerSetup.scala b/obp-api/src/test/scala/code/setup/ServerSetup.scala
index 31ff36032..176ccfcd2 100644
--- a/obp-api/src/test/scala/code/setup/ServerSetup.scala
+++ b/obp-api/src/test/scala/code/setup/ServerSetup.scala
@@ -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
diff --git a/obp-commons/src/main/scala/com/openbankproject/commons/model/enums/Enumerations.scala b/obp-commons/src/main/scala/com/openbankproject/commons/model/enums/Enumerations.scala
index a0ae6ca04..67c928afb 100644
--- a/obp-commons/src/main/scala/com/openbankproject/commons/model/enums/Enumerations.scala
+++ b/obp-commons/src/main/scala/com/openbankproject/commons/model/enums/Enumerations.scala
@@ -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
diff --git a/obp-commons/src/main/scala/com/openbankproject/commons/util/ApiVersion.scala b/obp-commons/src/main/scala/com/openbankproject/commons/util/ApiVersion.scala
index ad237ba7e..0a5617d18 100644
--- a/obp-commons/src/main/scala/com/openbankproject/commons/util/ApiVersion.scala
+++ b/obp-commons/src/main/scala/com/openbankproject/commons/util/ApiVersion.scala
@@ -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