mirror of
https://github.com/OpenBankProject/OBP-API.git
synced 2026-02-06 14:46:49 +00:00
Merge branch 'develop' of github.com:OpenBankProject/OBP-API into develop
This commit is contained in:
commit
ea83fce8ff
7
.gitignore
vendored
7
.gitignore
vendored
@ -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
|
||||
@ -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)
|
||||
////////////////////////////////////////////////////
|
||||
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
@ -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()
|
||||
|
||||
@ -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))
|
||||
|
||||
@ -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."
|
||||
|
||||
@ -769,17 +769,37 @@ object NewStyle extends MdcLoggable{
|
||||
def isEnabledTransactionRequests(callContext: Option[CallContext]): Future[Box[Unit]] = Helper.booleanToFuture(failMsg = TransactionRequestsNotEnabled, cc=callContext)(APIUtil.getPropsAsBoolValue("transactionRequests_enabled", false))
|
||||
|
||||
/**
|
||||
* Wraps a Future("try") block around the function f and
|
||||
* @param f - the block of code to evaluate
|
||||
* @return <ul>
|
||||
* <li>Future(result of the evaluation of f) if f doesn't throw any exception
|
||||
* <li>a Failure if f throws an exception with message = failMsg and code = failCode
|
||||
* </ul>
|
||||
*/
|
||||
def tryons[T](failMsg: String, failCode: Int = 400, callContext: Option[CallContext])(f: => T)(implicit m: Manifest[T]): Future[T]= {
|
||||
* Wraps a computation `f` in a Future, capturing exceptions and returning detailed error messages.
|
||||
*
|
||||
* @param failMsg Base error message to return if the computation fails.
|
||||
* @param failCode HTTP status code to return on failure (default: 400).
|
||||
* @param callContext Optional call context for logging or metadata.
|
||||
* @param f The computation to execute (call-by-name to defer evaluation).
|
||||
* @param m Implicit Manifest for type `T` (handled by Scala compiler).
|
||||
* @return Future[T] Success: Result of `f`; Failure: Detailed error message.
|
||||
*/
|
||||
def tryons[T](
|
||||
failMsg: String,
|
||||
failCode: Int = 400,
|
||||
callContext: Option[CallContext]
|
||||
)(f: => T)(implicit m: Manifest[T]): Future[T] = {
|
||||
Future {
|
||||
tryo {
|
||||
f
|
||||
try {
|
||||
// Attempt to execute `f` and wrap the result in `Full` (success) or `Failure` (error)
|
||||
tryo(f) match {
|
||||
case Full(result) =>
|
||||
Full(result) // Success: Forward the result
|
||||
case Failure(msg, _, _) =>
|
||||
// `tryo` encountered an exception (e.g., validation error)
|
||||
Failure(s"$failMsg. Details: $msg", Empty, Empty)
|
||||
case Empty =>
|
||||
// Edge case: Empty result (unlikely but handled defensively)
|
||||
Failure(s"$failMsg. Details: Empty result", Empty, Empty)
|
||||
}
|
||||
} catch {
|
||||
case e: Exception =>
|
||||
// Directly caught exception (e.g., JSON parsing error)
|
||||
Failure(s"$failMsg. Details: ${e.getMessage}", Full(e), Empty)
|
||||
}
|
||||
} map {
|
||||
x => unboxFullOrFail(x, callContext, failMsg, failCode)
|
||||
|
||||
@ -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.")
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
84
obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala
Normal file
84
obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala
Normal file
@ -0,0 +1,84 @@
|
||||
package code.api.v6_0_0
|
||||
|
||||
import code.api.ResourceDocs1_4_0.SwaggerDefinitionsJSON._
|
||||
import code.api.util.APIUtil._
|
||||
import code.api.util.ApiTag._
|
||||
import code.api.util.ErrorMessages.{$UserNotLoggedIn, InvalidJsonFormat, UnknownError, _}
|
||||
import code.api.util.FutureUtil.EndpointContext
|
||||
import code.bankconnectors.LocalMappedConnectorInternal
|
||||
import code.bankconnectors.LocalMappedConnectorInternal._
|
||||
import com.github.dwickern.macros.NameOf.nameOf
|
||||
import com.openbankproject.commons.model._
|
||||
import com.openbankproject.commons.util.{ApiVersion, ScannedApiVersion}
|
||||
import net.liftweb.http.rest.RestHelper
|
||||
|
||||
import scala.collection.immutable.{List, Nil}
|
||||
import scala.collection.mutable.ArrayBuffer
|
||||
|
||||
|
||||
trait APIMethods600 {
|
||||
self: RestHelper =>
|
||||
|
||||
val Implementations6_0_0 = new Implementations600()
|
||||
|
||||
class Implementations600 {
|
||||
|
||||
val implementedInApiVersion: ScannedApiVersion = ApiVersion.v6_0_0
|
||||
|
||||
private val staticResourceDocs = ArrayBuffer[ResourceDoc]()
|
||||
def resourceDocs = staticResourceDocs
|
||||
|
||||
val apiRelations = ArrayBuffer[ApiRelation]()
|
||||
val codeContext = CodeContext(staticResourceDocs, apiRelations)
|
||||
|
||||
staticResourceDocs += ResourceDoc(
|
||||
createTransactionRequestCardano,
|
||||
implementedInApiVersion,
|
||||
nameOf(createTransactionRequestCardano),
|
||||
"POST",
|
||||
"/banks/BANK_ID/accounts/ACCOUNT_ID/owner/transaction-request-types/CARDANO/transaction-requests",
|
||||
"Create Transaction Request (CARDANO)",
|
||||
s"""
|
||||
|
|
||||
|For sandbox mode, it will use the Cardano Preprod Network.
|
||||
|The accountId can be the wallet_id for now, as it uses cardano-wallet in the backend.
|
||||
|
|
||||
|${transactionRequestGeneralText}
|
||||
|
|
||||
""".stripMargin,
|
||||
transactionRequestBodyCardanoJsonV600,
|
||||
transactionRequestWithChargeJSON400,
|
||||
List(
|
||||
$UserNotLoggedIn,
|
||||
$BankNotFound,
|
||||
$BankAccountNotFound,
|
||||
InsufficientAuthorisationToCreateTransactionRequest,
|
||||
InvalidTransactionRequestType,
|
||||
InvalidJsonFormat,
|
||||
NotPositiveAmount,
|
||||
InvalidTransactionRequestCurrency,
|
||||
TransactionDisabled,
|
||||
UnknownError
|
||||
),
|
||||
List(apiTagTransactionRequest, apiTagPSD2PIS, apiTagPsd2)
|
||||
)
|
||||
|
||||
lazy val createTransactionRequestCardano: OBPEndpoint = {
|
||||
case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: ViewId(viewId) :: "transaction-request-types" ::
|
||||
"CARDANO" :: "transaction-requests" :: Nil JsonPost json -> _ =>
|
||||
cc => implicit val ec = EndpointContext(Some(cc))
|
||||
val transactionRequestType = TransactionRequestType("CARDANO")
|
||||
LocalMappedConnectorInternal.createTransactionRequest(bankId, accountId, viewId , transactionRequestType, json)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
object APIMethods600 extends RestHelper with APIMethods600 {
|
||||
lazy val newStyleEndpoints: List[(String, String)] = Implementations6_0_0.resourceDocs.map {
|
||||
rd => (rd.partialFunctionName, rd.implementedInApiVersion.toString())
|
||||
}.toList
|
||||
}
|
||||
|
||||
@ -0,0 +1,64 @@
|
||||
/**
|
||||
* Open Bank Project - API
|
||||
* Copyright (C) 2011-2019, TESOBE GmbH
|
||||
* *
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
* *
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
* *
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
* *
|
||||
* Email: contact@tesobe.com
|
||||
* TESOBE GmbH
|
||||
* Osloerstrasse 16/17
|
||||
* Berlin 13359, Germany
|
||||
* *
|
||||
* This product includes software developed at
|
||||
* TESOBE (http://www.tesobe.com/)
|
||||
*
|
||||
*/
|
||||
package code.api.v6_0_0
|
||||
|
||||
import code.api.util._
|
||||
import code.util.Helper.MdcLoggable
|
||||
import com.openbankproject.commons.model._
|
||||
|
||||
case class CardanoPaymentJsonV600(
|
||||
address: String,
|
||||
amount: CardanoAmountJsonV600,
|
||||
assets: Option[List[CardanoAssetJsonV600]] = None
|
||||
)
|
||||
|
||||
case class CardanoAmountJsonV600(
|
||||
quantity: Long,
|
||||
unit: String // "lovelace"
|
||||
)
|
||||
|
||||
case class CardanoAssetJsonV600(
|
||||
policy_id: String,
|
||||
asset_name: String,
|
||||
quantity: Long
|
||||
)
|
||||
|
||||
case class CardanoMetadataStringJsonV600(
|
||||
string: String
|
||||
)
|
||||
|
||||
case class TransactionRequestBodyCardanoJsonV600(
|
||||
to: CardanoPaymentJsonV600,
|
||||
value: AmountOfMoneyJsonV121,
|
||||
passphrase: String,
|
||||
description: String,
|
||||
metadata: Option[Map[String, CardanoMetadataStringJsonV600]] = None
|
||||
) extends TransactionRequestCommonBodyJSON
|
||||
|
||||
object JSONFactory600 extends CustomJsonFormats with MdcLoggable{
|
||||
|
||||
}
|
||||
122
obp-api/src/main/scala/code/api/v6_0_0/OBPAPI6_0_0.scala
Normal file
122
obp-api/src/main/scala/code/api/v6_0_0/OBPAPI6_0_0.scala
Normal file
@ -0,0 +1,122 @@
|
||||
/**
|
||||
Open Bank Project - API
|
||||
Copyright (C) 2011-2019, TESOBE GmbH.
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
Email: contact@tesobe.com
|
||||
TESOBE GmbH.
|
||||
Osloer Strasse 16/17
|
||||
Berlin 13359, Germany
|
||||
|
||||
This product includes software developed at
|
||||
TESOBE (http://www.tesobe.com/)
|
||||
|
||||
*/
|
||||
package code.api.v6_0_0
|
||||
|
||||
import code.api.OBPRestHelper
|
||||
import code.api.util.APIUtil.{OBPEndpoint, getAllowedEndpoints}
|
||||
import code.api.util.VersionedOBPApis
|
||||
import code.api.v1_3_0.APIMethods130
|
||||
import code.api.v1_4_0.APIMethods140
|
||||
import code.api.v2_0_0.APIMethods200
|
||||
import code.api.v2_1_0.APIMethods210
|
||||
import code.api.v2_2_0.APIMethods220
|
||||
import code.api.v3_0_0.APIMethods300
|
||||
import code.api.v3_0_0.custom.CustomAPIMethods300
|
||||
import code.api.v3_1_0.APIMethods310
|
||||
import code.api.v4_0_0.APIMethods400
|
||||
import code.api.v5_0_0.APIMethods500
|
||||
import code.api.v5_1_0.{APIMethods510, OBPAPI5_1_0}
|
||||
import code.util.Helper.MdcLoggable
|
||||
import com.github.dwickern.macros.NameOf.nameOf
|
||||
import com.openbankproject.commons.util.{ApiVersion, ApiVersionStatus}
|
||||
import net.liftweb.common.{Box, Full}
|
||||
import net.liftweb.http.{LiftResponse, PlainTextResponse}
|
||||
import org.apache.http.HttpStatus
|
||||
|
||||
/*
|
||||
This file defines which endpoints from all the versions are available in v5.0.0
|
||||
*/
|
||||
object OBPAPI6_0_0 extends OBPRestHelper
|
||||
with APIMethods130
|
||||
with APIMethods140
|
||||
with APIMethods200
|
||||
with APIMethods210
|
||||
with APIMethods220
|
||||
with APIMethods300
|
||||
with CustomAPIMethods300
|
||||
with APIMethods310
|
||||
with APIMethods400
|
||||
with APIMethods500
|
||||
with APIMethods510
|
||||
with APIMethods600
|
||||
with MdcLoggable
|
||||
with VersionedOBPApis{
|
||||
|
||||
val version : ApiVersion = ApiVersion.v6_0_0
|
||||
|
||||
val versionStatus = ApiVersionStatus.BLEEDING_EDGE.toString
|
||||
|
||||
// Possible Endpoints from 5.1.0, exclude one endpoint use - method,exclude multiple endpoints use -- method,
|
||||
// e.g getEndpoints(Implementations5_0_0) -- List(Implementations5_0_0.genericEndpoint, Implementations5_0_0.root)
|
||||
lazy val endpointsOf6_0_0 = getEndpoints(Implementations6_0_0)
|
||||
|
||||
lazy val excludeEndpoints =
|
||||
nameOf(Implementations3_0_0.getUserByUsername) :: // following 4 endpoints miss Provider parameter in the URL, we introduce new ones in V600.
|
||||
nameOf(Implementations3_1_0.getBadLoginStatus) ::
|
||||
nameOf(Implementations3_1_0.unlockUser) ::
|
||||
nameOf(Implementations4_0_0.lockUser) ::
|
||||
nameOf(Implementations4_0_0.createUserWithAccountAccess) :: // following 3 endpoints miss ViewId parameter in the URL, we introduce new ones in V600.
|
||||
nameOf(Implementations4_0_0.grantUserAccessToView) ::
|
||||
nameOf(Implementations4_0_0.revokeUserAccessToView) ::
|
||||
nameOf(Implementations4_0_0.revokeGrantUserAccessToViews) ::// this endpoint is forbidden in V600, we do not support multi views in one endpoint from V600.
|
||||
Nil
|
||||
|
||||
// if old version ResourceDoc objects have the same name endpoint with new version, omit old version ResourceDoc.
|
||||
def allResourceDocs = collectResourceDocs(
|
||||
OBPAPI5_1_0.allResourceDocs,
|
||||
Implementations6_0_0.resourceDocs
|
||||
).filterNot(it => it.partialFunctionName.matches(excludeEndpoints.mkString("|")))
|
||||
|
||||
// all endpoints
|
||||
private val endpoints: List[OBPEndpoint] = OBPAPI5_1_0.routes ++ endpointsOf6_0_0
|
||||
|
||||
// Filter the possible endpoints by the disabled / enabled Props settings and add them together
|
||||
val routes : List[OBPEndpoint] = getAllowedEndpoints(endpoints, allResourceDocs)
|
||||
|
||||
registerRoutes(routes, allResourceDocs, apiPrefix, true)
|
||||
|
||||
|
||||
logger.info(s"version $version has been run! There are ${routes.length} routes, ${allResourceDocs.length} allResourceDocs.")
|
||||
|
||||
// specified response for OPTIONS request.
|
||||
private val corsResponse: Box[LiftResponse] = Full{
|
||||
val corsHeaders = List(
|
||||
"Access-Control-Allow-Origin" -> "*",
|
||||
"Access-Control-Allow-Methods" -> "GET, POST, OPTIONS, PUT, PATCH, DELETE",
|
||||
"Access-Control-Allow-Headers" -> "*",
|
||||
"Access-Control-Allow-Credentials" -> "true",
|
||||
"Access-Control-Max-Age" -> "1728000" //Tell client that this pre-flight info is valid for 20 days
|
||||
)
|
||||
PlainTextResponse("", corsHeaders, HttpStatus.SC_NO_CONTENT)
|
||||
}
|
||||
/*
|
||||
* process OPTIONS http request, just return no content and status is 204
|
||||
*/
|
||||
this.serve({
|
||||
case req if req.requestType.method == "OPTIONS" => corsResponse
|
||||
})
|
||||
}
|
||||
@ -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(_) =>
|
||||
|
||||
@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@ -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
|
||||
@ -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(
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -1945,5 +1945,21 @@
|
||||
<CcyNbr>961</CcyNbr>
|
||||
<CcyMnrUnts>N.A.</CcyMnrUnts>
|
||||
</CcyNtry>
|
||||
<!-- Cardano (ADA) -->
|
||||
<CcyNtry>
|
||||
<CtryNm>Cardano</CtryNm>
|
||||
<CcyNm>Cardano</CcyNm>
|
||||
<Ccy>ada</Ccy>
|
||||
<CcyNbr>null</CcyNbr>
|
||||
<CcyMnrUnts>6</CcyMnrUnts> <!-- 1 ADA = 10^6 Lovelace -->
|
||||
</CcyNtry>
|
||||
<!-- Lovelace (The smallest unit of ADA) -->
|
||||
<CcyNtry>
|
||||
<CtryNm>Cardano_Lovelace</CtryNm>
|
||||
<CcyNm>Lovelace</CcyNm>
|
||||
<Ccy>lovelace</Ccy>
|
||||
<CcyNbr>null</CcyNbr>
|
||||
<CcyMnrUnts>0</CcyMnrUnts> <!-- Lovelace is the basic unit, no smaller subdivision -->
|
||||
</CcyNtry>
|
||||
</CcyTbl>
|
||||
</ISO_4217>
|
||||
132
obp-api/src/test/scala/code/api/OBPRestHelperTest.scala
Normal file
132
obp-api/src/test/scala/code/api/OBPRestHelperTest.scala
Normal file
@ -0,0 +1,132 @@
|
||||
package code.api
|
||||
|
||||
import code.api.util.APIUtil.{ResourceDoc, EmptyBody}
|
||||
import code.api.OBPRestHelper
|
||||
import com.openbankproject.commons.util.{ApiVersion, ScannedApiVersion}
|
||||
import org.scalatest.{FlatSpec, Matchers, Tag}
|
||||
|
||||
/**
|
||||
* Unit tests for OBPRestHelper.isAutoValidate method
|
||||
*
|
||||
* This test suite covers basic scenarios for the isAutoValidate function:
|
||||
* - When doc.isValidateEnabled is true
|
||||
* - When autoValidateAll is false
|
||||
* - When doc.isValidateDisabled is true
|
||||
* - When doc.implementedInApiVersion is not ScannedApiVersion
|
||||
* - Basic version comparison logic
|
||||
*/
|
||||
class OBPRestHelperTest extends FlatSpec with Matchers {
|
||||
|
||||
object tag extends Tag("OBPRestHelper")
|
||||
|
||||
// Create a test instance of OBPRestHelper
|
||||
private val testHelper = new OBPRestHelper {
|
||||
val version: com.openbankproject.commons.util.ApiVersion = ScannedApiVersion("obp", "OBP", "v4.0.0")
|
||||
val versionStatus: String = "stable"
|
||||
}
|
||||
|
||||
// Helper method to create a ResourceDoc with specific validation settings
|
||||
private def createResourceDoc(
|
||||
version: ScannedApiVersion,
|
||||
isValidateEnabled: Boolean = false,
|
||||
isValidateDisabled: Boolean = false
|
||||
): ResourceDoc = {
|
||||
// Create a minimal ResourceDoc for testing
|
||||
val doc = new ResourceDoc(
|
||||
partialFunction = null, // Not used in our tests
|
||||
implementedInApiVersion = version,
|
||||
partialFunctionName = "testFunction",
|
||||
requestVerb = "GET",
|
||||
requestUrl = "/test",
|
||||
summary = "Test endpoint",
|
||||
description = "Test description",
|
||||
exampleRequestBody = EmptyBody,
|
||||
successResponseBody = EmptyBody,
|
||||
errorResponseBodies = List(),
|
||||
tags = List()
|
||||
)
|
||||
|
||||
// Set validation flags using reflection or direct method calls
|
||||
if (isValidateEnabled) {
|
||||
doc.enableAutoValidate()
|
||||
}
|
||||
if (isValidateDisabled) {
|
||||
doc.disableAutoValidate()
|
||||
}
|
||||
|
||||
doc
|
||||
}
|
||||
|
||||
"isAutoValidate" should "return true when doc.isValidateEnabled is true" taggedAs tag in {
|
||||
val v4_0_0 = ScannedApiVersion("obp", "OBP", "v4.0.0")
|
||||
val doc = createResourceDoc(v4_0_0, isValidateEnabled = true)
|
||||
val result = testHelper.isAutoValidate(doc, autoValidateAll = false)
|
||||
result shouldBe true
|
||||
}
|
||||
|
||||
it should "return false when autoValidateAll is false and doc.isValidateEnabled is false" taggedAs tag in {
|
||||
val v4_0_0 = ScannedApiVersion("obp", "OBP", "v4.0.0")
|
||||
val doc = createResourceDoc(v4_0_0, isValidateEnabled = false)
|
||||
val result = testHelper.isAutoValidate(doc, autoValidateAll = false)
|
||||
result shouldBe false
|
||||
}
|
||||
|
||||
it should "return false when doc.isValidateDisabled is true" taggedAs tag in {
|
||||
val v4_0_0 = ScannedApiVersion("obp", "OBP", "v4.0.0")
|
||||
val doc = createResourceDoc(v4_0_0, isValidateDisabled = true)
|
||||
val result = testHelper.isAutoValidate(doc, autoValidateAll = true)
|
||||
result shouldBe false
|
||||
}
|
||||
|
||||
|
||||
|
||||
it should "return false for versions before v4.0.0" taggedAs tag in {
|
||||
val v3_1_0 = ScannedApiVersion("obp", "OBP", "v3.1.0")
|
||||
val doc = createResourceDoc(v3_1_0)
|
||||
val result = testHelper.isAutoValidate(doc, autoValidateAll = true)
|
||||
result shouldBe false
|
||||
}
|
||||
|
||||
it should "return true for v4.0.0" taggedAs tag in {
|
||||
val v4_0_0 = ScannedApiVersion("obp", "OBP", "v4.0.0")
|
||||
val doc = createResourceDoc(v4_0_0)
|
||||
val result = testHelper.isAutoValidate(doc, autoValidateAll = true)
|
||||
result shouldBe true
|
||||
}
|
||||
|
||||
it should "return true for versions after v4.0.0" taggedAs tag in {
|
||||
val v5_0_0 = ScannedApiVersion("obp", "OBP", "v5.0.0")
|
||||
val doc = createResourceDoc(v5_0_0)
|
||||
val result = testHelper.isAutoValidate(doc, autoValidateAll = true)
|
||||
result shouldBe true
|
||||
}
|
||||
|
||||
it should "return true for v4.1.0 (major=4, minor=1)" taggedAs tag in {
|
||||
val v4_1_0 = ScannedApiVersion("obp", "OBP", "v4.1.0")
|
||||
val doc = createResourceDoc(v4_1_0)
|
||||
val result = testHelper.isAutoValidate(doc, autoValidateAll = true)
|
||||
result shouldBe true
|
||||
}
|
||||
|
||||
it should "return false for malformed version strings" taggedAs tag in {
|
||||
val malformedVersion = ScannedApiVersion("obp", "OBP", "v4") // Missing minor version
|
||||
val doc = createResourceDoc(malformedVersion)
|
||||
val result = testHelper.isAutoValidate(doc, autoValidateAll = true)
|
||||
result shouldBe false
|
||||
}
|
||||
|
||||
it should "prioritize isValidateEnabled over autoValidateAll" taggedAs tag in {
|
||||
val v3_1_0 = ScannedApiVersion("obp", "OBP", "v3.1.0") // v3.1.0 normally wouldn't auto-validate
|
||||
val doc = createResourceDoc(v3_1_0, isValidateEnabled = true)
|
||||
val result = testHelper.isAutoValidate(doc, autoValidateAll = true)
|
||||
result shouldBe true // Should be true because isValidateEnabled is true
|
||||
}
|
||||
|
||||
it should "prioritize isValidateDisabled over autoValidateAll" taggedAs tag in {
|
||||
val v4_0_0 = ScannedApiVersion("obp", "OBP", "v4.0.0") // v4.0.0 normally would auto-validate
|
||||
val doc = createResourceDoc(v4_0_0, isValidateDisabled = true)
|
||||
val result = testHelper.isAutoValidate(doc, autoValidateAll = true)
|
||||
result shouldBe false // Should be false because isValidateDisabled is true
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,442 @@
|
||||
/**
|
||||
Open Bank Project - API
|
||||
Copyright (C) 2011-2019, TESOBE GmbH
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
Email: contact@tesobe.com
|
||||
TESOBE GmbH
|
||||
Osloerstrasse 16/17
|
||||
Berlin 13359, Germany
|
||||
|
||||
This product includes software developed at
|
||||
TESOBE (http://www.tesobe.com/)
|
||||
*/
|
||||
package code.api.v6_0_0
|
||||
|
||||
import code.api.Constant
|
||||
import code.api.ResourceDocs1_4_0.SwaggerDefinitionsJSON
|
||||
import code.api.util.ApiRole
|
||||
import code.api.util.ApiRole._
|
||||
import code.api.util.ErrorMessages._
|
||||
import code.api.v4_0_0.TransactionRequestWithChargeJSON400
|
||||
import code.entitlement.Entitlement
|
||||
import code.methodrouting.MethodRoutingCommons
|
||||
import com.github.dwickern.macros.NameOf.nameOf
|
||||
import com.openbankproject.commons.model.{AmountOfMoneyJsonV121, ErrorMessage}
|
||||
import com.openbankproject.commons.util.ApiVersion
|
||||
import net.liftweb.json.Serialization.write
|
||||
import org.scalatest.Tag
|
||||
import code.api.util.APIUtil.OAuth._
|
||||
import code.api.util.ApiRole
|
||||
import code.api.v6_0_0.OBPAPI6_0_0.Implementations6_0_0
|
||||
import code.entitlement.Entitlement
|
||||
|
||||
|
||||
|
||||
class CardanoTransactionRequestTest extends V600ServerSetup {
|
||||
|
||||
/**
|
||||
* Test tags
|
||||
* Example: To run tests with tag "getPermissions":
|
||||
* mvn test -D tagsToInclude
|
||||
*
|
||||
* This is made possible by the scalatest maven plugin
|
||||
*/
|
||||
object VersionOfApi extends Tag(ApiVersion.v6_0_0.toString)
|
||||
object CreateTransactionRequestCardano extends Tag(nameOf(Implementations6_0_0.createTransactionRequestCardano))
|
||||
|
||||
|
||||
val testBankId = testBankId1.value
|
||||
|
||||
// This is a test account for Cardano transaction request tests, testAccountId0 is the walletId, passphrase is the passphrase for the wallet
|
||||
val testAccountId = "62b27359c25d4f2a5f97acee521ac1df7ac5a606"
|
||||
val passphrase = "StrongPassword123!"
|
||||
|
||||
|
||||
val putCreateAccountJSONV310 = SwaggerDefinitionsJSON.createAccountRequestJsonV310.copy(
|
||||
user_id = resourceUser1.userId,
|
||||
balance = AmountOfMoneyJsonV121("lovelace", "0"),
|
||||
)
|
||||
|
||||
|
||||
feature("Create Cardano Transaction Request - v6.0.0") {
|
||||
|
||||
scenario("We will create Cardano transaction request - user is NOT logged in", CreateTransactionRequestCardano, VersionOfApi) {
|
||||
When("We make a request v6.0.0")
|
||||
val request600 = (v6_0_0_Request / "banks" / testBankId / "accounts" / testAccountId / Constant.SYSTEM_OWNER_VIEW_ID / "transaction-request-types" / "CARDANO" / "transaction-requests").POST
|
||||
val cardanoTransactionRequestBody = TransactionRequestBodyCardanoJsonV600(
|
||||
to = CardanoPaymentJsonV600(
|
||||
address = "addr_test1qpv3se9ghq87ud29l0a8asy8nlqwd765e5zt4rc2z4mktqulwagn832cuzcjknfyxwzxz2p2kumx6n58tskugny6mrqs7fd23z",
|
||||
amount = CardanoAmountJsonV600(
|
||||
quantity = 1000000,
|
||||
unit = "lovelace"
|
||||
)
|
||||
),
|
||||
value = AmountOfMoneyJsonV121("lovelace", "1000000"),
|
||||
passphrase = passphrase,
|
||||
description = "Basic ADA transfer"
|
||||
)
|
||||
val response600 = makePostRequest(request600, write(cardanoTransactionRequestBody))
|
||||
Then("We should get a 401")
|
||||
response600.code should equal(401)
|
||||
And("error should be " + UserNotLoggedIn)
|
||||
response600.body.extract[ErrorMessage].message should equal(UserNotLoggedIn)
|
||||
}
|
||||
|
||||
// scenario("We will create Cardano transaction request - user is logged in", CreateTransactionRequestCardano, VersionOfApi) {
|
||||
// Entitlement.entitlement.vend.addEntitlement(testBankId, resourceUser1.userId, ApiRole.canCreateAccount.toString())
|
||||
// val request = (v6_0_0_Request / "banks" / testBankId / "accounts" / testAccountId ).PUT <@(user1)
|
||||
// val response = makePutRequest(request, write(putCreateAccountJSONV310))
|
||||
// Then("We should get a 201")
|
||||
// response.code should equal(201)
|
||||
//
|
||||
// When("We create a method routing for makePaymentv210 to cardano_vJun2025")
|
||||
// val cardanoMethodRouting = MethodRoutingCommons(
|
||||
// methodName = "makePaymentv210",
|
||||
// connectorName = "cardano_vJun2025",
|
||||
// isBankIdExactMatch = false,
|
||||
// bankIdPattern = Some("*"),
|
||||
// parameters = List()
|
||||
// )
|
||||
// val request310 = (v6_0_0_Request / "management" / "method_routings").POST <@(user1)
|
||||
// Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanCreateMethodRouting.toString)
|
||||
// val response310 = makePostRequest(request310, write(cardanoMethodRouting))
|
||||
// response310.code should equal(201)
|
||||
//
|
||||
// When("We make a request v6.0.0")
|
||||
// val request600 = (v6_0_0_Request / "banks" / testBankId / "accounts" / testAccountId / Constant.SYSTEM_OWNER_VIEW_ID / "transaction-request-types" / "CARDANO" / "transaction-requests").POST <@(user1)
|
||||
// val cardanoTransactionRequestBody = TransactionRequestBodyCardanoJsonV600(
|
||||
// to = CardanoPaymentJsonV600(
|
||||
// address = "addr_test1qpv3se9ghq87ud29l0a8asy8nlqwd765e5zt4rc2z4mktqulwagn832cuzcjknfyxwzxz2p2kumx6n58tskugny6mrqs7fd23z",
|
||||
// amount = CardanoAmountJsonV600(
|
||||
// quantity = 1000000,
|
||||
// unit = "lovelace"
|
||||
// )
|
||||
// ),
|
||||
// value = AmountOfMoneyJsonV121("lovelace", "1000000"),
|
||||
// passphrase = passphrase,
|
||||
// description = "Basic ADA transfer"
|
||||
// )
|
||||
// val response600 = makePostRequest(request600, write(cardanoTransactionRequestBody))
|
||||
// Then("We should get a 201")
|
||||
// response600.code should equal(201)
|
||||
// And("response should contain transaction request")
|
||||
// val transactionRequest = response600.body.extract[TransactionRequestWithChargeJSON400]
|
||||
// transactionRequest.status should not be empty
|
||||
// }
|
||||
//
|
||||
// scenario("We will create Cardano transaction request with metadata - user is logged in", CreateTransactionRequestCardano, VersionOfApi) {
|
||||
// Entitlement.entitlement.vend.addEntitlement(testBankId, resourceUser1.userId, ApiRole.canCreateAccount.toString())
|
||||
// val request = (v6_0_0_Request / "banks" / testBankId / "accounts" / testAccountId ).PUT <@(user1)
|
||||
// val response = makePutRequest(request, write(putCreateAccountJSONV310))
|
||||
// Then("We should get a 201")
|
||||
// response.code should equal(201)
|
||||
//
|
||||
// When("We create a method routing for makePaymentv210 to cardano_vJun2025")
|
||||
// val cardanoMethodRouting = MethodRoutingCommons(
|
||||
// methodName = "makePaymentv210",
|
||||
// connectorName = "cardano_vJun2025",
|
||||
// isBankIdExactMatch = false,
|
||||
// bankIdPattern = Some("*"),
|
||||
// parameters = List()
|
||||
// )
|
||||
// val request310 = (v6_0_0_Request / "management" / "method_routings").POST <@(user1)
|
||||
// Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanCreateMethodRouting.toString)
|
||||
// val response310 = makePostRequest(request310, write(cardanoMethodRouting))
|
||||
// response310.code should equal(201)
|
||||
//
|
||||
// When("We make a request v6.0.0 with metadata")
|
||||
// val request600 = (v6_0_0_Request / "banks" / testBankId / "accounts" / testAccountId / Constant.SYSTEM_OWNER_VIEW_ID / "transaction-request-types" / "CARDANO" / "transaction-requests").POST <@(user1)
|
||||
// val cardanoTransactionRequestBody = TransactionRequestBodyCardanoJsonV600(
|
||||
// to = CardanoPaymentJsonV600(
|
||||
// address = "addr_test1qpv3se9ghq87ud29l0a8asy8nlqwd765e5zt4rc2z4mktqulwagn832cuzcjknfyxwzxz2p2kumx6n58tskugny6mrqs7fd23z",
|
||||
// amount = CardanoAmountJsonV600(
|
||||
// quantity = 1000000,
|
||||
// unit = "lovelace"
|
||||
// )
|
||||
// ),
|
||||
// value = AmountOfMoneyJsonV121("lovelace", "1000000"),
|
||||
// passphrase = passphrase,
|
||||
// description = "ADA transfer with metadata",
|
||||
// metadata = Some(Map("202507022319" -> CardanoMetadataStringJsonV600("Hello Cardano")))
|
||||
// )
|
||||
// val response600 = makePostRequest(request600, write(cardanoTransactionRequestBody))
|
||||
// Then("We should get a 201")
|
||||
// response600.code should equal(201)
|
||||
// And("response should contain transaction request")
|
||||
// val transactionRequest = response600.body.extract[TransactionRequestWithChargeJSON400]
|
||||
// transactionRequest.status should not be empty
|
||||
// }
|
||||
//
|
||||
// scenario("We will create Cardano transaction request with token - user is logged in", CreateTransactionRequestCardano, VersionOfApi) {
|
||||
// Entitlement.entitlement.vend.addEntitlement(testBankId, resourceUser1.userId, ApiRole.canCreateAccount.toString())
|
||||
// val request = (v6_0_0_Request / "banks" / testBankId / "accounts" / testAccountId ).PUT <@(user1)
|
||||
// val response = makePutRequest(request, write(putCreateAccountJSONV310))
|
||||
// Then("We should get a 201")
|
||||
// response.code should equal(201)
|
||||
//
|
||||
// When("We create a method routing for makePaymentv210 to cardano_vJun2025")
|
||||
// val cardanoMethodRouting = MethodRoutingCommons(
|
||||
// methodName = "makePaymentv210",
|
||||
// connectorName = "cardano_vJun2025",
|
||||
// isBankIdExactMatch = false,
|
||||
// bankIdPattern = Some("*"),
|
||||
// parameters = List()
|
||||
// )
|
||||
// val request310 = (v6_0_0_Request / "management" / "method_routings").POST <@(user1)
|
||||
// Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanCreateMethodRouting.toString)
|
||||
// val response310 = makePostRequest(request310, write(cardanoMethodRouting))
|
||||
// response310.code should equal(201)
|
||||
//
|
||||
// When("We make a request v6.0.0 with token")
|
||||
// val request600 = (v6_0_0_Request / "banks" / testBankId / "accounts" / testAccountId / Constant.SYSTEM_OWNER_VIEW_ID / "transaction-request-types" / "CARDANO" / "transaction-requests").POST <@(user1)
|
||||
// val cardanoTransactionRequestBody = TransactionRequestBodyCardanoJsonV600(
|
||||
// to = CardanoPaymentJsonV600(
|
||||
// address = "addr_test1qpv3se9ghq87ud29l0a8asy8nlqwd765e5zt4rc2z4mktqulwagn832cuzcjknfyxwzxz2p2kumx6n58tskugny6mrqs7fd23z",
|
||||
// amount = CardanoAmountJsonV600(
|
||||
// quantity = 1000000,
|
||||
// unit = "lovelace"
|
||||
// ),
|
||||
// assets = Some(List(CardanoAssetJsonV600(
|
||||
// policy_id = "ef1954d3a058a96d89d959939aeb5b2948a3df2eb40f9a78d61e3d4f",
|
||||
// asset_name = "OGCRA",
|
||||
// quantity = 10
|
||||
// )))
|
||||
// ),
|
||||
// value = AmountOfMoneyJsonV121("lovelace", "1000000"),
|
||||
// passphrase = passphrase,
|
||||
// description = "Token-only transfer"
|
||||
// )
|
||||
// val response600 = makePostRequest(request600, write(cardanoTransactionRequestBody))
|
||||
// Then("We should get a 201")
|
||||
// response600.code should equal(201)
|
||||
// And("response should contain transaction request")
|
||||
// val transactionRequest = response600.body.extract[TransactionRequestWithChargeJSON400]
|
||||
// transactionRequest.status should not be empty
|
||||
// }
|
||||
//
|
||||
// scenario("We will create Cardano transaction request with token and metadata - user is logged in", CreateTransactionRequestCardano, VersionOfApi) {
|
||||
// When("We make a request v6.0.0 with token and metadata")
|
||||
// val request600 = (v6_0_0_Request / "banks" / testBankId / "accounts" / testAccountId / Constant.SYSTEM_OWNER_VIEW_ID / "transaction-request-types" / "CARDANO" / "transaction-requests").POST <@(user1)
|
||||
// val cardanoTransactionRequestBody = TransactionRequestBodyCardanoJsonV600(
|
||||
// to = CardanoPaymentJsonV600(
|
||||
// address = "addr_test1qpv3se9ghq87ud29l0a8asy8nlqwd765e5zt4rc2z4mktqulwagn832cuzcjknfyxwzxz2p2kumx6n58tskugny6mrqs7fd23z",
|
||||
// amount = CardanoAmountJsonV600(
|
||||
// quantity = 5000000,
|
||||
// unit = "lovelace"
|
||||
// ),
|
||||
// assets = Some(List(CardanoAssetJsonV600(
|
||||
// policy_id = "ef1954d3a058a96d89d959939aeb5b2948a3df2eb40f9a78d61e3d4f",
|
||||
// asset_name = "OGCRA",
|
||||
// quantity = 10
|
||||
// )))
|
||||
// ),
|
||||
// value = AmountOfMoneyJsonV121("lovelace", "1000000"),
|
||||
// passphrase = passphrase,
|
||||
// description = "ADA transfer with token and metadata",
|
||||
// metadata = Some(Map("202507022319" -> CardanoMetadataStringJsonV600("Hello Cardano with Token")))
|
||||
// )
|
||||
// val response600 = makePostRequest(request600, write(cardanoTransactionRequestBody))
|
||||
// Then("We should get a 201")
|
||||
// response600.code should equal(201)
|
||||
// And("response should contain transaction request")
|
||||
// val transactionRequest = response600.body.extract[TransactionRequestWithChargeJSON400]
|
||||
// transactionRequest.status should not be empty
|
||||
// }
|
||||
//
|
||||
// scenario("We will try to create Cardano transaction request for someone else account - user is logged in", CreateTransactionRequestCardano, VersionOfApi) {
|
||||
// When("We make a request v6.0.0")
|
||||
// val request600 = (v6_0_0_Request / "banks" / testBankId / "accounts" / testAccountId / Constant.SYSTEM_OWNER_VIEW_ID / "transaction-request-types" / "CARDANO" / "transaction-requests").POST <@(user2)
|
||||
// val cardanoTransactionRequestBody = TransactionRequestBodyCardanoJsonV600(
|
||||
// to = CardanoPaymentJsonV600(
|
||||
// address = "addr_test1qpv3se9ghq87ud29l0a8asy8nlqwd765e5zt4rc2z4mktqulwagn832cuzcjknfyxwzxz2p2kumx6n58tskugny6mrqs7fd23z",
|
||||
// amount = CardanoAmountJsonV600(
|
||||
// quantity = 1000000,
|
||||
// unit = "lovelace"
|
||||
// )
|
||||
// ),
|
||||
// value = AmountOfMoneyJsonV121("lovelace", "1000000"),
|
||||
// passphrase = passphrase,
|
||||
// description = "Basic ADA transfer"
|
||||
// )
|
||||
// val response600 = makePostRequest(request600, write(cardanoTransactionRequestBody))
|
||||
// Then("We should get a 403")
|
||||
// response600.code should equal(403)
|
||||
// And("error should be " + UserNoPermissionAccessView)
|
||||
// response600.body.extract[ErrorMessage].message contains (UserNoPermissionAccessView) shouldBe (true)
|
||||
// }
|
||||
//
|
||||
// scenario("We will try to create Cardano transaction request with invalid address format", CreateTransactionRequestCardano, VersionOfApi) {
|
||||
// When("We make a request v6.0.0 with invalid address")
|
||||
// val request600 = (v6_0_0_Request / "banks" / testBankId / "accounts" / testAccountId / Constant.SYSTEM_OWNER_VIEW_ID / "transaction-request-types" / "CARDANO" / "transaction-requests").POST <@(user1)
|
||||
// val cardanoTransactionRequestBody = TransactionRequestBodyCardanoJsonV600(
|
||||
// to = CardanoPaymentJsonV600(
|
||||
// address = "invalid_address_format",
|
||||
// amount = CardanoAmountJsonV600(
|
||||
// quantity = 1000000,
|
||||
// unit = "lovelace"
|
||||
// )
|
||||
// ),
|
||||
// value = AmountOfMoneyJsonV121("lovelace", "1000000"),
|
||||
// passphrase = passphrase,
|
||||
// description = "Basic ADA transfer"
|
||||
// )
|
||||
// val response600 = makePostRequest(request600, write(cardanoTransactionRequestBody))
|
||||
// Then("We should get a 400")
|
||||
// response600.code should equal(400)
|
||||
// And("error should contain invalid address message")
|
||||
// response600.body.extract[ErrorMessage].message should include("Cardano address format is invalid")
|
||||
// }
|
||||
//
|
||||
// scenario("We will try to create Cardano transaction request with missing amount", CreateTransactionRequestCardano, VersionOfApi) {
|
||||
// When("We make a request v6.0.0 with missing amount")
|
||||
// val request600 = (v6_0_0_Request / "banks" / testBankId / "accounts" / testAccountId / Constant.SYSTEM_OWNER_VIEW_ID / "transaction-request-types" / "CARDANO" / "transaction-requests").POST <@(user1)
|
||||
// val invalidJson = """
|
||||
// {
|
||||
// "to": {
|
||||
// "address": "addr_test1qpv3se9ghq87ud29l0a8asy8nlqwd765e5zt4rc2z4mktqulwagn832cuzcjknfyxwzxz2p2kumx6n58tskugny6mrqs7fd23z"
|
||||
// },
|
||||
// "value": {
|
||||
// "currency": "lovelace",
|
||||
// "amount": "1000000"
|
||||
// },
|
||||
// "passphrase": "StrongPassword123!",
|
||||
// "description": "Basic ADA transfer"
|
||||
// }
|
||||
// """
|
||||
// val response600 = makePostRequest(request600, invalidJson)
|
||||
// Then("We should get a 400")
|
||||
// response600.code should equal(400)
|
||||
// And("error should contain invalid json format message")
|
||||
// response600.body.extract[ErrorMessage].message should include("InvalidJsonFormat")
|
||||
// }
|
||||
//
|
||||
// scenario("We will try to create Cardano transaction request with negative amount", CreateTransactionRequestCardano, VersionOfApi) {
|
||||
// When("We make a request v6.0.0 with negative amount")
|
||||
// val request600 = (v6_0_0_Request / "banks" / testBankId / "accounts" / testAccountId / Constant.SYSTEM_OWNER_VIEW_ID / "transaction-request-types" / "CARDANO" / "transaction-requests").POST <@(user1)
|
||||
// val cardanoTransactionRequestBody = TransactionRequestBodyCardanoJsonV600(
|
||||
// to = CardanoPaymentJsonV600(
|
||||
// address = "addr_test1qpv3se9ghq87ud29l0a8asy8nlqwd765e5zt4rc2z4mktqulwagn832cuzcjknfyxwzxz2p2kumx6n58tskugny6mrqs7fd23z",
|
||||
// amount = CardanoAmountJsonV600(
|
||||
// quantity = -1000000,
|
||||
// unit = "lovelace"
|
||||
// )
|
||||
// ),
|
||||
// value = AmountOfMoneyJsonV121("lovelace", "1000000"),
|
||||
// passphrase = passphrase,
|
||||
// description = "Basic ADA transfer"
|
||||
// )
|
||||
// val response600 = makePostRequest(request600, write(cardanoTransactionRequestBody))
|
||||
// Then("We should get a 400")
|
||||
// response600.code should equal(400)
|
||||
// And("error should contain invalid amount message")
|
||||
// response600.body.extract[ErrorMessage].message should include("Cardano amount quantity must be non-negative")
|
||||
// }
|
||||
//
|
||||
// scenario("We will try to create Cardano transaction request with invalid amount unit", CreateTransactionRequestCardano, VersionOfApi) {
|
||||
// When("We make a request v6.0.0 with invalid amount unit")
|
||||
// val request600 = (v6_0_0_Request / "banks" / testBankId / "accounts" / testAccountId / Constant.SYSTEM_OWNER_VIEW_ID / "transaction-request-types" / "CARDANO" / "transaction-requests").POST <@(user1)
|
||||
// val cardanoTransactionRequestBody = TransactionRequestBodyCardanoJsonV600(
|
||||
// to = CardanoPaymentJsonV600(
|
||||
// address = "addr_test1qpv3se9ghq87ud29l0a8asy8nlqwd765e5zt4rc2z4mktqulwagn832cuzcjknfyxwzxz2p2kumx6n58tskugny6mrqs7fd23z",
|
||||
// amount = CardanoAmountJsonV600(
|
||||
// quantity = 1000000,
|
||||
// unit = "abc" // Invalid unit, should be "lovelace"
|
||||
// )
|
||||
// ),
|
||||
// value = AmountOfMoneyJsonV121("lovelace", "1000000"),
|
||||
// passphrase = passphrase,
|
||||
// description = "Basic ADA transfer"
|
||||
// )
|
||||
// val response600 = makePostRequest(request600, write(cardanoTransactionRequestBody))
|
||||
// Then("We should get a 400")
|
||||
// response600.code should equal(400)
|
||||
// And("error should contain invalid unit message")
|
||||
// response600.body.extract[ErrorMessage].message should include("Cardano amount unit must be 'lovelace'")
|
||||
// }
|
||||
//
|
||||
// scenario("We will try to create Cardano transaction request with zero amount but no assets", CreateTransactionRequestCardano, VersionOfApi) {
|
||||
// When("We make a request v6.0.0 with zero amount but no assets")
|
||||
// val request600 = (v6_0_0_Request / "banks" / testBankId / "accounts" / testAccountId / Constant.SYSTEM_OWNER_VIEW_ID / "transaction-request-types" / "CARDANO" / "transaction-requests").POST <@(user1)
|
||||
// val cardanoTransactionRequestBody = TransactionRequestBodyCardanoJsonV600(
|
||||
// to = CardanoPaymentJsonV600(
|
||||
// address = "addr_test1qpv3se9ghq87ud29l0a8asy8nlqwd765e5zt4rc2z4mktqulwagn832cuzcjknfyxwzxz2p2kumx6n58tskugny6mrqs7fd23z",
|
||||
// amount = CardanoAmountJsonV600(
|
||||
// quantity = 0,
|
||||
// unit = "lovelace"
|
||||
// )
|
||||
// ),
|
||||
// value = AmountOfMoneyJsonV121("lovelace", "0.0"),
|
||||
// passphrase = passphrase,
|
||||
// description = "Zero amount without assets"
|
||||
// )
|
||||
// val response600 = makePostRequest(request600, write(cardanoTransactionRequestBody))
|
||||
// Then("We should get a 400")
|
||||
// response600.code should equal(400)
|
||||
// And("error should contain invalid amount message")
|
||||
// response600.body.extract[ErrorMessage].message should include("Cardano transfer with zero amount must include assets")
|
||||
// }
|
||||
//
|
||||
// scenario("We will try to create Cardano transaction request with invalid assets", CreateTransactionRequestCardano, VersionOfApi) {
|
||||
// When("We make a request v6.0.0 with invalid assets")
|
||||
// val request600 = (v6_0_0_Request / "banks" / testBankId / "accounts" / testAccountId / Constant.SYSTEM_OWNER_VIEW_ID / "transaction-request-types" / "CARDANO" / "transaction-requests").POST <@(user1)
|
||||
// val cardanoTransactionRequestBody = TransactionRequestBodyCardanoJsonV600(
|
||||
// to = CardanoPaymentJsonV600(
|
||||
// address = "addr_test1qpv3se9ghq87ud29l0a8asy8nlqwd765e5zt4rc2z4mktqulwagn832cuzcjknfyxwzxz2p2kumx6n58tskugny6mrqs7fd23z",
|
||||
// amount = CardanoAmountJsonV600(
|
||||
// quantity = 0,
|
||||
// unit = "lovelace"
|
||||
// ),
|
||||
// assets = Some(List(CardanoAssetJsonV600(
|
||||
// policy_id = "",
|
||||
// asset_name = "",
|
||||
// quantity = 0
|
||||
// )))
|
||||
// ),
|
||||
// value = AmountOfMoneyJsonV121("lovelace", "0.0"),
|
||||
// passphrase = passphrase,
|
||||
// description = "Invalid assets"
|
||||
// )
|
||||
// val response600 = makePostRequest(request600, write(cardanoTransactionRequestBody))
|
||||
// Then("We should get a 400")
|
||||
// response600.code should equal(400)
|
||||
// And("error should contain invalid assets message")
|
||||
// response600.body.extract[ErrorMessage].message should include("Cardano assets must have valid policy_id and asset_name")
|
||||
// }
|
||||
//
|
||||
// scenario("We will try to create Cardano transaction request with invalid metadata", CreateTransactionRequestCardano, VersionOfApi) {
|
||||
// When("We make a request v6.0.0 with invalid metadata")
|
||||
// val request600 = (v6_0_0_Request / "banks" / testBankId / "accounts" / testAccountId / Constant.SYSTEM_OWNER_VIEW_ID / "transaction-request-types" / "CARDANO" / "transaction-requests").POST <@(user1)
|
||||
// val cardanoTransactionRequestBody = TransactionRequestBodyCardanoJsonV600(
|
||||
// to = CardanoPaymentJsonV600(
|
||||
// address = "addr_test1qpv3se9ghq87ud29l0a8asy8nlqwd765e5zt4rc2z4mktqulwagn832cuzcjknfyxwzxz2p2kumx6n58tskugny6mrqs7fd23z",
|
||||
// amount = CardanoAmountJsonV600(
|
||||
// quantity = 1000000,
|
||||
// unit = "lovelace"
|
||||
// )
|
||||
// ),
|
||||
// value = AmountOfMoneyJsonV121("lovelace", "1000000"),
|
||||
// passphrase = passphrase,
|
||||
// description = "ADA transfer with invalid metadata",
|
||||
// metadata = Some(Map("" -> CardanoMetadataStringJsonV600("")))
|
||||
// )
|
||||
// val response600 = makePostRequest(request600, write(cardanoTransactionRequestBody))
|
||||
// Then("We should get a 400")
|
||||
// response600.code should equal(400)
|
||||
// And("error should contain invalid metadata message")
|
||||
// response600.body.extract[ErrorMessage].message should include("Cardano metadata must have valid structure")
|
||||
// }
|
||||
}
|
||||
}
|
||||
13
obp-api/src/test/scala/code/api/v6_0_0/V600ServerSetup.scala
Normal file
13
obp-api/src/test/scala/code/api/v6_0_0/V600ServerSetup.scala
Normal file
@ -0,0 +1,13 @@
|
||||
package code.api.v6_0_0
|
||||
|
||||
import code.setup.{DefaultUsers, ServerSetupWithTestData}
|
||||
import dispatch.Req
|
||||
|
||||
trait V600ServerSetup extends ServerSetupWithTestData with DefaultUsers {
|
||||
|
||||
def v4_0_0_Request: Req = baseRequest / "obp" / "v4.0.0"
|
||||
def v5_0_0_Request: Req = baseRequest / "obp" / "v5.0.0"
|
||||
def v5_1_0_Request: Req = baseRequest / "obp" / "v5.1.0"
|
||||
def v6_0_0_Request: Req = baseRequest / "obp" / "v6.0.0"
|
||||
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user