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 176726d85..8d824c085 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 - */ - 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..a6fece789 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. + + + ZZ12_Cardano + Cardano + ada + null + 6 + + + + ZZ12_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