From d86aea2abf9e44349628a19d2aac7495b30ac705 Mon Sep 17 00:00:00 2001 From: hongwei Date: Thu, 17 Jul 2025 17:49:25 +0200 Subject: [PATCH 01/28] feature/OBPv5.1.0 added cardaro payment --- .../SwaggerDefinitionsJSON.scala | 12 + .../scala/code/api/v4_0_0/APIMethods400.scala | 702 +---------------- .../scala/code/api/v5_1_0/APIMethods510.scala | 56 ++ .../code/api/v5_1_0/JSONFactory5.1.0.scala | 12 + .../LocalMappedConnectorInternal.scala | 718 +++++++++++++++++- .../cardano/CardanoConnector_vJun2025.scala | 174 ++++- .../commons/model/enums/Enumerations.scala | 7 +- 7 files changed, 969 insertions(+), 712 deletions(-) diff --git a/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala b/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala index 8c3cf3710..7e3925cc6 100644 --- a/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala +++ b/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala @@ -5695,6 +5695,18 @@ object SwaggerDefinitionsJSON { permission_name = CAN_GRANT_ACCESS_TO_VIEWS, extra_data = Some(List(SYSTEM_ACCOUNTANT_VIEW_ID, SYSTEM_AUDITOR_VIEW_ID)) ) + + + lazy val cardanoPaymentJsonV510 = CardanoPaymentJsonV510( + address = "addr_test1qpv3se9ghq87ud29l0a8asy8nlqwd765e5zt4rc2z4mktqulwagn832cuzcjknfyxwzxz2p2kumx6n58tskugny6mrqs7fd12", + amount = amountOfMoneyJsonV121 + ) + + lazy val transactionRequestBodyCardanoJsonV510 = TransactionRequestBodyCardanoJsonV510( + to = List(cardanoPaymentJsonV510), + passphrase = "password1234!" + ) + //The common error or success format. //Just some helper format to use in Json case class NotSupportedYet() diff --git a/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala b/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala index 8643d84b3..eff55d0c7 100644 --- a/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala +++ b/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala @@ -27,19 +27,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} @@ -47,7 +46,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 @@ -70,7 +68,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, _} @@ -80,7 +77,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.Mailer.{From, PlainMailBodyType, Subject, To, XHTMLMailBodyType} @@ -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} @@ -900,7 +895,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 = { @@ -908,7 +903,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 = { @@ -916,7 +911,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 = { @@ -924,7 +919,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 = { @@ -932,7 +927,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 = { @@ -940,7 +935,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 = { @@ -948,7 +943,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 = { @@ -956,7 +951,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) } @@ -1002,7 +997,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) } @@ -12189,677 +12184,6 @@ object APIMethods400 extends RestHelper with APIMethods400 { lazy val newStyleEndpoints: List[(String, String)] = Implementations4_0_0.resourceDocs.map { rd => (rd.partialFunctionName, rd.implementedInApiVersion.toString()) }.toList - - - - - // This text is used in the various Create Transaction Request resource docs - val transactionRequestGeneralText = - s""" - | - |For an introduction to Transaction Requests, see: ${Glossary.getGlossaryItemLink("Transaction-Request-Introduction")} - | - |""".stripMargin - - val lowAmount = AmountOfMoneyJsonV121("EUR", "12.50") - - val sharedChargePolicy = ChargePolicy.withName("SHARED") - - def createTransactionRequest(bankId: BankId, accountId: AccountId, viewId: ViewId, transactionRequestType: TransactionRequestType, json: JValue): Future[(TransactionRequestWithChargeJSON400, Option[CallContext])] = { - for { - (Full(u), callContext) <- SS.user - - transactionRequestTypeValue <- NewStyle.function.tryons(s"$InvalidTransactionRequestType: '${transactionRequestType.value}'. OBP does not support it.", 400, callContext) { - TransactionRequestTypes.withName(transactionRequestType.value) - } - - (fromAccount, callContext) <- transactionRequestTypeValue match { - case CARD => - for{ - transactionRequestBodyCard <- NewStyle.function.tryons(s"${InvalidJsonFormat}, it should be $CARD json format", 400, callContext) { - json.extract[TransactionRequestBodyCardJsonV400] - } - // 1.1 get Card from card_number - (cardFromCbs,callContext) <- NewStyle.function.getPhysicalCardByCardNumber(transactionRequestBodyCard.card.card_number, callContext) - - // 1.2 check card name/expire month. year. - calendar = Calendar.getInstance - _ = calendar.setTime(cardFromCbs.expires) - yearFromCbs = calendar.get(Calendar.YEAR).toString - monthFromCbs = calendar.get(Calendar.MONTH).toString - nameOnCardFromCbs= cardFromCbs.nameOnCard - cvvFromCbs= cardFromCbs.cvv.getOrElse("") - brandFromCbs= cardFromCbs.brand.getOrElse("") - - _ <- Helper.booleanToFuture(s"$InvalidJsonValue brand is not matched", cc=callContext) { - transactionRequestBodyCard.card.brand.equalsIgnoreCase(brandFromCbs) - } - - dateFromJsonBody <- NewStyle.function.tryons(s"$InvalidDateFormat year should be 'yyyy', " + - s"eg: 2023, but current expiry_year(${transactionRequestBodyCard.card.expiry_year}), " + - s"month should be 'xx', eg: 02, but current expiry_month(${transactionRequestBodyCard.card.expiry_month})", 400, callContext) { - DateWithMonthFormat.parse(s"${transactionRequestBodyCard.card.expiry_year}-${transactionRequestBodyCard.card.expiry_month}") - } - _ <- Helper.booleanToFuture(s"$InvalidJsonValue your credit card is expired.", cc=callContext) { - org.apache.commons.lang3.time.DateUtils.addMonths(new Date(), 1).before(dateFromJsonBody) - } - - _ <- Helper.booleanToFuture(s"$InvalidJsonValue expiry_year is not matched", cc=callContext) { - transactionRequestBodyCard.card.expiry_year.equalsIgnoreCase(yearFromCbs) - } - _ <- Helper.booleanToFuture(s"$InvalidJsonValue expiry_month is not matched", cc=callContext) { - transactionRequestBodyCard.card.expiry_month.toInt.equals(monthFromCbs.toInt+1) - } - - _ <- Helper.booleanToFuture(s"$InvalidJsonValue name_on_card is not matched", cc=callContext) { - transactionRequestBodyCard.card.name_on_card.equalsIgnoreCase(nameOnCardFromCbs) - } - _ <- Helper.booleanToFuture(s"$InvalidJsonValue cvv is not matched", cc=callContext) { - HashUtil.Sha256Hash(transactionRequestBodyCard.card.cvv).equals(cvvFromCbs) - } - - } yield{ - (cardFromCbs.account, callContext) - } - - case _ => NewStyle.function.getBankAccount(bankId,accountId, callContext) - } - _ <- NewStyle.function.isEnabledTransactionRequests(callContext) - _ <- Helper.booleanToFuture(InvalidAccountIdFormat, cc=callContext) { - isValidID(fromAccount.accountId.value) - } - _ <- Helper.booleanToFuture(InvalidBankIdFormat, cc=callContext) { - isValidID(fromAccount.bankId.value) - } - - _ <- NewStyle.function.checkAuthorisationToCreateTransactionRequest(viewId, BankIdAccountId(fromAccount.bankId, fromAccount.accountId), u, callContext) - - _ <- Helper.booleanToFuture(s"${InvalidTransactionRequestType}: '${transactionRequestType.value}'. Current Sandbox does not support it. ", cc=callContext) { - APIUtil.getPropsValue("transactionRequests_supported_types", "").split(",").contains(transactionRequestType.value) - } - - // Check the input JSON format, here is just check the common parts of all four types - transDetailsJson <- NewStyle.function.tryons(s"$InvalidJsonFormat The Json body should be the $TransactionRequestBodyCommonJSON ", 400, callContext) { - json.extract[TransactionRequestBodyCommonJSON] - } - - transactionAmountNumber <- NewStyle.function.tryons(s"$InvalidNumber Current input is ${transDetailsJson.value.amount} ", 400, callContext) { - BigDecimal(transDetailsJson.value.amount) - } - - _ <- Helper.booleanToFuture(s"${NotPositiveAmount} Current input is: '${transactionAmountNumber}'", cc=callContext) { - transactionAmountNumber > BigDecimal("0") - } - - _ <- Helper.booleanToFuture(s"${InvalidISOCurrencyCode} Current input is: '${transDetailsJson.value.currency}'", cc=callContext) { - APIUtil.isValidCurrencyISOCode(transDetailsJson.value.currency) - } - - (createdTransactionRequest, callContext) <- transactionRequestTypeValue match { - case REFUND => { - for { - transactionRequestBodyRefundJson <- NewStyle.function.tryons(s"${InvalidJsonFormat}, it should be $ACCOUNT json format", 400, callContext) { - json.extract[TransactionRequestBodyRefundJsonV400] - } - - transactionId = TransactionId(transactionRequestBodyRefundJson.refund.transaction_id) - - (fromAccount, toAccount, transaction, callContext) <- transactionRequestBodyRefundJson.to match { - case Some(refundRequestTo) if refundRequestTo.account_id.isDefined && refundRequestTo.bank_id.isDefined => - val toBankId = BankId(refundRequestTo.bank_id.get) - val toAccountId = AccountId(refundRequestTo.account_id.get) - for { - (transaction, callContext) <- NewStyle.function.getTransaction(fromAccount.bankId, fromAccount.accountId, transactionId, callContext) - (toAccount, callContext) <- NewStyle.function.checkBankAccountExists(toBankId, toAccountId, callContext) - } yield (fromAccount, toAccount, transaction, callContext) - - case Some(refundRequestTo) if refundRequestTo.counterparty_id.isDefined => - val toCounterpartyId = CounterpartyId(refundRequestTo.counterparty_id.get) - for { - (toCounterparty, callContext) <- NewStyle.function.getCounterpartyByCounterpartyId(toCounterpartyId, callContext) - (toAccount, callContext) <- NewStyle.function.getBankAccountFromCounterparty(toCounterparty, isOutgoingAccount = true, callContext) - _ <- Helper.booleanToFuture(s"$CounterpartyBeneficiaryPermit", cc=callContext) { - toCounterparty.isBeneficiary - } - (transaction, callContext) <- NewStyle.function.getTransaction(fromAccount.bankId, fromAccount.accountId, transactionId, callContext) - } yield (fromAccount, toAccount, transaction, callContext) - - case None if transactionRequestBodyRefundJson.from.isDefined => - val fromCounterpartyId = CounterpartyId(transactionRequestBodyRefundJson.from.get.counterparty_id) - val toAccount = fromAccount - for { - (fromCounterparty, callContext) <- NewStyle.function.getCounterpartyByCounterpartyId(fromCounterpartyId, callContext) - (fromAccount, callContext) <- NewStyle.function.getBankAccountFromCounterparty(fromCounterparty, isOutgoingAccount = false, callContext) - _ <- Helper.booleanToFuture(s"$CounterpartyBeneficiaryPermit", cc=callContext) { - fromCounterparty.isBeneficiary - } - (transaction, callContext) <- NewStyle.function.getTransaction(toAccount.bankId, toAccount.accountId, transactionId, callContext) - } yield (fromAccount, toAccount, transaction, callContext) - } - - transDetailsSerialized <- NewStyle.function.tryons(UnknownError, 400, callContext) { - write(transactionRequestBodyRefundJson)(Serialization.formats(NoTypeHints)) - } - - _ <- Helper.booleanToFuture(s"${RefundedTransaction} Current input amount is: '${transDetailsJson.value.amount}'. It can not be more than the original amount(${(transaction.amount).abs})", cc=callContext) { - (transaction.amount).abs >= transactionAmountNumber - } - //TODO, we need additional field to guarantee the transaction is refunded... - // _ <- Helper.booleanToFuture(s"${RefundedTransaction}") { - // !((transaction.description.toString contains(" Refund to ")) && (transaction.description.toString contains(" and transaction_id("))) - // } - - //we add the extra info (counterparty name + transaction_id) for this special Refund endpoint. - newDescription = s"${transactionRequestBodyRefundJson.description} - Refund for transaction_id: (${transactionId.value}) to ${transaction.otherAccount.counterpartyName}" - - //This is the refund endpoint, the original fromAccount is the `toAccount` which will receive money. - refundToAccount = fromAccount - //This is the refund endpoint, the original toAccount is the `fromAccount` which will lose money. - refundFromAccount = toAccount - - (createdTransactionRequest, callContext) <- NewStyle.function.createTransactionRequestv400(u, - viewId, - refundFromAccount, - refundToAccount, - transactionRequestType, - transactionRequestBodyRefundJson.copy(description = newDescription), - transDetailsSerialized, - sharedChargePolicy.toString, - Some(OBP_TRANSACTION_REQUEST_CHALLENGE), - getScaMethodAtInstance(transactionRequestType.value).toOption, - None, - callContext) //in ACCOUNT, ChargePolicy set default "SHARED" - - _ <- NewStyle.function.createOrUpdateTransactionRequestAttribute( - bankId = bankId, - transactionRequestId = createdTransactionRequest.id, - transactionRequestAttributeId = None, - name = "original_transaction_id", - attributeType = TransactionRequestAttributeType.withName("STRING"), - value = transactionId.value, - callContext = callContext - ) - - refundReasonCode = transactionRequestBodyRefundJson.refund.reason_code - _ <- if (refundReasonCode.nonEmpty) { - NewStyle.function.createOrUpdateTransactionRequestAttribute( - bankId = bankId, - transactionRequestId = createdTransactionRequest.id, - transactionRequestAttributeId = None, - name = "refund_reason_code", - attributeType = TransactionRequestAttributeType.withName("STRING"), - value = refundReasonCode, - callContext = callContext) - } else Future.successful() - - (newTransactionRequestStatus, callContext) <- NewStyle.function.notifyTransactionRequest(refundFromAccount, refundToAccount, createdTransactionRequest, callContext) - _ <- NewStyle.function.saveTransactionRequestStatusImpl(createdTransactionRequest.id, newTransactionRequestStatus.toString, callContext) - createdTransactionRequest <- Future(createdTransactionRequest.copy(status = newTransactionRequestStatus.toString)) - - } yield (createdTransactionRequest, callContext) - } - case ACCOUNT | SANDBOX_TAN => { - for { - transactionRequestBodySandboxTan <- NewStyle.function.tryons(s"${InvalidJsonFormat}, it should be $ACCOUNT json format", 400, callContext) { - json.extract[TransactionRequestBodySandBoxTanJSON] - } - - toBankId = BankId(transactionRequestBodySandboxTan.to.bank_id) - toAccountId = AccountId(transactionRequestBodySandboxTan.to.account_id) - (toAccount, callContext) <- NewStyle.function.checkBankAccountExists(toBankId, toAccountId, callContext) - - transDetailsSerialized <- NewStyle.function.tryons(UnknownError, 400, callContext) { - write(transactionRequestBodySandboxTan)(Serialization.formats(NoTypeHints)) - } - - (createdTransactionRequest, callContext) <- NewStyle.function.createTransactionRequestv400(u, - viewId, - fromAccount, - toAccount, - transactionRequestType, - transactionRequestBodySandboxTan, - transDetailsSerialized, - sharedChargePolicy.toString, - Some(OBP_TRANSACTION_REQUEST_CHALLENGE), - getScaMethodAtInstance(transactionRequestType.value).toOption, - None, - callContext) //in ACCOUNT, ChargePolicy set default "SHARED" - } yield (createdTransactionRequest, callContext) - } - case ACCOUNT_OTP => { - for { - transactionRequestBodySandboxTan <- NewStyle.function.tryons(s"${InvalidJsonFormat}, it should be $ACCOUNT json format", 400, callContext) { - json.extract[TransactionRequestBodySandBoxTanJSON] - } - - toBankId = BankId(transactionRequestBodySandboxTan.to.bank_id) - toAccountId = AccountId(transactionRequestBodySandboxTan.to.account_id) - (toAccount, callContext) <- NewStyle.function.checkBankAccountExists(toBankId, toAccountId, callContext) - - transDetailsSerialized <- NewStyle.function.tryons(UnknownError, 400, callContext) { - write(transactionRequestBodySandboxTan)(Serialization.formats(NoTypeHints)) - } - - (createdTransactionRequest, callContext) <- NewStyle.function.createTransactionRequestv400(u, - viewId, - fromAccount, - toAccount, - transactionRequestType, - transactionRequestBodySandboxTan, - transDetailsSerialized, - sharedChargePolicy.toString, - Some(OBP_TRANSACTION_REQUEST_CHALLENGE), - getScaMethodAtInstance(transactionRequestType.value).toOption, - None, - callContext) //in ACCOUNT, ChargePolicy set default "SHARED" - } yield (createdTransactionRequest, callContext) - } - case COUNTERPARTY => { - for { - _ <- Future { logger.debug(s"Before extracting counterparty id") } - //For COUNTERPARTY, Use the counterpartyId to find the toCounterparty and set up the toAccount - transactionRequestBodyCounterparty <- NewStyle.function.tryons(s"${InvalidJsonFormat}, it should be $COUNTERPARTY json format", 400, callContext) { - json.extract[TransactionRequestBodyCounterpartyJSON] - } - toCounterpartyId = transactionRequestBodyCounterparty.to.counterparty_id - _ <- Future { logger.debug(s"After extracting counterparty id: $toCounterpartyId") } - (toCounterparty, callContext) <- NewStyle.function.getCounterpartyByCounterpartyId(CounterpartyId(toCounterpartyId), callContext) - - transactionRequestAttributes <- if(transactionRequestBodyCounterparty.attributes.isDefined && transactionRequestBodyCounterparty.attributes.head.length > 0 ) { - - val attributes = transactionRequestBodyCounterparty.attributes.head - - val failMsg = s"$InvalidJsonFormat The attribute `type` field can only accept the following field: " + - s"${TransactionRequestAttributeType.DOUBLE}(12.1234)," + - s" ${TransactionRequestAttributeType.STRING}(TAX_NUMBER), " + - s"${TransactionRequestAttributeType.INTEGER}(123) and " + - s"${TransactionRequestAttributeType.DATE_WITH_DAY}(2012-04-23)" - - for{ - _ <- NewStyle.function.tryons(failMsg, 400, callContext) { - attributes.map(attribute => TransactionRequestAttributeType.withName(attribute.attribute_type)) - } - }yield{ - attributes - } - - } else { - Future.successful(List.empty[TransactionRequestAttributeJsonV400]) - } - - (counterpartyLimitBox, callContext) <- Connector.connector.vend.getCounterpartyLimit( - bankId.value, - accountId.value, - viewId.value, - toCounterpartyId, - callContext - ) - _<- if(counterpartyLimitBox.isDefined){ - for{ - counterpartyLimit <- Future.successful(counterpartyLimitBox.head) - maxSingleAmount = counterpartyLimit.maxSingleAmount - maxMonthlyAmount = counterpartyLimit.maxMonthlyAmount - maxNumberOfMonthlyTransactions = counterpartyLimit.maxNumberOfMonthlyTransactions - maxYearlyAmount = counterpartyLimit.maxYearlyAmount - maxNumberOfYearlyTransactions = counterpartyLimit.maxNumberOfYearlyTransactions - maxTotalAmount = counterpartyLimit.maxTotalAmount - maxNumberOfTransactions = counterpartyLimit.maxNumberOfTransactions - - // Get the first day of the current month - firstDayOfMonth: LocalDate = LocalDate.now().withDayOfMonth(1) - - // Get the last day of the current month - lastDayOfMonth: LocalDate = LocalDate.now().withDayOfMonth( - LocalDate.now().lengthOfMonth() - ) - // Get the first day of the current year - firstDayOfYear: LocalDate = LocalDate.now().withDayOfYear(1) - - // Get the last day of the current year - lastDayOfYear: LocalDate = LocalDate.now().withDayOfYear( - LocalDate.now().lengthOfYear() - ) - - // Convert LocalDate to Date - zoneId: ZoneId = ZoneId.systemDefault() - firstCurrentMonthDate: Date = Date.from(firstDayOfMonth.atStartOfDay(zoneId).toInstant) - // Adjust to include 23:59:59.999 - lastCurrentMonthDate: Date = Date.from( - lastDayOfMonth - .atTime(23, 59, 59, 999000000) - .atZone(zoneId) - .toInstant - ) - - firstCurrentYearDate: Date = Date.from(firstDayOfYear.atStartOfDay(zoneId).toInstant) - // Adjust to include 23:59:59.999 - lastCurrentYearDate: Date = Date.from( - lastDayOfYear - .atTime(23, 59, 59, 999000000) - .atZone(zoneId) - .toInstant - ) - - defaultFromDate: Date = theEpochTime - defaultToDate: Date = APIUtil.ToDateInFuture - - (sumOfTransactionsFromAccountToCounterpartyMonthly, callContext) <- NewStyle.function.getSumOfTransactionsFromAccountToCounterparty( - fromAccount.bankId: BankId, - fromAccount.accountId: AccountId, - CounterpartyId(toCounterpartyId): CounterpartyId, - firstCurrentMonthDate: Date, - lastCurrentMonthDate: Date, - callContext: Option[CallContext] - ) - - (countOfTransactionsFromAccountToCounterpartyMonthly, callContext) <- NewStyle.function.getCountOfTransactionsFromAccountToCounterparty( - fromAccount.bankId: BankId, - fromAccount.accountId: AccountId, - CounterpartyId(toCounterpartyId): CounterpartyId, - firstCurrentMonthDate: Date, - lastCurrentMonthDate: Date, - callContext: Option[CallContext] - ) - - (sumOfTransactionsFromAccountToCounterpartyYearly, callContext) <- NewStyle.function.getSumOfTransactionsFromAccountToCounterparty( - fromAccount.bankId: BankId, - fromAccount.accountId: AccountId, - CounterpartyId(toCounterpartyId): CounterpartyId, - firstCurrentYearDate: Date, - lastCurrentYearDate: Date, - callContext: Option[CallContext] - ) - - (countOfTransactionsFromAccountToCounterpartyYearly, callContext) <- NewStyle.function.getCountOfTransactionsFromAccountToCounterparty( - fromAccount.bankId: BankId, - fromAccount.accountId: AccountId, - CounterpartyId(toCounterpartyId): CounterpartyId, - firstCurrentYearDate: Date, - lastCurrentYearDate: Date, - callContext: Option[CallContext] - ) - - (sumOfAllTransactionsFromAccountToCounterparty, callContext) <- NewStyle.function.getSumOfTransactionsFromAccountToCounterparty( - fromAccount.bankId: BankId, - fromAccount.accountId: AccountId, - CounterpartyId(toCounterpartyId): CounterpartyId, - defaultFromDate: Date, - defaultToDate: Date, - callContext: Option[CallContext] - ) - - (countOfAllTransactionsFromAccountToCounterparty, callContext) <- NewStyle.function.getCountOfTransactionsFromAccountToCounterparty( - fromAccount.bankId: BankId, - fromAccount.accountId: AccountId, - CounterpartyId(toCounterpartyId): CounterpartyId, - defaultFromDate: Date, - defaultToDate: Date, - callContext: Option[CallContext] - ) - - - currentTransactionAmountWithFxApplied <- NewStyle.function.tryons(s"${InvalidJsonFormat}, it should be $COUNTERPARTY json format", 400, callContext) { - val fromAccountCurrency = fromAccount.currency //eg: if from account currency is EUR - val transferCurrency = transactionRequestBodyCounterparty.value.currency //eg: if the payment json body currency is GBP. - val transferAmount = BigDecimal(transactionRequestBodyCounterparty.value.amount) //eg: if the payment json body amount is 1. - val debitRate = fx.exchangeRate(transferCurrency, fromAccountCurrency, Some(fromAccount.bankId.value), callContext) //eg: the rate here is 1.16278. - fx.convert(transferAmount, debitRate) // 1.16278 Euro - } - - _ <- Helper.booleanToFuture(s"$CounterpartyLimitValidationError max_single_amount is $maxSingleAmount ${fromAccount.currency}, " + - s"but current transaction body amount is ${transactionRequestBodyCounterparty.value.amount} ${transactionRequestBodyCounterparty.value.currency}, " + - s"which is $currentTransactionAmountWithFxApplied ${fromAccount.currency}. ", cc = callContext) { - maxSingleAmount >= currentTransactionAmountWithFxApplied - } - _ <- Helper.booleanToFuture(s"$CounterpartyLimitValidationError max_monthly_amount is $maxMonthlyAmount, but current monthly amount is ${BigDecimal(sumOfTransactionsFromAccountToCounterpartyMonthly.amount)+currentTransactionAmountWithFxApplied}", cc = callContext) { - maxMonthlyAmount >= BigDecimal(sumOfTransactionsFromAccountToCounterpartyMonthly.amount)+currentTransactionAmountWithFxApplied - } - _ <- Helper.booleanToFuture(s"$CounterpartyLimitValidationError max_number_of_monthly_transactions is $maxNumberOfMonthlyTransactions, but current count of monthly transactions is ${countOfTransactionsFromAccountToCounterpartyMonthly+1}", cc = callContext) { - maxNumberOfMonthlyTransactions >= countOfTransactionsFromAccountToCounterpartyMonthly+1 - } - _ <- Helper.booleanToFuture(s"$CounterpartyLimitValidationError max_yearly_amount is $maxYearlyAmount, but current yearly amount is ${BigDecimal(sumOfTransactionsFromAccountToCounterpartyYearly.amount)+currentTransactionAmountWithFxApplied}", cc = callContext) { - maxYearlyAmount >= BigDecimal(sumOfTransactionsFromAccountToCounterpartyYearly.amount)+currentTransactionAmountWithFxApplied - } - result <- Helper.booleanToFuture(s"$CounterpartyLimitValidationError max_number_of_yearly_transactions is $maxNumberOfYearlyTransactions, but current count of yearly transaction is ${countOfTransactionsFromAccountToCounterpartyYearly+1}", cc = callContext) { - maxNumberOfYearlyTransactions >= countOfTransactionsFromAccountToCounterpartyYearly+1 - } - _ <- Helper.booleanToFuture(s"$CounterpartyLimitValidationError max_total_amount is $maxTotalAmount, but current amount is ${BigDecimal(sumOfAllTransactionsFromAccountToCounterparty.amount)+currentTransactionAmountWithFxApplied}", cc = callContext) { - maxTotalAmount >= BigDecimal(sumOfAllTransactionsFromAccountToCounterparty.amount)+currentTransactionAmountWithFxApplied - } - result <- Helper.booleanToFuture(s"$CounterpartyLimitValidationError max_number_of_transactions is $maxNumberOfTransactions, but current count of all transactions is ${countOfAllTransactionsFromAccountToCounterparty+1}", cc = callContext) { - maxNumberOfTransactions >= countOfAllTransactionsFromAccountToCounterparty+1 - } - }yield{ - result - } - } - else { - Future.successful(true) - } - - (toAccount, callContext) <- NewStyle.function.getBankAccountFromCounterparty(toCounterparty, true, callContext) - // Check we can send money to it. - _ <- Helper.booleanToFuture(s"$CounterpartyBeneficiaryPermit", cc=callContext) { - toCounterparty.isBeneficiary - } - chargePolicy = transactionRequestBodyCounterparty.charge_policy - _ <- Helper.booleanToFuture(s"$InvalidChargePolicy", cc=callContext) { - ChargePolicy.values.contains(ChargePolicy.withName(chargePolicy)) - } - transDetailsSerialized <- NewStyle.function.tryons(UnknownError, 400, callContext) { - write(transactionRequestBodyCounterparty)(Serialization.formats(NoTypeHints)) - } - (createdTransactionRequest, callContext) <- NewStyle.function.createTransactionRequestv400(u, - viewId, - fromAccount, - toAccount, - transactionRequestType, - transactionRequestBodyCounterparty, - transDetailsSerialized, - chargePolicy, - Some(OBP_TRANSACTION_REQUEST_CHALLENGE), - getScaMethodAtInstance(transactionRequestType.value).toOption, - None, - callContext) - - _ <- NewStyle.function.createTransactionRequestAttributes( - bankId: BankId, - createdTransactionRequest.id, - transactionRequestAttributes, - true, - callContext: Option[CallContext] - ) - } yield (createdTransactionRequest, callContext) - } - case AGENT_CASH_WITHDRAWAL => { - for { - //For Agent, Use the agentId to find the agent and set up the toAccount - transactionRequestBodyAgent <- NewStyle.function.tryons(s"${InvalidJsonFormat}, it should be $AGENT_CASH_WITHDRAWAL json format", 400, callContext) { - json.extract[TransactionRequestBodyAgentJsonV400] - } - (agent, callContext) <- NewStyle.function.getAgentByAgentNumber(BankId(transactionRequestBodyAgent.to.bank_id),transactionRequestBodyAgent.to.agent_number, callContext) - (agentAccountLinks, callContext) <- NewStyle.function.getAgentAccountLinksByAgentId(agent.agentId, callContext) - agentAccountLink <- NewStyle.function.tryons(AgentAccountLinkNotFound, 400, callContext) { - agentAccountLinks.head - } - // Check we can send money to it. - _ <- Helper.booleanToFuture(s"$AgentBeneficiaryPermit", cc=callContext) { - !agent.isPendingAgent && agent.isConfirmedAgent - } - (toAccount, callContext) <- NewStyle.function.getBankAccount(BankId(agentAccountLink.bankId), AccountId(agentAccountLink.accountId), callContext) - chargePolicy = transactionRequestBodyAgent.charge_policy - _ <- Helper.booleanToFuture(s"$InvalidChargePolicy", cc=callContext) { - ChargePolicy.values.contains(ChargePolicy.withName(chargePolicy)) - } - transDetailsSerialized <- NewStyle.function.tryons(UnknownError, 400, callContext) { - write(transactionRequestBodyAgent)(Serialization.formats(NoTypeHints)) - } - (createdTransactionRequest, callContext) <- NewStyle.function.createTransactionRequestv400(u, - viewId, - fromAccount, - toAccount, - transactionRequestType, - transactionRequestBodyAgent, - transDetailsSerialized, - chargePolicy, - Some(OBP_TRANSACTION_REQUEST_CHALLENGE), - getScaMethodAtInstance(transactionRequestType.value).toOption, - None, - callContext) - } yield (createdTransactionRequest, callContext) - } - case CARD => { - for { - //2rd: get toAccount from counterpartyId - transactionRequestBodyCard <- NewStyle.function.tryons(s"${InvalidJsonFormat}, it should be $CARD json format", 400, callContext) { - json.extract[TransactionRequestBodyCardJsonV400] - } - toCounterpartyId = transactionRequestBodyCard.to.counterparty_id - (toCounterparty, callContext) <- NewStyle.function.getCounterpartyByCounterpartyId(CounterpartyId(toCounterpartyId), callContext) - (toAccount, callContext) <- NewStyle.function.getBankAccountFromCounterparty(toCounterparty, true, callContext) - // Check we can send money to it. - _ <- Helper.booleanToFuture(s"$CounterpartyBeneficiaryPermit", cc=callContext) { - toCounterparty.isBeneficiary - } - chargePolicy = ChargePolicy.RECEIVER.toString - transDetailsSerialized <- NewStyle.function.tryons(UnknownError, 400, callContext) { - write(transactionRequestBodyCard)(Serialization.formats(NoTypeHints)) - } - (createdTransactionRequest, callContext) <- NewStyle.function.createTransactionRequestv400(u, - viewId, - fromAccount, - toAccount, - transactionRequestType, - transactionRequestBodyCard, - transDetailsSerialized, - chargePolicy, - Some(OBP_TRANSACTION_REQUEST_CHALLENGE), - getScaMethodAtInstance(transactionRequestType.value).toOption, - None, - callContext) - } yield (createdTransactionRequest, callContext) - - } - case SIMPLE => { - for { - //For SAMPLE, we will create/get toCounterparty on site and set up the toAccount - transactionRequestBodySimple <- NewStyle.function.tryons(s"${InvalidJsonFormat}, it should be $SIMPLE json format", 400, callContext) { - json.extract[TransactionRequestBodySimpleJsonV400] - } - (toCounterparty, callContext) <- NewStyle.function.getOrCreateCounterparty( - name = transactionRequestBodySimple.to.name, - description = transactionRequestBodySimple.to.description, - currency = transactionRequestBodySimple.value.currency, - createdByUserId = u.userId, - thisBankId = bankId.value, - thisAccountId = accountId.value, - thisViewId = viewId.value, - otherBankRoutingScheme = StringHelpers.snakify(transactionRequestBodySimple.to.other_bank_routing_scheme).toUpperCase, - otherBankRoutingAddress = transactionRequestBodySimple.to.other_bank_routing_address, - otherBranchRoutingScheme = StringHelpers.snakify(transactionRequestBodySimple.to.other_branch_routing_scheme).toUpperCase, - otherBranchRoutingAddress = transactionRequestBodySimple.to.other_branch_routing_address, - otherAccountRoutingScheme = StringHelpers.snakify(transactionRequestBodySimple.to.other_account_routing_scheme).toUpperCase, - otherAccountRoutingAddress = transactionRequestBodySimple.to.other_account_routing_address, - otherAccountSecondaryRoutingScheme = StringHelpers.snakify(transactionRequestBodySimple.to.other_account_secondary_routing_scheme).toUpperCase, - otherAccountSecondaryRoutingAddress = transactionRequestBodySimple.to.other_account_secondary_routing_address, - callContext: Option[CallContext], - ) - (toAccount, callContext) <- NewStyle.function.getBankAccountFromCounterparty(toCounterparty, true, callContext) - // Check we can send money to it. - _ <- Helper.booleanToFuture(s"$CounterpartyBeneficiaryPermit", cc=callContext) { - toCounterparty.isBeneficiary - } - chargePolicy = transactionRequestBodySimple.charge_policy - _ <- Helper.booleanToFuture(s"$InvalidChargePolicy", cc=callContext) { - ChargePolicy.values.contains(ChargePolicy.withName(chargePolicy)) - } - transDetailsSerialized <- NewStyle.function.tryons(UnknownError, 400, callContext) { - write(transactionRequestBodySimple)(Serialization.formats(NoTypeHints)) - } - (createdTransactionRequest, callContext) <- NewStyle.function.createTransactionRequestv400(u, - viewId, - fromAccount, - toAccount, - transactionRequestType, - transactionRequestBodySimple, - transDetailsSerialized, - chargePolicy, - Some(OBP_TRANSACTION_REQUEST_CHALLENGE), - getScaMethodAtInstance(transactionRequestType.value).toOption, - None, - callContext) - } yield (createdTransactionRequest, callContext) - - } - case SEPA => { - for { - //For SEPA, Use the IBAN to find the toCounterparty and set up the toAccount - transDetailsSEPAJson <- NewStyle.function.tryons(s"${InvalidJsonFormat}, it should be $SEPA json format", 400, callContext) { - json.extract[TransactionRequestBodySEPAJsonV400] - } - toIban = transDetailsSEPAJson.to.iban - (toCounterparty, callContext) <- NewStyle.function.getCounterpartyByIbanAndBankAccountId(toIban, fromAccount.bankId, fromAccount.accountId, callContext) - (toAccount, callContext) <- NewStyle.function.getBankAccountFromCounterparty(toCounterparty, true, callContext) - _ <- Helper.booleanToFuture(s"$CounterpartyBeneficiaryPermit", cc=callContext) { - toCounterparty.isBeneficiary - } - chargePolicy = transDetailsSEPAJson.charge_policy - _ <- Helper.booleanToFuture(s"$InvalidChargePolicy", cc=callContext) { - ChargePolicy.values.contains(ChargePolicy.withName(chargePolicy)) - } - transDetailsSerialized <- NewStyle.function.tryons(UnknownError, 400, callContext) { - write(transDetailsSEPAJson)(Serialization.formats(NoTypeHints)) - } - (createdTransactionRequest, callContext) <- NewStyle.function.createTransactionRequestv400(u, - viewId, - fromAccount, - toAccount, - transactionRequestType, - transDetailsSEPAJson, - transDetailsSerialized, - chargePolicy, - Some(OBP_TRANSACTION_REQUEST_CHALLENGE), - getScaMethodAtInstance(transactionRequestType.value).toOption, - transDetailsSEPAJson.reasons.map(_.map(_.transform)), - callContext) - } yield (createdTransactionRequest, callContext) - } - case FREE_FORM => { - for { - transactionRequestBodyFreeForm <- NewStyle.function.tryons(s"${InvalidJsonFormat}, it should be $FREE_FORM json format", 400, callContext) { - json.extract[TransactionRequestBodyFreeFormJSON] - } - // Following lines: just transfer the details body, add Bank_Id and Account_Id in the Detail part. This is for persistence and 'answerTransactionRequestChallenge' - transactionRequestAccountJSON = TransactionRequestAccountJsonV140(bankId.value, accountId.value) - transDetailsSerialized <- NewStyle.function.tryons(UnknownError, 400, callContext) { - write(transactionRequestBodyFreeForm)(Serialization.formats(NoTypeHints)) - } - (createdTransactionRequest, callContext) <- NewStyle.function.createTransactionRequestv400(u, - viewId, - fromAccount, - fromAccount, - transactionRequestType, - transactionRequestBodyFreeForm, - transDetailsSerialized, - sharedChargePolicy.toString, - Some(OBP_TRANSACTION_REQUEST_CHALLENGE), - getScaMethodAtInstance(transactionRequestType.value).toOption, - None, - callContext) - } yield - (createdTransactionRequest, callContext) - } - } - (challenges, callContext) <- NewStyle.function.getChallengesByTransactionRequestId(createdTransactionRequest.id.value, callContext) - (transactionRequestAttributes, callContext) <- NewStyle.function.getTransactionRequestAttributes( - bankId, - createdTransactionRequest.id, - callContext - ) - } yield { - (JSONFactory400.createTransactionRequestWithChargeJSON(createdTransactionRequest, challenges, transactionRequestAttributes), HttpCode.`201`(callContext)) - } - } - + } diff --git a/obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala b/obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala index 32f8ce6c2..96f6c612b 100644 --- a/obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala +++ b/obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala @@ -31,6 +31,7 @@ import code.api.v5_0_0.JSONFactory500 import code.api.v5_1_0.JSONFactory510.{createConsentsInfoJsonV510, createConsentsJsonV510, createRegulatedEntitiesJson, createRegulatedEntityJson} import code.atmattribute.AtmAttribute import code.bankconnectors.Connector +import code.bankconnectors.LocalMappedConnectorInternal._ import code.consent.{ConsentRequests, ConsentStatus, Consents, MappedConsent} import code.consumer.Consumers import code.entitlement.Entitlement @@ -5313,6 +5314,61 @@ trait APIMethods510 { } } + staticResourceDocs += ResourceDoc( + createTransactionRequestCardano, + implementedInApiVersion, + nameOf(createTransactionRequestCardano), + "POST", + "/banks/cardano/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, + transactionRequestBodyCardanoJsonV510, + transactionRequestWithChargeJSON400, + List( + $UserNotLoggedIn, + $BankNotFound, + $BankAccountNotFound, + InsufficientAuthorisationToCreateTransactionRequest, + InvalidTransactionRequestType, + InvalidJsonFormat, + NotPositiveAmount, + InvalidTransactionRequestCurrency, + TransactionDisabled, + UnknownError + ), + List(apiTagTransactionRequest, apiTagPSD2PIS, apiTagPsd2) + ) + + lazy val createTransactionRequestCardano: OBPEndpoint = { + case "banks" :: "cardano" :: "accounts" :: AccountId(accountId) :: ViewId(viewId) :: "transaction-request-types" :: + "CARDANO" :: "transaction-requests" :: Nil JsonPost json -> _ => + cc => implicit val ec = EndpointContext(Some(cc)) + + for { + (createdTransactionId, callContext) <- NewStyle.function.makePaymentv210( + null, + null, + null, + null, + null, + "", //BG no description so far + null, + "", // chargePolicy is not used in BG so far., + cc.callContext + ) + } yield (createdTransactionId, HttpCode.`201`(cc.callContext)) + +// val transactionRequestType = TransactionRequestType("CARDANO") +// LocalMappedConnectorInternal.createTransactionRequest(BankId("cardano"), accountId, viewId , transactionRequestType, json) + } + } } diff --git a/obp-api/src/main/scala/code/api/v5_1_0/JSONFactory5.1.0.scala b/obp-api/src/main/scala/code/api/v5_1_0/JSONFactory5.1.0.scala index 7d965f83a..72db5f730 100644 --- a/obp-api/src/main/scala/code/api/v5_1_0/JSONFactory5.1.0.scala +++ b/obp-api/src/main/scala/code/api/v5_1_0/JSONFactory5.1.0.scala @@ -578,6 +578,18 @@ case class ConsentRequestToAccountJson( limit: PostCounterpartyLimitV510 ) +case class CardanoPaymentJsonV510( + address: String, + amount: AmountOfMoneyJsonV121 +) + +case class TransactionRequestBodyCardanoJsonV510( + to: List[CardanoPaymentJsonV510], + passphrase: String +// value: AmountOfMoneyJsonV121, +// description: String, +) //extends TransactionRequestCommonBodyJSON + case class CreateViewPermissionJson( permission_name: String, extra_data: Option[List[String]] diff --git a/obp-api/src/main/scala/code/bankconnectors/LocalMappedConnectorInternal.scala b/obp-api/src/main/scala/code/bankconnectors/LocalMappedConnectorInternal.scala index 56e5c4ac3..87a034088 100644 --- a/obp-api/src/main/scala/code/bankconnectors/LocalMappedConnectorInternal.scala +++ b/obp-api/src/main/scala/code/bankconnectors/LocalMappedConnectorInternal.scala @@ -1,14 +1,20 @@ 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.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 +22,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.{Calendar, Date} import java.util.UUID.randomUUID -import scala.concurrent._ +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 +683,701 @@ 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 { +// transactionRequestBodyFreeForm <- NewStyle.function.tryons(s"${InvalidJsonFormat}, it should be $CARDANO json format", 400, callContext) { +// json.extract[TransactionRequestBodyCardanoJsonV510] +// } +// // Following lines: just transfer the details body, add Bank_Id and Account_Id in the Detail part. This is for persistence and 'answerTransactionRequestChallenge' +// transactionRequestAccountJSON = TransactionRequestAccountJsonV140(bankId.value, accountId.value) +// transDetailsSerialized <- NewStyle.function.tryons(UnknownError, 400, callContext) { +// write(transactionRequestBodyFreeForm)(Serialization.formats(NoTypeHints)) +// } +// (createdTransactionRequest, callContext) <- NewStyle.function.createTransactionRequestv400(u, +// viewId, +// fromAccount, +// fromAccount, +// transactionRequestType, +// transactionRequestBodyFreeForm, +// transDetailsSerialized, +// sharedChargePolicy.toString, +// Some(OBP_TRANSACTION_REQUEST_CHALLENGE), +// getScaMethodAtInstance(transactionRequestType.value).toOption, +// None, +// callContext) +// } yield +// (createdTransactionRequest, callContext) +// } + } + (challenges, callContext) <- NewStyle.function.getChallengesByTransactionRequestId(createdTransactionRequest.id.value, callContext) + (transactionRequestAttributes, callContext) <- NewStyle.function.getTransactionRequestAttributes( + bankId, + createdTransactionRequest.id, + callContext + ) + } yield { + (JSONFactory400.createTransactionRequestWithChargeJSON(createdTransactionRequest, challenges, transactionRequestAttributes), HttpCode.`201`(callContext)) + } + } + + } diff --git a/obp-api/src/main/scala/code/bankconnectors/cardano/CardanoConnector_vJun2025.scala b/obp-api/src/main/scala/code/bankconnectors/cardano/CardanoConnector_vJun2025.scala index 7dce2dd03..38a7e1938 100644 --- a/obp-api/src/main/scala/code/bankconnectors/cardano/CardanoConnector_vJun2025.scala +++ b/obp-api/src/main/scala/code/bankconnectors/cardano/CardanoConnector_vJun2025.scala @@ -24,14 +24,15 @@ Berlin 13359, Germany */ import code.api.util.APIUtil._ -import code.api.util.CallContext +import code.api.util.{CallContext, CustomJsonFormats} import code.bankconnectors._ import code.util.AkkaHttpClient._ 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 @@ -46,6 +47,47 @@ trait CardanoConnector_vJun2025 extends Connector with MdcLoggable { override val messageDocs = ArrayBuffer[MessageDoc]() +// case class Amount(quantity: Long, unit: String) +// case class Output(address: String, amount: Amount, assets: List[String]) +// case class Input(address: String, amount: Amount, assets: List[String], id: String, index: Int) +// case class SlotTime(absolute_slot_number: Long, epoch_number: Int, slot_number: Long, time: String) +// case class PendingSince( +// absolute_slot_number: Long, +// epoch_number: Int, +// height: Amount, +// slot_number: Long, +// time: String +// ) +// case class ValidityInterval( +// invalid_before: Amount, +// invalid_hereafter: Amount +// ) +// case class TokenContainer(tokens: List[String]) // for mint, burn + + case class TransactionCardano( +// amount: Amount, +// burn: TokenContainer, +// certificates: List[String], +// collateral: List[String], +// collateral_outputs: List[String], +// deposit_returned: Amount, +// deposit_taken: Amount, +// direction: String, +// expires_at: SlotTime, +// extra_signatures: List[String], +// fee: Amount, + id: String//, +// inputs: List[Input], +// mint: TokenContainer, +// outputs: List[Output], +// pending_since: PendingSince, +// script_validity: String, +// status: String, +// validity_interval: ValidityInterval, +// withdrawals: List[String] + ) + + override def makePaymentv210(fromAccount: BankAccount, toAccount: BankAccount, @@ -56,21 +98,123 @@ 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) + implicit val formats = CustomJsonFormats.nullTolerateFormats + + case class TransactionCardano2( + id: String + ) + + val paramUrl = "http://localhost:8090/v2/wallets/62b27359c25d4f2a5f97acee521ac1df7ac5a606/transactions" + val method = "POST" + val jsonToSend = """{ + | "payments": [ + | { + | "address": "addr_test1qpv3se9ghq87ud29l0a8asy8nlqwd765e5zt4rc2z4mktqulwagn832cuzcjknfyxwzxz2p2kumx6n58tskugny6mrqs7fd23z", + | "amount": { + | "quantity": 1000000, + | "unit": "lovelace" + | } + | } + | ], + | "passphrase": "StrongPassword123!" + |}""".stripMargin + val request = prepareHttpRequest(paramUrl, _root_.akka.http.scaladsl.model.HttpMethods.POST, _root_.akka.http.scaladsl.model.HttpProtocol("HTTP/1.1"), jsonToSend) //.withHeaders(buildHeaders(paramUrl,jsonToSend,callContext)) + logger.debug(s"CardanoConnector_vJun2025.makePaymentv210 request is : $request") + val responseFuture: Future[_root_.akka.http.scaladsl.model.HttpResponse] = makeHttpRequest(request) + + val transactionFuture: Future[TransactionCardano2] = responseFuture.flatMap { response => + response.entity.dataBytes.runFold(_root_.akka.util.ByteString(""))(_ ++ _).map(_.utf8String).map { jsonString: String => + logger.debug(s"CardanoConnector_vJun2025.makePaymentv210 response jsonString is : $jsonString") + val jValue: JValue = json.parse(jsonString) + val id = (jValue \ "id").values.toString + TransactionCardano2(id) } - 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) + } + + transactionFuture.map { tx => + (Full(TransactionId(tx.id)), callContext) + } + } +// 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 myApp extends App{ + + implicit val formats = CustomJsonFormats.nullTolerateFormats + + val aaa ="""{"amount":{"quantity":1168537,"unit":"lovelace"},"burn":{"tokens":[]},"certificates":[],"collateral":[],"collateral_outputs":[],"deposit_returned":{"quantity":0,"unit":"lovelace"},"deposit_taken":{"quantity":0,"unit":"lovelace"},"direction":"outgoing","expires_at":{"absolute_slot_number":97089863,"epoch_number":228,"slot_number":235463,"time":"2025-07-17T17:24:23Z"},"extra_signatures":[],"fee":{"quantity":168537,"unit":"lovelace"},"id":"7aa959f2408ac15b9831a78f6639737c6124610f8b6c9f010bd72f9da2db8aa4","inputs":[{"address":"addr_test1qzq9w0xx8qerljrm59vs02e89vkqxqpwljcra09lr8gmjch9ygp2lexx64xjddk6l3k5k60uelxz3q2dumx99etycq9qnev3j8","amount":{"quantity":4988973201,"unit":"lovelace"},"assets":[],"id":"d8127d7e242d10c0f496fe808989826807806ff5d8ceeb8e3b4c0f31704a6e08","index":1}],"mint":{"tokens":[]},"outputs":[{"address":"addr_test1qpv3se9ghq87ud29l0a8asy8nlqwd765e5zt4rc2z4mktqulwagn832cuzcjknfyxwzxz2p2kumx6n58tskugny6mrqs7fd23z","amount":{"quantity":1000000,"unit":"lovelace"},"assets":[]},{"address":"addr_test1qzw70uzlms3ktewhqly4s45d9m0ks6tueajtjzvhy2y2ft89ygp2lexx64xjddk6l3k5k60uelxz3q2dumx99etycq9q4keup2","amount":{"quantity":4987804664,"unit":"lovelace"},"assets":[]}],"pending_since":{"absolute_slot_number":97082631,"epoch_number":228,"height":{"quantity":3684539,"unit":"block"},"slot_number":228231,"time":"2025-07-17T15:23:51Z"},"script_validity":"valid","status":"pending","validity_interval":{"invalid_before":{"quantity":0,"unit":"slot"},"invalid_hereafter":{"quantity":97089863,"unit":"slot"}},"withdrawals":[]}""".stripMargin + + val aaa1 = """{"id":"123"}""" + println(aaa) + + + case class Amount(quantity: Long, unit: String) + case class Output(address: String, amount: Amount, assets: List[String]) + case class Input(address: String, amount: Amount, assets: List[String], id: String, index: Int) + case class SlotTime(absolute_slot_number: Long, epoch_number: Int, slot_number: Long, time: String) + case class PendingSince( + absolute_slot_number: Long, + epoch_number: Int, + height: Amount, + slot_number: Long, + time: String + ) + case class ValidityInterval( + invalid_before: Amount, + invalid_hereafter: Amount + ) + case class TokenContainer(tokens: List[String]) // for mint, burn + + case class TransactionCardano( + amount: Amount, + burn: TokenContainer, + certificates: List[String], + collateral: List[String], + collateral_outputs: List[String], + deposit_returned: Amount, + deposit_taken: Amount, + direction: String, + expires_at: SlotTime, + extra_signatures: List[String], + fee: Amount, + id: String, + inputs: List[Input], + mint: TokenContainer, + outputs: List[Output], + pending_since: PendingSince, + script_validity: String, + status: String, + validity_interval: ValidityInterval, + withdrawals: List[String] + ) + private val jValue = json.parse(aaa) + println(jValue) + private val transaction: TransactionCardano = jValue.extract[TransactionCardano] + println(transaction) +} diff --git a/obp-commons/src/main/scala/com/openbankproject/commons/model/enums/Enumerations.scala b/obp-commons/src/main/scala/com/openbankproject/commons/model/enums/Enumerations.scala index 6de788ee3..1497cebff 100644 --- a/obp-commons/src/main/scala/com/openbankproject/commons/model/enums/Enumerations.scala +++ b/obp-commons/src/main/scala/com/openbankproject/commons/model/enums/Enumerations.scala @@ -1,11 +1,11 @@ package com.openbankproject.commons.model.enums -import java.time.format.DateTimeFormatter - import com.openbankproject.commons.util.{EnumValue, JsonAble, OBPEnumeration} import net.liftweb.common.Box import net.liftweb.json.JsonAST.{JNothing, JString} -import net.liftweb.json.{Formats, JBool, JDouble, JInt, JValue} +import net.liftweb.json._ + +import java.time.format.DateTimeFormatter sealed trait UserAttributeType extends EnumValue @@ -113,6 +113,7 @@ object TransactionRequestTypes extends OBPEnumeration[TransactionRequestTypes]{ object CROSS_BORDER_CREDIT_TRANSFERS extends Value object REFUND extends Value object AGENT_CASH_WITHDRAWAL extends Value + object CARDANO extends Value } sealed trait StrongCustomerAuthentication extends EnumValue From 76ec1aab59c3492c4781b56106adb6f7412b7899 Mon Sep 17 00:00:00 2001 From: hongwei Date: Tue, 22 Jul 2025 15:35:30 +0200 Subject: [PATCH 02/28] refactor/enhanced the error handling for NewStyle.tryons method --- .../main/scala/code/api/util/NewStyle.scala | 42 ++++++++++++++----- 1 file changed, 32 insertions(+), 10 deletions(-) diff --git a/obp-api/src/main/scala/code/api/util/NewStyle.scala b/obp-api/src/main/scala/code/api/util/NewStyle.scala index 41797fa88..964ac24dc 100644 --- a/obp-api/src/main/scala/code/api/util/NewStyle.scala +++ b/obp-api/src/main/scala/code/api/util/NewStyle.scala @@ -762,23 +762,45 @@ object NewStyle extends MdcLoggable{ def isEnabledTransactionRequests(callContext: Option[CallContext]): Future[Box[Unit]] = Helper.booleanToFuture(failMsg = TransactionRequestsNotEnabled, cc=callContext)(APIUtil.getPropsAsBoolValue("transactionRequests_enabled", false)) /** - * Wraps a Future("try") block around the function f and - * @param f - the block of code to evaluate - * @return - */ - def tryons[T](failMsg: String, failCode: Int = 400, callContext: Option[CallContext])(f: => T)(implicit m: Manifest[T]): Future[T]= { + * Wraps a computation `f` in a Future, capturing exceptions and returning detailed error messages. + * + * @param failMsg Base error message to return if the computation fails. + * @param failCode HTTP status code to return on failure (default: 400). + * @param callContext Optional call context for logging or metadata. + * @param f The computation to execute (call-by-name to defer evaluation). + * @param m Implicit Manifest for type `T` (handled by Scala compiler). + * @return Future[T] Success: Result of `f`; Failure: Detailed error message. + */ + def tryons[T]( + failMsg: String, + failCode: Int = 400, + callContext: Option[CallContext] + )(f: => T)(implicit m: Manifest[T]): Future[T] = { Future { - tryo { - f + try { + // Attempt to execute `f` and wrap the result in `Full` (success) or `Failure` (error) + tryo(f) match { + case Full(result) => + Full(result) // Success: Forward the result + case Failure(msg, _, _) => + // `tryo` encountered an exception (e.g., validation error) + Failure(s"$failMsg. Details: $msg", Empty, Empty) + case Empty => + // Edge case: Empty result (unlikely but handled defensively) + Failure(s"$failMsg. Details: Empty result", Empty, Empty) + } + } catch { + case e: Exception => + // Directly caught exception (e.g., JSON parsing error) + Failure(s"$failMsg. Details: ${e.getMessage}", Full(e), Empty) } } map { + // Convert `Box[T]` to `Future[T]` using `unboxFullOrFail` x => unboxFullOrFail(x, callContext, failMsg, failCode) } } + def extractHttpParamsFromUrl(url: String): Future[List[HTTPParam]] = { createHttpParamsByUrlFuture(url) map { unboxFull(_) } } From 2f78d9a23e67f1b92f5be086a258828c320449c5 Mon Sep 17 00:00:00 2001 From: hongwei Date: Tue, 22 Jul 2025 16:59:15 +0200 Subject: [PATCH 03/28] refactor/comment the soap in pom --- obp-api/pom.xml | 37 +++++++++++++++++++------------------ 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/obp-api/pom.xml b/obp-api/pom.xml index 69e3c2790..290e716c4 100644 --- a/obp-api/pom.xml +++ b/obp-api/pom.xml @@ -648,24 +648,25 @@ ${java.version} - - org.scalaxb - scalaxb-maven-plugin - 1.7.5 - - code.adapter.soap - src/main/resources/custom_webapp/wsdl - src/main/resources/custom_webapp/xsd - - - - scalaxb - - generate - - - - + + + + + + + + + + + + + + + + + + + + + ZZ12_Cardano + Cardano + ada + null + 6 + + + + ZZ12_Cardano_Lovelace + Lovelace + lovelace + null + 0 + \ No newline at end of file From 5a0083a5eb5633f9951df16c85d6189fbf5809f3 Mon Sep 17 00:00:00 2001 From: hongwei Date: Tue, 22 Jul 2025 17:30:59 +0200 Subject: [PATCH 05/28] refactor/improve error handling in createCounterparty method --- .../counterparties/MapperCounterparties.scala | 66 +++++++++---------- 1 file changed, 31 insertions(+), 35 deletions(-) diff --git a/obp-api/src/main/scala/code/metadata/counterparties/MapperCounterparties.scala b/obp-api/src/main/scala/code/metadata/counterparties/MapperCounterparties.scala index 1066aece8..e4c0e430c 100644 --- a/obp-api/src/main/scala/code/metadata/counterparties/MapperCounterparties.scala +++ b/obp-api/src/main/scala/code/metadata/counterparties/MapperCounterparties.scala @@ -1,24 +1,20 @@ package code.metadata.counterparties -import java.util.UUID.randomUUID -import java.util.{Date, UUID} - import code.api.cache.Caching -import code.api.util.{APIUtil, CallContext} -import code.api.util.APIUtil.getSecondsCache -import code.model._ +import code.api.util.APIUtil import code.model.dataAccess.ResourceUser import code.users.Users import code.util.Helper.MdcLoggable import code.util._ -import com.google.common.cache.CacheBuilder import com.openbankproject.commons.model._ import com.tesobe.CacheKeyFromArguments import net.liftweb.common.{Box, Full} -import net.liftweb.mapper.{By, MappedString, _} +import net.liftweb.mapper._ import net.liftweb.util.Helpers.tryo import net.liftweb.util.StringHelpers +import java.util.UUID.randomUUID +import java.util.{Date, UUID} import scala.concurrent.duration._ // For now, there are two Counterparties: one is used for CreateCounterParty.Counterparty, the other is for getTransactions.Counterparty. @@ -210,34 +206,34 @@ object MapperCounterparties extends Counterparties with MdcLoggable { currency: String, bespoke: List[CounterpartyBespoke] ): Box[CounterpartyTrait] = { + tryo{ + val mappedCounterparty = MappedCounterparty.create + .mCounterPartyId(APIUtil.createExplicitCounterpartyId) //We create the Counterparty_Id here, it means, it will be 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() - 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( From 0557efb4e64ccc636ac247ed9ee328deb210f12a Mon Sep 17 00:00:00 2001 From: hongwei Date: Tue, 22 Jul 2025 17:33:01 +0200 Subject: [PATCH 06/28] refactor/update InvalidISOCurrencyCode error message for clarity --- obp-api/src/main/scala/code/api/util/ErrorMessages.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/obp-api/src/main/scala/code/api/util/ErrorMessages.scala b/obp-api/src/main/scala/code/api/util/ErrorMessages.scala index c2a49fa27..07da1e107 100644 --- a/obp-api/src/main/scala/code/api/util/ErrorMessages.scala +++ b/obp-api/src/main/scala/code/api/util/ErrorMessages.scala @@ -79,7 +79,7 @@ object ErrorMessages { // General messages (OBP-10XXX) val InvalidJsonFormat = "OBP-10001: Incorrect json format." val InvalidNumber = "OBP-10002: Invalid Number. Could not convert value to a number." - val InvalidISOCurrencyCode = "OBP-10003: Invalid Currency Value. It should be three letters ISO Currency Code. " + val InvalidISOCurrencyCode = """OBP-10003: Invalid Currency Value. Expected a 3-letter ISO Currency Code (e.g., 'USD', 'EUR') or 'lovelace' for Cardano transactions.""".stripMargin val FXCurrencyCodeCombinationsNotSupported = "OBP-10004: ISO Currency code combination not supported for FX. Please modify the FROM_CURRENCY_CODE or TO_CURRENCY_CODE. " val InvalidDateFormat = "OBP-10005: Invalid Date Format. Could not convert value to a Date." val InvalidCurrency = "OBP-10006: Invalid Currency Value." From 1bc1014988d223acc3c6e4126e4237880b6ea53a Mon Sep 17 00:00:00 2001 From: hongwei Date: Thu, 24 Jul 2025 11:07:09 +0200 Subject: [PATCH 07/28] feature/add support for Cardano transactions and enhance transaction request handling --- .../SwaggerDefinitionsJSON.scala | 7 +- .../scala/code/api/v5_1_0/APIMethods510.scala | 33 ++++--- .../code/api/v5_1_0/JSONFactory5.1.0.scala | 13 ++- .../bankconnectors/LocalMappedConnector.scala | 2 + .../LocalMappedConnectorInternal.scala | 88 +++++++++++++------ .../cardano/CardanoConnector_vJun2025.scala | 67 +++++++------- .../counterparties/MapperCounterparties.scala | 2 +- 7 files changed, 123 insertions(+), 89 deletions(-) diff --git a/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala b/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala index 7e3925cc6..94a2875d4 100644 --- a/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala +++ b/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala @@ -5699,12 +5699,13 @@ object SwaggerDefinitionsJSON { lazy val cardanoPaymentJsonV510 = CardanoPaymentJsonV510( address = "addr_test1qpv3se9ghq87ud29l0a8asy8nlqwd765e5zt4rc2z4mktqulwagn832cuzcjknfyxwzxz2p2kumx6n58tskugny6mrqs7fd12", - amount = amountOfMoneyJsonV121 ) lazy val transactionRequestBodyCardanoJsonV510 = TransactionRequestBodyCardanoJsonV510( - to = List(cardanoPaymentJsonV510), - passphrase = "password1234!" + to = cardanoPaymentJsonV510, + value = amountOfMoneyJsonV121, + passphrase = "password1234!", + description = descriptionExample.value, ) //The common error or success format. diff --git a/obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala b/obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala index 96f6c612b..a4351df31 100644 --- a/obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala +++ b/obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala @@ -30,8 +30,8 @@ import code.api.v4_0_0._ import code.api.v5_0_0.JSONFactory500 import code.api.v5_1_0.JSONFactory510.{createConsentsInfoJsonV510, createConsentsJsonV510, createRegulatedEntitiesJson, createRegulatedEntityJson} import code.atmattribute.AtmAttribute -import code.bankconnectors.Connector import code.bankconnectors.LocalMappedConnectorInternal._ +import code.bankconnectors.{Connector, LocalMappedConnectorInternal} import code.consent.{ConsentRequests, ConsentStatus, Consents, MappedConsent} import code.consumer.Consumers import code.entitlement.Entitlement @@ -5350,23 +5350,22 @@ trait APIMethods510 { case "banks" :: "cardano" :: "accounts" :: AccountId(accountId) :: ViewId(viewId) :: "transaction-request-types" :: "CARDANO" :: "transaction-requests" :: Nil JsonPost json -> _ => cc => implicit val ec = EndpointContext(Some(cc)) - - for { - (createdTransactionId, callContext) <- NewStyle.function.makePaymentv210( - null, - null, - null, - null, - null, - "", //BG no description so far - null, - "", // chargePolicy is not used in BG so far., - cc.callContext - ) - } yield (createdTransactionId, HttpCode.`201`(cc.callContext)) +// for { +// (createdTransactionId, callContext) <- NewStyle.function.makePaymentv210( +// null, +// null, +// null, +// null, +// null, +// "", //BG no description so far +// null, +// "", // chargePolicy is not used in BG so far., +// cc.callContext +// ) +// } yield (createdTransactionId, HttpCode.`201`(cc.callContext)) -// val transactionRequestType = TransactionRequestType("CARDANO") -// LocalMappedConnectorInternal.createTransactionRequest(BankId("cardano"), accountId, viewId , transactionRequestType, json) + val transactionRequestType = TransactionRequestType("CARDANO") + LocalMappedConnectorInternal.createTransactionRequest(BankId("cardano"), accountId, viewId , transactionRequestType, json) } diff --git a/obp-api/src/main/scala/code/api/v5_1_0/JSONFactory5.1.0.scala b/obp-api/src/main/scala/code/api/v5_1_0/JSONFactory5.1.0.scala index 72db5f730..09b9f71ed 100644 --- a/obp-api/src/main/scala/code/api/v5_1_0/JSONFactory5.1.0.scala +++ b/obp-api/src/main/scala/code/api/v5_1_0/JSONFactory5.1.0.scala @@ -579,16 +579,15 @@ case class ConsentRequestToAccountJson( ) case class CardanoPaymentJsonV510( - address: String, - amount: AmountOfMoneyJsonV121 + address: String ) case class TransactionRequestBodyCardanoJsonV510( - to: List[CardanoPaymentJsonV510], - passphrase: String -// value: AmountOfMoneyJsonV121, -// description: String, -) //extends TransactionRequestCommonBodyJSON + to: CardanoPaymentJsonV510, + value: AmountOfMoneyJsonV121, + passphrase: String, + description: String, +) extends TransactionRequestCommonBodyJSON case class CreateViewPermissionJson( permission_name: String, diff --git a/obp-api/src/main/scala/code/bankconnectors/LocalMappedConnector.scala b/obp-api/src/main/scala/code/bankconnectors/LocalMappedConnector.scala index 19fcd715f..8ac8f43f1 100644 --- a/obp-api/src/main/scala/code/bankconnectors/LocalMappedConnector.scala +++ b/obp-api/src/main/scala/code/bankconnectors/LocalMappedConnector.scala @@ -164,6 +164,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.equals("lovelace")||(currency.equals("ada")))) => + (Full(AmountOfMoney(currency, "10000000000000")), callContext) case true => fx.exchangeRate(thresholdCurrency, currency, Some(bankId), callContext) match { case rate@Some(_) => diff --git a/obp-api/src/main/scala/code/bankconnectors/LocalMappedConnectorInternal.scala b/obp-api/src/main/scala/code/bankconnectors/LocalMappedConnectorInternal.scala index 87a034088..ae197c04e 100644 --- a/obp-api/src/main/scala/code/bankconnectors/LocalMappedConnectorInternal.scala +++ b/obp-api/src/main/scala/code/bankconnectors/LocalMappedConnectorInternal.scala @@ -13,6 +13,7 @@ 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.v5_1_0.TransactionRequestBodyCardanoJsonV510 import code.branches.MappedBranch import code.fx.fx import code.fx.fx.TTL @@ -39,8 +40,8 @@ import net.liftweb.util.Helpers.{now, tryo} import net.liftweb.util.StringHelpers import java.time.{LocalDate, ZoneId} -import java.util.{Calendar, Date} import java.util.UUID.randomUUID +import java.util.{Calendar, Date} import scala.collection.immutable.{List, Nil} import scala.concurrent.Future import scala.concurrent.duration.DurationInt @@ -754,6 +755,19 @@ object LocalMappedConnectorInternal extends MdcLoggable { } yield{ (cardFromCbs.account, callContext) } +// case CARDANO => +// for{ +// transactionRequestBodyCardanoJson <- NewStyle.function.tryons(s"${InvalidJsonFormat}, it should be $CARD json format", 400, callContext) { +// json.extract[TransactionRequestBodyCardanoJsonV510] +// } +// (account, callContext) <- NewStyle.function.getBankAccountByRouting( +// None, //No need for the bankId, only to address is enough +// "cardano_wallet_id", +// transactionRequestBodyCardanoJson.to.address, +// callContext +// ) +// } yield +// (account, callContext) case _ => NewStyle.function.getBankAccount(bankId,accountId, callContext) } @@ -1342,31 +1356,53 @@ object LocalMappedConnectorInternal extends MdcLoggable { } yield (createdTransactionRequest, callContext) } -// case CARDANO => { -// for { -// transactionRequestBodyFreeForm <- NewStyle.function.tryons(s"${InvalidJsonFormat}, it should be $CARDANO json format", 400, callContext) { -// json.extract[TransactionRequestBodyCardanoJsonV510] -// } -// // 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 $TransactionRequestBodyCardanoJsonV510 json format", 400, callContext) { + json.extract[TransactionRequestBodyCardanoJsonV510] + } + (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( diff --git a/obp-api/src/main/scala/code/bankconnectors/cardano/CardanoConnector_vJun2025.scala b/obp-api/src/main/scala/code/bankconnectors/cardano/CardanoConnector_vJun2025.scala index 38a7e1938..27457148d 100644 --- a/obp-api/src/main/scala/code/bankconnectors/cardano/CardanoConnector_vJun2025.scala +++ b/obp-api/src/main/scala/code/bankconnectors/cardano/CardanoConnector_vJun2025.scala @@ -25,6 +25,7 @@ Berlin 13359, Germany import code.api.util.APIUtil._ import code.api.util.{CallContext, CustomJsonFormats} +import code.api.v5_1_0.TransactionRequestBodyCardanoJsonV510 import code.bankconnectors._ import code.util.AkkaHttpClient._ import code.util.Helper.MdcLoggable @@ -89,7 +90,8 @@ trait CardanoConnector_vJun2025 extends Connector with MdcLoggable { - override def makePaymentv210(fromAccount: BankAccount, + override def makePaymentv210( + fromAccount: BankAccount, toAccount: BankAccount, transactionRequestId: TransactionRequestId, transactionRequestCommonBody: TransactionRequestCommonBodyJSON, @@ -98,42 +100,37 @@ trait CardanoConnector_vJun2025 extends Connector with MdcLoggable { transactionRequestType: TransactionRequestType, chargePolicy: String, callContext: Option[CallContext]): OBPReturnType[Box[TransactionId]] = { - implicit val formats = CustomJsonFormats.nullTolerateFormats - - case class TransactionCardano2( - id: String - ) - val paramUrl = "http://localhost:8090/v2/wallets/62b27359c25d4f2a5f97acee521ac1df7ac5a606/transactions" - val method = "POST" - val jsonToSend = """{ - | "payments": [ - | { - | "address": "addr_test1qpv3se9ghq87ud29l0a8asy8nlqwd765e5zt4rc2z4mktqulwagn832cuzcjknfyxwzxz2p2kumx6n58tskugny6mrqs7fd23z", - | "amount": { - | "quantity": 1000000, - | "unit": "lovelace" - | } - | } - | ], - | "passphrase": "StrongPassword123!" - |}""".stripMargin - val request = prepareHttpRequest(paramUrl, _root_.akka.http.scaladsl.model.HttpMethods.POST, _root_.akka.http.scaladsl.model.HttpProtocol("HTTP/1.1"), jsonToSend) //.withHeaders(buildHeaders(paramUrl,jsonToSend,callContext)) - logger.debug(s"CardanoConnector_vJun2025.makePaymentv210 request is : $request") - val responseFuture: Future[_root_.akka.http.scaladsl.model.HttpResponse] = makeHttpRequest(request) - - val transactionFuture: Future[TransactionCardano2] = responseFuture.flatMap { response => - response.entity.dataBytes.runFold(_root_.akka.util.ByteString(""))(_ ++ _).map(_.utf8String).map { jsonString: String => - logger.debug(s"CardanoConnector_vJun2025.makePaymentv210 response jsonString is : $jsonString") - val jValue: JValue = json.parse(jsonString) - val id = (jValue \ "id").values.toString - TransactionCardano2(id) + val walletId = fromAccount.accountId.value + val transactionRequestBodyCardanoJson = transactionRequestCommonBody.asInstanceOf[TransactionRequestBodyCardanoJsonV510] + val paramUrl = s"http://localhost:8090/v2/wallets/${walletId}/transactions" + val jsonToSend = s"""{ + | "payments": [ + | { + | "address": "addr_test1qpv3se9ghq87ud29l0a8asy8nlqwd765e5zt4rc2z4mktqulwagn832cuzcjknfyxwzxz2p2kumx6n58tskugny6mrqs7fd23z", + | "amount": { + | "quantity": "${transactionRequestCommonBody.value.amount}", + | "unit": "${transactionRequestCommonBody.value.currency}" + | } + | } + | ], + | "passphrase": "${transactionRequestBodyCardanoJson.passphrase}", + |}""".stripMargin + val request = prepareHttpRequest(paramUrl, _root_.akka.http.scaladsl.model.HttpMethods.POST, _root_.akka.http.scaladsl.model.HttpProtocol("HTTP/1.1"), jsonToSend) //.withHeaders(buildHeaders(paramUrl,jsonToSend,callContext)) + logger.debug(s"CardanoConnector_vJun2025.makePaymentv210 request is : $request") + val responseFuture: Future[_root_.akka.http.scaladsl.model.HttpResponse] = makeHttpRequest(request) + + val transactionIdFuture = responseFuture.flatMap { response => + response.entity.dataBytes.runFold(_root_.akka.util.ByteString(""))(_ ++ _).map(_.utf8String).map { jsonString: String => + logger.debug(s"CardanoConnector_vJun2025.makePaymentv210 response jsonString is : $jsonString") + val jValue: JValue = json.parse(jsonString) + (jValue \ "id").values.toString + } + } + + transactionIdFuture.map { id => + (Full(TransactionId(id)), callContext) } - } - - transactionFuture.map { tx => - (Full(TransactionId(tx.id)), callContext) - } } // override def makePaymentv210(fromAccount: BankAccount, diff --git a/obp-api/src/main/scala/code/metadata/counterparties/MapperCounterparties.scala b/obp-api/src/main/scala/code/metadata/counterparties/MapperCounterparties.scala index e4c0e430c..08c244a22 100644 --- a/obp-api/src/main/scala/code/metadata/counterparties/MapperCounterparties.scala +++ b/obp-api/src/main/scala/code/metadata/counterparties/MapperCounterparties.scala @@ -208,7 +208,7 @@ object MapperCounterparties extends Counterparties with MdcLoggable { ): Box[CounterpartyTrait] = { tryo{ val mappedCounterparty = MappedCounterparty.create - .mCounterPartyId(APIUtil.createExplicitCounterpartyId) //We create the Counterparty_Id here, it means, it will be create in each connector. + .mCounterPartyId(APIUtil.createExplicitCounterpartyId) //We create the Counterparty_Id here, it means, it will be created in each connector. .mName(name) .mCreatedByUserId(createdByUserId) .mThisBankId(thisBankId) From 9b0b49d34f3e1f1f46b12b85ae6be8493976798a Mon Sep 17 00:00:00 2001 From: hongwei Date: Thu, 24 Jul 2025 11:32:10 +0200 Subject: [PATCH 08/28] refactor/improve error handling and logging in makePaymentv210 method --- .../cardano/CardanoConnector_vJun2025.scala | 182 +++++------------- 1 file changed, 53 insertions(+), 129 deletions(-) diff --git a/obp-api/src/main/scala/code/bankconnectors/cardano/CardanoConnector_vJun2025.scala b/obp-api/src/main/scala/code/bankconnectors/cardano/CardanoConnector_vJun2025.scala index 27457148d..3c3f3f18b 100644 --- a/obp-api/src/main/scala/code/bankconnectors/cardano/CardanoConnector_vJun2025.scala +++ b/obp-api/src/main/scala/code/bankconnectors/cardano/CardanoConnector_vJun2025.scala @@ -24,10 +24,11 @@ Berlin 13359, Germany */ import code.api.util.APIUtil._ -import code.api.util.{CallContext, CustomJsonFormats} +import code.api.util.{CallContext, ErrorMessages, NewStyle} import code.api.v5_1_0.TransactionRequestBodyCardanoJsonV510 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._ @@ -43,53 +44,11 @@ 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]() -// case class Amount(quantity: Long, unit: String) -// case class Output(address: String, amount: Amount, assets: List[String]) -// case class Input(address: String, amount: Amount, assets: List[String], id: String, index: Int) -// case class SlotTime(absolute_slot_number: Long, epoch_number: Int, slot_number: Long, time: String) -// case class PendingSince( -// absolute_slot_number: Long, -// epoch_number: Int, -// height: Amount, -// slot_number: Long, -// time: String -// ) -// case class ValidityInterval( -// invalid_before: Amount, -// invalid_hereafter: Amount -// ) -// case class TokenContainer(tokens: List[String]) // for mint, burn - - case class TransactionCardano( -// amount: Amount, -// burn: TokenContainer, -// certificates: List[String], -// collateral: List[String], -// collateral_outputs: List[String], -// deposit_returned: Amount, -// deposit_taken: Amount, -// direction: String, -// expires_at: SlotTime, -// extra_signatures: List[String], -// fee: Amount, - id: String//, -// inputs: List[Input], -// mint: TokenContainer, -// outputs: List[Output], -// pending_since: PendingSince, -// script_validity: String, -// status: String, -// validity_interval: ValidityInterval, -// withdrawals: List[String] - ) - - - override def makePaymentv210( fromAccount: BankAccount, toAccount: BankAccount, @@ -100,38 +59,58 @@ trait CardanoConnector_vJun2025 extends Connector with MdcLoggable { transactionRequestType: TransactionRequestType, chargePolicy: String, callContext: Option[CallContext]): OBPReturnType[Box[TransactionId]] = { - - val walletId = fromAccount.accountId.value - val transactionRequestBodyCardanoJson = transactionRequestCommonBody.asInstanceOf[TransactionRequestBodyCardanoJsonV510] - val paramUrl = s"http://localhost:8090/v2/wallets/${walletId}/transactions" - val jsonToSend = s"""{ - | "payments": [ - | { - | "address": "addr_test1qpv3se9ghq87ud29l0a8asy8nlqwd765e5zt4rc2z4mktqulwagn832cuzcjknfyxwzxz2p2kumx6n58tskugny6mrqs7fd23z", - | "amount": { - | "quantity": "${transactionRequestCommonBody.value.amount}", - | "unit": "${transactionRequestCommonBody.value.currency}" - | } - | } - | ], - | "passphrase": "${transactionRequestBodyCardanoJson.passphrase}", - |}""".stripMargin - val request = prepareHttpRequest(paramUrl, _root_.akka.http.scaladsl.model.HttpMethods.POST, _root_.akka.http.scaladsl.model.HttpProtocol("HTTP/1.1"), jsonToSend) //.withHeaders(buildHeaders(paramUrl,jsonToSend,callContext)) - logger.debug(s"CardanoConnector_vJun2025.makePaymentv210 request is : $request") - val responseFuture: Future[_root_.akka.http.scaladsl.model.HttpResponse] = makeHttpRequest(request) + + for { + failMsg <- Future.successful(s"${ErrorMessages.InvalidJsonFormat} The transaction request body should be $TransactionRequestBodyCardanoJsonV510") + transactionRequestBodyCardano <- NewStyle.function.tryons(failMsg, 400, callContext) { + transactionRequestCommonBody.asInstanceOf[TransactionRequestBodyCardanoJsonV510] + } + + walletId = fromAccount.accountId.value + paramUrl = s"http://localhost:8090/v2/wallets/${walletId}/transactions" + jsonToSend = s"""{ + | "payments": [ + | { + | "address": "addr_test1qpv3se9ghq87ud29l0a8asy8nlqwd765e5zt4rc2z4mktqulwagn832cuzcjknfyxwzxz2p2kumx6n58tskugny6mrqs7fd23z", + | "amount": { + | "quantity": ${transactionRequestCommonBody.value.amount}, + | "unit": "${transactionRequestCommonBody.value.currency}" + | } + | } + | ], + | "passphrase": "${transactionRequestBodyCardano.passphrase}" + |}""".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 - val transactionIdFuture = responseFuture.flatMap { response => - response.entity.dataBytes.runFold(_root_.akka.util.ByteString(""))(_ ++ _).map(_.utf8String).map { jsonString: String => - logger.debug(s"CardanoConnector_vJun2025.makePaymentv210 response jsonString is : $jsonString") - val jValue: JValue = json.parse(jsonString) - (jValue \ "id").values.toString + _ <- 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 <- 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") } } - - transactionIdFuture.map { id => - (Full(TransactionId(id)), callContext) - } - + + } yield { + (Full(transactionId), callContext) + } } // override def makePaymentv210(fromAccount: BankAccount, // toAccount: BankAccount, @@ -159,59 +138,4 @@ trait CardanoConnector_vJun2025 extends Connector with MdcLoggable { // } } -object CardanoConnector_vJun2025 extends CardanoConnector_vJun2025 - -object myApp extends App{ - - implicit val formats = CustomJsonFormats.nullTolerateFormats - - val aaa ="""{"amount":{"quantity":1168537,"unit":"lovelace"},"burn":{"tokens":[]},"certificates":[],"collateral":[],"collateral_outputs":[],"deposit_returned":{"quantity":0,"unit":"lovelace"},"deposit_taken":{"quantity":0,"unit":"lovelace"},"direction":"outgoing","expires_at":{"absolute_slot_number":97089863,"epoch_number":228,"slot_number":235463,"time":"2025-07-17T17:24:23Z"},"extra_signatures":[],"fee":{"quantity":168537,"unit":"lovelace"},"id":"7aa959f2408ac15b9831a78f6639737c6124610f8b6c9f010bd72f9da2db8aa4","inputs":[{"address":"addr_test1qzq9w0xx8qerljrm59vs02e89vkqxqpwljcra09lr8gmjch9ygp2lexx64xjddk6l3k5k60uelxz3q2dumx99etycq9qnev3j8","amount":{"quantity":4988973201,"unit":"lovelace"},"assets":[],"id":"d8127d7e242d10c0f496fe808989826807806ff5d8ceeb8e3b4c0f31704a6e08","index":1}],"mint":{"tokens":[]},"outputs":[{"address":"addr_test1qpv3se9ghq87ud29l0a8asy8nlqwd765e5zt4rc2z4mktqulwagn832cuzcjknfyxwzxz2p2kumx6n58tskugny6mrqs7fd23z","amount":{"quantity":1000000,"unit":"lovelace"},"assets":[]},{"address":"addr_test1qzw70uzlms3ktewhqly4s45d9m0ks6tueajtjzvhy2y2ft89ygp2lexx64xjddk6l3k5k60uelxz3q2dumx99etycq9q4keup2","amount":{"quantity":4987804664,"unit":"lovelace"},"assets":[]}],"pending_since":{"absolute_slot_number":97082631,"epoch_number":228,"height":{"quantity":3684539,"unit":"block"},"slot_number":228231,"time":"2025-07-17T15:23:51Z"},"script_validity":"valid","status":"pending","validity_interval":{"invalid_before":{"quantity":0,"unit":"slot"},"invalid_hereafter":{"quantity":97089863,"unit":"slot"}},"withdrawals":[]}""".stripMargin - - val aaa1 = """{"id":"123"}""" - println(aaa) - - - case class Amount(quantity: Long, unit: String) - case class Output(address: String, amount: Amount, assets: List[String]) - case class Input(address: String, amount: Amount, assets: List[String], id: String, index: Int) - case class SlotTime(absolute_slot_number: Long, epoch_number: Int, slot_number: Long, time: String) - case class PendingSince( - absolute_slot_number: Long, - epoch_number: Int, - height: Amount, - slot_number: Long, - time: String - ) - case class ValidityInterval( - invalid_before: Amount, - invalid_hereafter: Amount - ) - case class TokenContainer(tokens: List[String]) // for mint, burn - - case class TransactionCardano( - amount: Amount, - burn: TokenContainer, - certificates: List[String], - collateral: List[String], - collateral_outputs: List[String], - deposit_returned: Amount, - deposit_taken: Amount, - direction: String, - expires_at: SlotTime, - extra_signatures: List[String], - fee: Amount, - id: String, - inputs: List[Input], - mint: TokenContainer, - outputs: List[Output], - pending_since: PendingSince, - script_validity: String, - status: String, - validity_interval: ValidityInterval, - withdrawals: List[String] - ) - private val jValue = json.parse(aaa) - println(jValue) - private val transaction: TransactionCardano = jValue.extract[TransactionCardano] - println(transaction) -} +object CardanoConnector_vJun2025 extends CardanoConnector_vJun2025 \ No newline at end of file From 9883c14fcb4cf0814a50e1a4f19df97a4015ad1c Mon Sep 17 00:00:00 2001 From: hongwei Date: Tue, 29 Jul 2025 09:20:21 +0200 Subject: [PATCH 09/28] feature/add detailed validation for Cardano payment processing and enhance JSON structure for transactions --- .../SwaggerDefinitionsJSON.scala | 28 +++++++++ .../code/api/v5_1_0/JSONFactory5.1.0.scala | 20 ++++++- .../LocalMappedConnectorInternal.scala | 58 +++++++++++++++++++ 3 files changed, 105 insertions(+), 1 deletion(-) diff --git a/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala b/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala index 94a2875d4..80b5a1ccd 100644 --- a/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala +++ b/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala @@ -5699,6 +5699,33 @@ object SwaggerDefinitionsJSON { lazy val cardanoPaymentJsonV510 = CardanoPaymentJsonV510( address = "addr_test1qpv3se9ghq87ud29l0a8asy8nlqwd765e5zt4rc2z4mktqulwagn832cuzcjknfyxwzxz2p2kumx6n58tskugny6mrqs7fd12", + amount = Some(CardanoAmountJsonV510( + quantity = 1000000, + unit = "lovelace" + )), + assets = Some(List(CardanoAssetJsonV510( + policy_id = "policy1234567890abcdef", + asset_name = "4f47435241", + quantity = 10 + ))) + ) + + // Example for Send ADA with Token only (no ADA amount) + lazy val cardanoPaymentTokenOnlyJsonV510 = CardanoPaymentJsonV510( + address = "addr_test1qpv3se9ghq87ud29l0a8asy8nlqwd765e5zt4rc2z4mktqulwagn832cuzcjknfyxwzxz2p2kumx6n58tskugny6mrqs7fd12", + amount = Some(CardanoAmountJsonV510( + quantity = 0, + unit = "lovelace" + )), + assets = Some(List(CardanoAssetJsonV510( + policy_id = "policy1234567890abcdef", + asset_name = "4f47435241", + quantity = 10 + ))) + ) + + lazy val cardanoMetadataStringJsonV510 = CardanoMetadataStringJsonV510( + string = "Hello Cardano" ) lazy val transactionRequestBodyCardanoJsonV510 = TransactionRequestBodyCardanoJsonV510( @@ -5706,6 +5733,7 @@ object SwaggerDefinitionsJSON { value = amountOfMoneyJsonV121, passphrase = "password1234!", description = descriptionExample.value, + metadata = Some(Map("202507022319" -> cardanoMetadataStringJsonV510)) ) //The common error or success format. diff --git a/obp-api/src/main/scala/code/api/v5_1_0/JSONFactory5.1.0.scala b/obp-api/src/main/scala/code/api/v5_1_0/JSONFactory5.1.0.scala index 09b9f71ed..4c18c66da 100644 --- a/obp-api/src/main/scala/code/api/v5_1_0/JSONFactory5.1.0.scala +++ b/obp-api/src/main/scala/code/api/v5_1_0/JSONFactory5.1.0.scala @@ -579,7 +579,24 @@ case class ConsentRequestToAccountJson( ) case class CardanoPaymentJsonV510( - address: String + address: String, + amount: Option[CardanoAmountJsonV510] = None, + assets: Option[List[CardanoAssetJsonV510]] = None +) + +case class CardanoAmountJsonV510( + quantity: Long, + unit: String // "lovelace" +) + +case class CardanoAssetJsonV510( + policy_id: String, + asset_name: String, + quantity: Long +) + +case class CardanoMetadataStringJsonV510( + string: String ) case class TransactionRequestBodyCardanoJsonV510( @@ -587,6 +604,7 @@ case class TransactionRequestBodyCardanoJsonV510( value: AmountOfMoneyJsonV121, passphrase: String, description: String, + metadata: Option[Map[String, CardanoMetadataStringJsonV510]] = None ) extends TransactionRequestCommonBodyJSON case class CreateViewPermissionJson( diff --git a/obp-api/src/main/scala/code/bankconnectors/LocalMappedConnectorInternal.scala b/obp-api/src/main/scala/code/bankconnectors/LocalMappedConnectorInternal.scala index ae197c04e..81a208d22 100644 --- a/obp-api/src/main/scala/code/bankconnectors/LocalMappedConnectorInternal.scala +++ b/obp-api/src/main/scala/code/bankconnectors/LocalMappedConnectorInternal.scala @@ -1362,6 +1362,64 @@ object LocalMappedConnectorInternal extends MdcLoggable { transactionRequestBodyCardano <- NewStyle.function.tryons(s"${InvalidJsonFormat} It should be $TransactionRequestBodyCardanoJsonV510 json format", 400, callContext) { json.extract[TransactionRequestBodyCardanoJsonV510] } + + // 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 if provided (can be 0 for token-only transfers) + _ <- transactionRequestBodyCardano.to.amount match { + case Some(amount) => Helper.booleanToFuture(s"$InvalidJsonValue Cardano amount quantity must be non-negative", cc=callContext) { + amount.quantity >= 0 + } + case None => Future.successful(true) + } + + // Validate amount unit if provided + _ <- transactionRequestBodyCardano.to.amount match { + case Some(amount) => Helper.booleanToFuture(s"$InvalidJsonValue Cardano amount unit must be 'lovelace'", cc=callContext) { + amount.unit == "lovelace" + } + case None => Future.successful(true) + } + + // 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 (Some(amount), Some(assets)) if amount.quantity == 0 => Helper.booleanToFuture(s"$InvalidJsonValue Cardano token-only transfer must have assets", cc=callContext) { + assets.nonEmpty + } + case (Some(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, From c169f4d07de74c870e83e1a919f267887f712695 Mon Sep 17 00:00:00 2001 From: hongwei Date: Tue, 29 Jul 2025 10:00:28 +0200 Subject: [PATCH 10/28] feature/add support for building payments array and metadata for Cardano transactions --- .../scala/code/api/v5_1_0/APIMethods510.scala | 14 ---- .../cardano/CardanoConnector_vJun2025.scala | 74 +++++++++++++++++-- 2 files changed, 67 insertions(+), 21 deletions(-) diff --git a/obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala b/obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala index a4351df31..219e7433b 100644 --- a/obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala +++ b/obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala @@ -5350,20 +5350,6 @@ trait APIMethods510 { case "banks" :: "cardano" :: "accounts" :: AccountId(accountId) :: ViewId(viewId) :: "transaction-request-types" :: "CARDANO" :: "transaction-requests" :: Nil JsonPost json -> _ => cc => implicit val ec = EndpointContext(Some(cc)) -// for { -// (createdTransactionId, callContext) <- NewStyle.function.makePaymentv210( -// null, -// null, -// null, -// null, -// null, -// "", //BG no description so far -// null, -// "", // chargePolicy is not used in BG so far., -// cc.callContext -// ) -// } yield (createdTransactionId, HttpCode.`201`(cc.callContext)) - val transactionRequestType = TransactionRequestType("CARDANO") LocalMappedConnectorInternal.createTransactionRequest(BankId("cardano"), accountId, viewId , transactionRequestType, json) } diff --git a/obp-api/src/main/scala/code/bankconnectors/cardano/CardanoConnector_vJun2025.scala b/obp-api/src/main/scala/code/bankconnectors/cardano/CardanoConnector_vJun2025.scala index 3c3f3f18b..e8f2eec46 100644 --- a/obp-api/src/main/scala/code/bankconnectors/cardano/CardanoConnector_vJun2025.scala +++ b/obp-api/src/main/scala/code/bankconnectors/cardano/CardanoConnector_vJun2025.scala @@ -68,17 +68,19 @@ trait CardanoConnector_vJun2025 extends Connector with MdcLoggable { 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": [ - | { - | "address": "addr_test1qpv3se9ghq87ud29l0a8asy8nlqwd765e5zt4rc2z4mktqulwagn832cuzcjknfyxwzxz2p2kumx6n58tskugny6mrqs7fd23z", - | "amount": { - | "quantity": ${transactionRequestCommonBody.value.amount}, - | "unit": "${transactionRequestCommonBody.value.currency}" - | } - | } + | $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) @@ -112,6 +114,64 @@ trait CardanoConnector_vJun2025 extends Connector with MdcLoggable { (Full(transactionId), callContext) } } + + /** + * Build payments array for Cardano API + * Supports different payment types: ADA only, Token only, ADA + Token + */ + private def buildPaymentsArray(transactionRequestBodyCardano: TransactionRequestBodyCardanoJsonV510): String = { + val address = transactionRequestBodyCardano.to.address + val amountJson = transactionRequestBodyCardano.to.amount match { + case Some(amount) => s""" + | "amount": { + | "quantity": ${amount.quantity}, + | "unit": "${amount.unit}" + | }""".stripMargin + case None => "" + } + + val assetsJson = transactionRequestBodyCardano.to.assets match { + case Some(assets) if assets.nonEmpty => { + val assetsArray = assets.map { asset => + s""" { + | "policy_id": "${asset.policy_id}", + | "asset_name": "${asset.asset_name}", + | "quantity": ${asset.quantity} + | }""".stripMargin + }.mkString(",\n") + s""", + | "assets": [ + |$assetsArray + | ]""".stripMargin + } + case _ => "" + } + + s""" { + | "address": "$address",$amountJson$assetsJson + | }""".stripMargin + } + + /** + * Build metadata JSON for Cardano API + * Supports simple string metadata format + */ + private def buildMetadataJson(transactionRequestBodyCardano: TransactionRequestBodyCardanoJsonV510): 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, From eecadf49e1ba31e3527d55c213db8222f465337f Mon Sep 17 00:00:00 2001 From: hongwei Date: Tue, 29 Jul 2025 10:12:08 +0200 Subject: [PATCH 11/28] feature/require amount in Cardano payment structure and enhance validation --- .../SwaggerDefinitionsJSON.scala | 8 +++--- .../code/api/v5_1_0/JSONFactory5.1.0.scala | 2 +- .../LocalMappedConnectorInternal.scala | 24 +++++++---------- .../cardano/CardanoConnector_vJun2025.scala | 27 ++++++++++++------- 4 files changed, 33 insertions(+), 28 deletions(-) diff --git a/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala b/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala index 80b5a1ccd..69209d134 100644 --- a/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala +++ b/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala @@ -5699,10 +5699,10 @@ object SwaggerDefinitionsJSON { lazy val cardanoPaymentJsonV510 = CardanoPaymentJsonV510( address = "addr_test1qpv3se9ghq87ud29l0a8asy8nlqwd765e5zt4rc2z4mktqulwagn832cuzcjknfyxwzxz2p2kumx6n58tskugny6mrqs7fd12", - amount = Some(CardanoAmountJsonV510( + amount = CardanoAmountJsonV510( quantity = 1000000, unit = "lovelace" - )), + ), assets = Some(List(CardanoAssetJsonV510( policy_id = "policy1234567890abcdef", asset_name = "4f47435241", @@ -5713,10 +5713,10 @@ object SwaggerDefinitionsJSON { // Example for Send ADA with Token only (no ADA amount) lazy val cardanoPaymentTokenOnlyJsonV510 = CardanoPaymentJsonV510( address = "addr_test1qpv3se9ghq87ud29l0a8asy8nlqwd765e5zt4rc2z4mktqulwagn832cuzcjknfyxwzxz2p2kumx6n58tskugny6mrqs7fd12", - amount = Some(CardanoAmountJsonV510( + amount = CardanoAmountJsonV510( quantity = 0, unit = "lovelace" - )), + ), assets = Some(List(CardanoAssetJsonV510( policy_id = "policy1234567890abcdef", asset_name = "4f47435241", diff --git a/obp-api/src/main/scala/code/api/v5_1_0/JSONFactory5.1.0.scala b/obp-api/src/main/scala/code/api/v5_1_0/JSONFactory5.1.0.scala index 4c18c66da..39fc61e24 100644 --- a/obp-api/src/main/scala/code/api/v5_1_0/JSONFactory5.1.0.scala +++ b/obp-api/src/main/scala/code/api/v5_1_0/JSONFactory5.1.0.scala @@ -580,7 +580,7 @@ case class ConsentRequestToAccountJson( case class CardanoPaymentJsonV510( address: String, - amount: Option[CardanoAmountJsonV510] = None, + amount: CardanoAmountJsonV510, assets: Option[List[CardanoAssetJsonV510]] = None ) diff --git a/obp-api/src/main/scala/code/bankconnectors/LocalMappedConnectorInternal.scala b/obp-api/src/main/scala/code/bankconnectors/LocalMappedConnectorInternal.scala index 81a208d22..ef460f051 100644 --- a/obp-api/src/main/scala/code/bankconnectors/LocalMappedConnectorInternal.scala +++ b/obp-api/src/main/scala/code/bankconnectors/LocalMappedConnectorInternal.scala @@ -1375,20 +1375,16 @@ object LocalMappedConnectorInternal extends MdcLoggable { transactionRequestBodyCardano.to.address.startsWith("addr_main") } - // Validate amount if provided (can be 0 for token-only transfers) - _ <- transactionRequestBodyCardano.to.amount match { - case Some(amount) => Helper.booleanToFuture(s"$InvalidJsonValue Cardano amount quantity must be non-negative", cc=callContext) { - amount.quantity >= 0 - } - case None => Future.successful(true) + + + // 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 if provided - _ <- transactionRequestBodyCardano.to.amount match { - case Some(amount) => Helper.booleanToFuture(s"$InvalidJsonValue Cardano amount unit must be 'lovelace'", cc=callContext) { - amount.unit == "lovelace" - } - case None => Future.successful(true) + // Validate amount unit must be 'lovelace' + _ <- Helper.booleanToFuture(s"$InvalidJsonValue Cardano amount unit must be 'lovelace'", cc=callContext) { + transactionRequestBodyCardano.to.amount.unit == "lovelace" } // Validate assets if provided @@ -1401,10 +1397,10 @@ object LocalMappedConnectorInternal extends MdcLoggable { // Validate that if amount is 0, there must be assets (token-only transfer) _ <- (transactionRequestBodyCardano.to.amount, transactionRequestBodyCardano.to.assets) match { - case (Some(amount), Some(assets)) if amount.quantity == 0 => Helper.booleanToFuture(s"$InvalidJsonValue Cardano token-only transfer must have assets", cc=callContext) { + case (amount, Some(assets)) if amount.quantity == 0 => Helper.booleanToFuture(s"$InvalidJsonValue Cardano token-only transfer must have assets", cc=callContext) { assets.nonEmpty } - case (Some(amount), None) if amount.quantity == 0 => Helper.booleanToFuture(s"$InvalidJsonValue Cardano transfer with zero amount must include assets", cc=callContext) { + 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) diff --git a/obp-api/src/main/scala/code/bankconnectors/cardano/CardanoConnector_vJun2025.scala b/obp-api/src/main/scala/code/bankconnectors/cardano/CardanoConnector_vJun2025.scala index e8f2eec46..4572e34d3 100644 --- a/obp-api/src/main/scala/code/bankconnectors/cardano/CardanoConnector_vJun2025.scala +++ b/obp-api/src/main/scala/code/bankconnectors/cardano/CardanoConnector_vJun2025.scala @@ -117,18 +117,20 @@ trait CardanoConnector_vJun2025 extends Connector with MdcLoggable { /** * 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: TransactionRequestBodyCardanoJsonV510): String = { val address = transactionRequestBodyCardano.to.address - val amountJson = transactionRequestBodyCardano.to.amount match { - case Some(amount) => s""" - | "amount": { - | "quantity": ${amount.quantity}, - | "unit": "${amount.unit}" - | }""".stripMargin - case None => "" - } + + // 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 => { @@ -147,8 +149,15 @@ trait CardanoConnector_vJun2025 extends Connector with MdcLoggable { case _ => "" } + // Always include amount, optionally include assets + val jsonContent = if (assetsJson.isEmpty) { + s""" "address": "$address",$amountJson""" + } else { + s""" "address": "$address",$amountJson$assetsJson""" + } + s""" { - | "address": "$address",$amountJson$assetsJson + |$jsonContent | }""".stripMargin } From 26f12ce0ae6bbdeb223ddaa0ca968d0d51321f0f Mon Sep 17 00:00:00 2001 From: hongwei Date: Tue, 29 Jul 2025 13:08:51 +0200 Subject: [PATCH 12/28] feature/add support for Cardano transaction requests and enhance transaction request handling --- .../scala/code/api/v5_1_0/APIMethods510.scala | 6 +- .../MappedTransactionRequestProvider.scala | 51 +- .../CardanoTransactionRequestTest.scala | 439 ++++++++++++++++++ .../test/scala/code/setup/ServerSetup.scala | 6 +- 4 files changed, 468 insertions(+), 34 deletions(-) create mode 100644 obp-api/src/test/scala/code/api/v5_1_0/CardanoTransactionRequestTest.scala diff --git a/obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala b/obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala index 219e7433b..a404c8220 100644 --- a/obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala +++ b/obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala @@ -5319,7 +5319,7 @@ trait APIMethods510 { implementedInApiVersion, nameOf(createTransactionRequestCardano), "POST", - "/banks/cardano/accounts/ACCOUNT_ID/owner/transaction-request-types/CARDANO/transaction-requests", + "/banks/BANK_ID/accounts/ACCOUNT_ID/owner/transaction-request-types/CARDANO/transaction-requests", "Create Transaction Request (CARDANO)", s""" | @@ -5347,11 +5347,11 @@ trait APIMethods510 { ) lazy val createTransactionRequestCardano: OBPEndpoint = { - case "banks" :: "cardano" :: "accounts" :: AccountId(accountId) :: ViewId(viewId) :: "transaction-request-types" :: + 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("cardano"), accountId, viewId , transactionRequestType, json) + LocalMappedConnectorInternal.createTransactionRequest(bankId, accountId, viewId , transactionRequestType, json) } diff --git a/obp-api/src/main/scala/code/transactionrequests/MappedTransactionRequestProvider.scala b/obp-api/src/main/scala/code/transactionrequests/MappedTransactionRequestProvider.scala index b413c2ea1..9a763192c 100644 --- a/obp-api/src/main/scala/code/transactionrequests/MappedTransactionRequestProvider.scala +++ b/obp-api/src/main/scala/code/transactionrequests/MappedTransactionRequestProvider.scala @@ -1,25 +1,22 @@ package code.transactionrequests -import code.api.util.APIUtil.{DateWithMsFormat} -import code.api.util.{APIUtil, CallContext, CustomJsonFormats} +import code.api.util.APIUtil.DateWithMsFormat import code.api.util.ErrorMessages._ +import code.api.util.{APIUtil, CallContext, CustomJsonFormats} import code.api.v2_1_0.TransactionRequestBodyCounterpartyJSON import code.bankconnectors.LocalMappedConnectorInternal import code.consent.Consents import code.model._ import code.util.{AccountIdString, UUIDString} import com.openbankproject.commons.model._ -import com.openbankproject.commons.model.enums.{AccountRoutingScheme, TransactionRequestStatus} -import com.openbankproject.commons.model.enums.TransactionRequestTypes import com.openbankproject.commons.model.enums.TransactionRequestTypes.{COUNTERPARTY, SEPA} +import com.openbankproject.commons.model.enums.{AccountRoutingScheme, TransactionRequestStatus, TransactionRequestTypes} import net.liftweb.common.{Box, Failure, Full, Logger} import net.liftweb.json import net.liftweb.json.JsonAST.{JField, JObject, JString} import net.liftweb.mapper._ import net.liftweb.util.Helpers._ -import java.text.SimpleDateFormat - object MappedTransactionRequestProvider extends TransactionRequestProvider { private val logger = Logger(classOf[TransactionRequestProvider]) @@ -237,24 +234,24 @@ class MappedTransactionRequest extends LongKeyedMapper[MappedTransactionRequest] //transaction request fields: object mTransactionRequestId extends UUIDString(this) - object mType extends MappedString(this, 32) + object mType extends MappedString(this, 2000) //transaction fields: object mTransactionIDs extends MappedString(this, 2000) - object mStatus extends MappedString(this, 32) + object mStatus extends MappedString(this, 2000) object mStartDate extends MappedDate(this) object mEndDate extends MappedDate(this) - object mChallenge_Id extends MappedString(this, 64) + object mChallenge_Id extends MappedString(this, 2000) object mChallenge_AllowedAttempts extends MappedInt(this) - 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_Policy extends MappedString(this, 32) + object mChallenge_ChallengeType extends MappedString(this, 2000) + object mCharge_Summary extends MappedString(this, 2000) + object mCharge_Amount extends MappedString(this, 2000) + object mCharge_Currency extends MappedString(this, 2000) + object mcharge_Policy extends MappedString(this, 2000) //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_Amount extends MappedString(this, 32) + object mBody_Value_Currency extends MappedString(this, 2000) + object mBody_Value_Amount extends MappedString(this, 2000) object mBody_Description extends MappedString(this, 2000) // This is the details / body of the request (contains all fields in the body) // Note:this need to be a longer string, defaults is 2000, maybe not enough @@ -271,28 +268,28 @@ class MappedTransactionRequest extends LongKeyedMapper[MappedTransactionRequest] object mTo_AccountId extends AccountIdString(this) //toCounterparty fields - object mName extends MappedString(this, 64) + object mName extends MappedString(this, 2000) object mThisBankId extends UUIDString(this) object mThisAccountId extends AccountIdString(this) 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 mOtherBankRoutingScheme extends MappedString(this, 32) - object mOtherBankRoutingAddress extends MappedString(this, 64) + object mOtherAccountRoutingScheme extends MappedString(this, 2000) // TODO Add class for Scheme and Address + object mOtherAccountRoutingAddress extends MappedString(this, 2000) + object mOtherBankRoutingScheme extends MappedString(this, 2000) + object mOtherBankRoutingAddress extends MappedString(this, 2000) object mIsBeneficiary extends MappedBoolean(this) //Here are for Berlin Group V1.3 object mPaymentStartDate extends MappedDate(this) //BGv1.3 Open API Document example value: "startDate":"2024-08-12" object mPaymentEndDate extends MappedDate(this) //BGv1.3 Open API Document example value: "startDate":"2025-08-01" - object mPaymentExecutionRule extends MappedString(this, 64) //BGv1.3 Open API Document example value: "executionRule":"preceding" - object mPaymentFrequency extends MappedString(this, 64) //BGv1.3 Open API Document example value: "frequency":"Monthly", - object mPaymentDayOfExecution extends MappedString(this, 64)//BGv1.3 Open API Document example value: "dayOfExecution":"01" + object mPaymentExecutionRule extends MappedString(this, 2000) //BGv1.3 Open API Document example value: "executionRule":"preceding" + object mPaymentFrequency extends MappedString(this, 2000) //BGv1.3 Open API Document example value: "frequency":"Monthly", + object mPaymentDayOfExecution extends MappedString(this, 2000)//BGv1.3 Open API Document example value: "dayOfExecution":"01" - object mConsentReferenceId extends MappedString(this, 64) + object mConsentReferenceId extends MappedString(this, 2000) - object mApiStandard extends MappedString(this, 50) - object mApiVersion extends MappedString(this, 50) + object mApiStandard extends MappedString(this, 2000) + object mApiVersion extends MappedString(this, 2000) def updateStatus(newStatus: String) = { mStatus.set(newStatus) diff --git a/obp-api/src/test/scala/code/api/v5_1_0/CardanoTransactionRequestTest.scala b/obp-api/src/test/scala/code/api/v5_1_0/CardanoTransactionRequestTest.scala new file mode 100644 index 000000000..afc5b49e0 --- /dev/null +++ b/obp-api/src/test/scala/code/api/v5_1_0/CardanoTransactionRequestTest.scala @@ -0,0 +1,439 @@ +/** +Open Bank Project - API +Copyright (C) 2011-2019, TESOBE GmbH + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . + +Email: contact@tesobe.com +TESOBE GmbH +Osloerstrasse 16/17 +Berlin 13359, Germany + +This product includes software developed at +TESOBE (http://www.tesobe.com/) +*/ +package code.api.v5_1_0 + +import code.api.Constant +import code.api.ResourceDocs1_4_0.SwaggerDefinitionsJSON +import code.api.util.APIUtil.OAuth._ +import code.api.util.ApiRole +import code.api.util.ApiRole._ +import code.api.util.ErrorMessages._ +import code.api.v4_0_0.TransactionRequestWithChargeJSON400 +import code.api.v5_1_0.OBPAPI5_1_0.Implementations5_1_0 +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 + + +class CardanoTransactionRequestTest extends V510ServerSetup { + + /** + * 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.v5_1_0.toString) + object CreateTransactionRequestCardano extends Tag(nameOf(Implementations5_1_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 - v5.1.0") { + + scenario("We will create Cardano transaction request - user is NOT logged in", CreateTransactionRequestCardano, VersionOfApi) { + When("We make a request v5.1.0") + val request510 = (v5_1_0_Request / "banks" / testBankId / "accounts" / testAccountId / Constant.SYSTEM_OWNER_VIEW_ID / "transaction-request-types" / "CARDANO" / "transaction-requests").POST + val cardanoTransactionRequestBody = TransactionRequestBodyCardanoJsonV510( + to = CardanoPaymentJsonV510( + address = "addr_test1qpv3se9ghq87ud29l0a8asy8nlqwd765e5zt4rc2z4mktqulwagn832cuzcjknfyxwzxz2p2kumx6n58tskugny6mrqs7fd23z", + amount = CardanoAmountJsonV510( + quantity = 1000000, + unit = "lovelace" + ) + ), + value = AmountOfMoneyJsonV121("lovelace", "1000000"), + passphrase = passphrase, + description = "Basic ADA transfer" + ) + val response510 = makePostRequest(request510, write(cardanoTransactionRequestBody)) + Then("We should get a 401") + response510.code should equal(401) + And("error should be " + UserNotLoggedIn) + response510.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 = (v5_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 = (v5_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 v5.1.0") + val request510 = (v5_1_0_Request / "banks" / testBankId / "accounts" / testAccountId / Constant.SYSTEM_OWNER_VIEW_ID / "transaction-request-types" / "CARDANO" / "transaction-requests").POST <@(user1) + val cardanoTransactionRequestBody = TransactionRequestBodyCardanoJsonV510( + to = CardanoPaymentJsonV510( + address = "addr_test1qpv3se9ghq87ud29l0a8asy8nlqwd765e5zt4rc2z4mktqulwagn832cuzcjknfyxwzxz2p2kumx6n58tskugny6mrqs7fd23z", + amount = CardanoAmountJsonV510( + quantity = 1000000, + unit = "lovelace" + ) + ), + value = AmountOfMoneyJsonV121("lovelace", "1000000"), + passphrase = passphrase, + description = "Basic ADA transfer" + ) + val response510 = makePostRequest(request510, write(cardanoTransactionRequestBody)) + Then("We should get a 201") + response510.code should equal(201) + And("response should contain transaction request") + val transactionRequest = response510.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 = (v5_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 = (v5_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 v5.1.0 with metadata") + val request510 = (v5_1_0_Request / "banks" / testBankId / "accounts" / testAccountId / Constant.SYSTEM_OWNER_VIEW_ID / "transaction-request-types" / "CARDANO" / "transaction-requests").POST <@(user1) + val cardanoTransactionRequestBody = TransactionRequestBodyCardanoJsonV510( + to = CardanoPaymentJsonV510( + address = "addr_test1qpv3se9ghq87ud29l0a8asy8nlqwd765e5zt4rc2z4mktqulwagn832cuzcjknfyxwzxz2p2kumx6n58tskugny6mrqs7fd23z", + amount = CardanoAmountJsonV510( + quantity = 1000000, + unit = "lovelace" + ) + ), + value = AmountOfMoneyJsonV121("lovelace", "1000000"), + passphrase = passphrase, + description = "ADA transfer with metadata", + metadata = Some(Map("202507022319" -> CardanoMetadataStringJsonV510("Hello Cardano"))) + ) + val response510 = makePostRequest(request510, write(cardanoTransactionRequestBody)) + Then("We should get a 201") + response510.code should equal(201) + And("response should contain transaction request") + val transactionRequest = response510.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 = (v5_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 = (v5_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 v5.1.0 with token") + val request510 = (v5_1_0_Request / "banks" / testBankId / "accounts" / testAccountId / Constant.SYSTEM_OWNER_VIEW_ID / "transaction-request-types" / "CARDANO" / "transaction-requests").POST <@(user1) + val cardanoTransactionRequestBody = TransactionRequestBodyCardanoJsonV510( + to = CardanoPaymentJsonV510( + address = "addr_test1qpv3se9ghq87ud29l0a8asy8nlqwd765e5zt4rc2z4mktqulwagn832cuzcjknfyxwzxz2p2kumx6n58tskugny6mrqs7fd23z", + amount = CardanoAmountJsonV510( + quantity = 1000000, + unit = "lovelace" + ), + assets = Some(List(CardanoAssetJsonV510( + policy_id = "policy1234567890abcdef", + asset_name = "4f47435241", + quantity = 10 + ))) + ), + value = AmountOfMoneyJsonV121("lovelace", "1000000"), + passphrase = passphrase, + description = "Token-only transfer" + ) + val response510 = makePostRequest(request510, write(cardanoTransactionRequestBody)) + Then("We should get a 201") + response510.code should equal(201) + And("response should contain transaction request") + val transactionRequest = response510.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 v5.1.0 with token and metadata") + val request510 = (v5_1_0_Request / "banks" / testBankId / "accounts" / testAccountId / Constant.SYSTEM_OWNER_VIEW_ID / "transaction-request-types" / "CARDANO" / "transaction-requests").POST <@(user1) + val cardanoTransactionRequestBody = TransactionRequestBodyCardanoJsonV510( + to = CardanoPaymentJsonV510( + address = "addr_test1qpv3se9ghq87ud29l0a8asy8nlqwd765e5zt4rc2z4mktqulwagn832cuzcjknfyxwzxz2p2kumx6n58tskugny6mrqs7fd23z", + amount = CardanoAmountJsonV510( + quantity = 5000000, + unit = "lovelace" + ), + assets = Some(List(CardanoAssetJsonV510( + policy_id = "policy1234567890abcdef", + asset_name = "4f47435241", + quantity = 10 + ))) + ), + value = AmountOfMoneyJsonV121("lovelace", "1000000"), + passphrase = passphrase, + description = "ADA transfer with token and metadata", + metadata = Some(Map("202507022319" -> CardanoMetadataStringJsonV510("Hello Cardano with Token"))) + ) + val response510 = makePostRequest(request510, write(cardanoTransactionRequestBody)) + Then("We should get a 201") + response510.code should equal(201) + And("response should contain transaction request") + val transactionRequest = response510.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 v5.1.0") + val request510 = (v5_1_0_Request / "banks" / testBankId / "accounts" / testAccountId / Constant.SYSTEM_OWNER_VIEW_ID / "transaction-request-types" / "CARDANO" / "transaction-requests").POST <@(user2) + val cardanoTransactionRequestBody = TransactionRequestBodyCardanoJsonV510( + to = CardanoPaymentJsonV510( + address = "addr_test1qpv3se9ghq87ud29l0a8asy8nlqwd765e5zt4rc2z4mktqulwagn832cuzcjknfyxwzxz2p2kumx6n58tskugny6mrqs7fd23z", + amount = CardanoAmountJsonV510( + quantity = 1000000, + unit = "lovelace" + ) + ), + value = AmountOfMoneyJsonV121("lovelace", "1000000"), + passphrase = passphrase, + description = "Basic ADA transfer" + ) + val response510 = makePostRequest(request510, write(cardanoTransactionRequestBody)) + Then("We should get a 403") + response510.code should equal(403) + And("error should be " + UserNoPermissionAccessView) + response510.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 v5.1.0 with invalid address") + val request510 = (v5_1_0_Request / "banks" / testBankId / "accounts" / testAccountId / Constant.SYSTEM_OWNER_VIEW_ID / "transaction-request-types" / "CARDANO" / "transaction-requests").POST <@(user1) + val cardanoTransactionRequestBody = TransactionRequestBodyCardanoJsonV510( + to = CardanoPaymentJsonV510( + address = "invalid_address_format", + amount = CardanoAmountJsonV510( + quantity = 1000000, + unit = "lovelace" + ) + ), + value = AmountOfMoneyJsonV121("lovelace", "1000000"), + passphrase = passphrase, + description = "Basic ADA transfer" + ) + val response510 = makePostRequest(request510, write(cardanoTransactionRequestBody)) + Then("We should get a 400") + response510.code should equal(400) + And("error should contain invalid address message") + response510.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 v5.1.0 with missing amount") + val request510 = (v5_1_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 response510 = makePostRequest(request510, invalidJson) + Then("We should get a 400") + response510.code should equal(400) + And("error should contain invalid json format message") + response510.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 v5.1.0 with negative amount") + val request510 = (v5_1_0_Request / "banks" / testBankId / "accounts" / testAccountId / Constant.SYSTEM_OWNER_VIEW_ID / "transaction-request-types" / "CARDANO" / "transaction-requests").POST <@(user1) + val cardanoTransactionRequestBody = TransactionRequestBodyCardanoJsonV510( + to = CardanoPaymentJsonV510( + address = "addr_test1qpv3se9ghq87ud29l0a8asy8nlqwd765e5zt4rc2z4mktqulwagn832cuzcjknfyxwzxz2p2kumx6n58tskugny6mrqs7fd23z", + amount = CardanoAmountJsonV510( + quantity = -1000000, + unit = "lovelace" + ) + ), + value = AmountOfMoneyJsonV121("lovelace", "1000000"), + passphrase = passphrase, + description = "Basic ADA transfer" + ) + val response510 = makePostRequest(request510, write(cardanoTransactionRequestBody)) + Then("We should get a 400") + response510.code should equal(400) + And("error should contain invalid amount message") + response510.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 v5.1.0 with invalid amount unit") + val request510 = (v5_1_0_Request / "banks" / testBankId / "accounts" / testAccountId / Constant.SYSTEM_OWNER_VIEW_ID / "transaction-request-types" / "CARDANO" / "transaction-requests").POST <@(user1) + val cardanoTransactionRequestBody = TransactionRequestBodyCardanoJsonV510( + to = CardanoPaymentJsonV510( + address = "addr_test1qpv3se9ghq87ud29l0a8asy8nlqwd765e5zt4rc2z4mktqulwagn832cuzcjknfyxwzxz2p2kumx6n58tskugny6mrqs7fd23z", + amount = CardanoAmountJsonV510( + quantity = 1000000, + unit = "abc" // Invalid unit, should be "lovelace" + ) + ), + value = AmountOfMoneyJsonV121("lovelace", "1000000"), + passphrase = passphrase, + description = "Basic ADA transfer" + ) + val response510 = makePostRequest(request510, write(cardanoTransactionRequestBody)) + Then("We should get a 400") + response510.code should equal(400) + And("error should contain invalid unit message") + response510.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 v5.1.0 with zero amount but no assets") + val request510 = (v5_1_0_Request / "banks" / testBankId / "accounts" / testAccountId / Constant.SYSTEM_OWNER_VIEW_ID / "transaction-request-types" / "CARDANO" / "transaction-requests").POST <@(user1) + val cardanoTransactionRequestBody = TransactionRequestBodyCardanoJsonV510( + to = CardanoPaymentJsonV510( + address = "addr_test1qpv3se9ghq87ud29l0a8asy8nlqwd765e5zt4rc2z4mktqulwagn832cuzcjknfyxwzxz2p2kumx6n58tskugny6mrqs7fd23z", + amount = CardanoAmountJsonV510( + quantity = 0, + unit = "lovelace" + ) + ), + value = AmountOfMoneyJsonV121("lovelace", "0.0"), + passphrase = passphrase, + description = "Zero amount without assets" + ) + val response510 = makePostRequest(request510, write(cardanoTransactionRequestBody)) + Then("We should get a 400") + response510.code should equal(400) + And("error should contain invalid amount message") + response510.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 v5.1.0 with invalid assets") + val request510 = (v5_1_0_Request / "banks" / testBankId / "accounts" / testAccountId / Constant.SYSTEM_OWNER_VIEW_ID / "transaction-request-types" / "CARDANO" / "transaction-requests").POST <@(user1) + val cardanoTransactionRequestBody = TransactionRequestBodyCardanoJsonV510( + to = CardanoPaymentJsonV510( + address = "addr_test1qpv3se9ghq87ud29l0a8asy8nlqwd765e5zt4rc2z4mktqulwagn832cuzcjknfyxwzxz2p2kumx6n58tskugny6mrqs7fd23z", + amount = CardanoAmountJsonV510( + quantity = 0, + unit = "lovelace" + ), + assets = Some(List(CardanoAssetJsonV510( + policy_id = "", + asset_name = "", + quantity = 0 + ))) + ), + value = AmountOfMoneyJsonV121("lovelace", "0.0"), + passphrase = passphrase, + description = "Invalid assets" + ) + val response510 = makePostRequest(request510, write(cardanoTransactionRequestBody)) + Then("We should get a 400") + response510.code should equal(400) + And("error should contain invalid assets message") + response510.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 v5.1.0 with invalid metadata") + val request510 = (v5_1_0_Request / "banks" / testBankId / "accounts" / testAccountId / Constant.SYSTEM_OWNER_VIEW_ID / "transaction-request-types" / "CARDANO" / "transaction-requests").POST <@(user1) + val cardanoTransactionRequestBody = TransactionRequestBodyCardanoJsonV510( + to = CardanoPaymentJsonV510( + address = "addr_test1qpv3se9ghq87ud29l0a8asy8nlqwd765e5zt4rc2z4mktqulwagn832cuzcjknfyxwzxz2p2kumx6n58tskugny6mrqs7fd23z", + amount = CardanoAmountJsonV510( + quantity = 1000000, + unit = "lovelace" + ) + ), + value = AmountOfMoneyJsonV121("lovelace", "1000000"), + passphrase = passphrase, + description = "ADA transfer with invalid metadata", + metadata = Some(Map("" -> CardanoMetadataStringJsonV510(""))) + ) + val response510 = makePostRequest(request510, write(cardanoTransactionRequestBody)) + Then("We should get a 400") + response510.code should equal(400) + And("error should contain invalid metadata message") + response510.body.extract[ErrorMessage].message should include("Cardano metadata must have valid structure") + } + } +} \ No newline at end of file diff --git a/obp-api/src/test/scala/code/setup/ServerSetup.scala b/obp-api/src/test/scala/code/setup/ServerSetup.scala index 31ff36032..176ccfcd2 100644 --- a/obp-api/src/test/scala/code/setup/ServerSetup.scala +++ b/obp-api/src/test/scala/code/setup/ServerSetup.scala @@ -27,8 +27,6 @@ TESOBE (http://www.tesobe.com/) package code.setup -import java.net.URI - import _root_.net.liftweb.json.JsonAST.JObject import code.TestServer import code.api.util.APIUtil._ @@ -51,11 +49,11 @@ trait ServerSetup extends FeatureSpec with SendServerRequests setPropsValues("dauth.host" -> "127.0.0.1") setPropsValues("jwt_token_secret"->"your-at-least-256-bit-secret-token") setPropsValues("jwt.public_key_rsa" -> "src/test/resources/cert/public_dauth.pem") - setPropsValues("transactionRequests_supported_types" -> "SEPA,SANDBOX_TAN,FREE_FORM,COUNTERPARTY,ACCOUNT,ACCOUNT_OTP,SIMPLE,CARD,AGENT_CASH_WITHDRAWAL") + setPropsValues("transactionRequests_supported_types" -> "SEPA,SANDBOX_TAN,FREE_FORM,COUNTERPARTY,ACCOUNT,ACCOUNT_OTP,SIMPLE,CARD,AGENT_CASH_WITHDRAWAL,CARDANO") setPropsValues("CARD_OTP_INSTRUCTION_TRANSPORT" -> "DUMMY") setPropsValues("AGENT_CASH_WITHDRAWAL_OTP_INSTRUCTION_TRANSPORT" -> "DUMMY") setPropsValues("api_instance_id" -> "1_final") - setPropsValues("starConnector_supported_types" -> "mapped,internal") + setPropsValues("starConnector_supported_types" -> "mapped,internal,cardano_vJun2025") setPropsValues("connector" -> "star") // Berlin Group From dd6170451fcadd556eb7bedb1726cf173a890f2f Mon Sep 17 00:00:00 2001 From: Hongwei Date: Fri, 1 Aug 2025 09:12:56 +0200 Subject: [PATCH 13/28] refactor/Update Jakarta Activation dependency for Java 11 compatibility --- obp-api/pom.xml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/obp-api/pom.xml b/obp-api/pom.xml index 1227da21a..eaae7b12e 100644 --- a/obp-api/pom.xml +++ b/obp-api/pom.xml @@ -498,10 +498,11 @@ test + - com.sun.activation - jakarta.activation - 1.2.2 + javax.activation + activation + 1.1.1 From ec71be2920801d76942f0f7a75bcab6660571892 Mon Sep 17 00:00:00 2001 From: Hongwei Date: Fri, 1 Aug 2025 15:47:52 +0200 Subject: [PATCH 14/28] refactor/Update Jakarta Activation and add Jakarta Mail dependencies for improved compatibility --- obp-api/pom.xml | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/obp-api/pom.xml b/obp-api/pom.xml index eaae7b12e..5705edd40 100644 --- a/obp-api/pom.xml +++ b/obp-api/pom.xml @@ -498,11 +498,16 @@ test - - javax.activation - activation - 1.1.1 + com.sun.activation + javax.activation + 1.2.0 + + + + com.sun.mail + jakarta.mail + 1.6.7 From c67a0baece790fa1ce496cfbe5064464d0c2ab36 Mon Sep 17 00:00:00 2001 From: hongwei Date: Mon, 18 Aug 2025 16:25:55 +0200 Subject: [PATCH 15/28] refactor/improve error handling in LocalMappedConnector for account number validation --- .../scala/code/bankconnectors/LocalMappedConnector.scala | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/obp-api/src/main/scala/code/bankconnectors/LocalMappedConnector.scala b/obp-api/src/main/scala/code/bankconnectors/LocalMappedConnector.scala index 4446d8b78..8654f0ccd 100644 --- a/obp-api/src/main/scala/code/bankconnectors/LocalMappedConnector.scala +++ b/obp-api/src/main/scala/code/bankconnectors/LocalMappedConnector.scala @@ -1028,12 +1028,12 @@ object LocalMappedConnector extends Connector with MdcLoggable { else s"$AccountNumberNotUniqueError, current BankId is ${bankId.head.value}, AccountNumber is $accountNumber" - if(bankAccounts.length > 1){ + if(bankAccounts.length > 1){ // If the account number is not unique, return the error message (Failure(errorMessage), callContext) - }else if (bankAccounts.length == 1){ + }else if (bankAccounts.length == 1){ // If the account number is unique, return the account (Full(bankAccounts.head), callContext) - }else{ - (Failure(errorMessage), callContext) + }else{ // If the account number is not found, return the error message + (Failure(s"$InvalidAccountNumber, current AccountNumber is $accountNumber"), callContext) } } From a893eb0627f97539e2e7c95fab8ce4b2f83c39a1 Mon Sep 17 00:00:00 2001 From: hongwei Date: Wed, 20 Aug 2025 10:20:06 +0200 Subject: [PATCH 16/28] refactor/update .gitignore to include additional project-specific files and directories --- .gitignore | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.gitignore b/.gitignore index 35831f408..c4210993b 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,9 @@ obp-api/src/main/scala/code/api/v3_0_0/custom/ /obp-api2/ /.java-version .scannerwork +.bloop +.bsp +.specstory +project/project +coursier +*.code-workspace \ No newline at end of file From 7f504008e635e4f3cfcfdf33da93e846304686fc Mon Sep 17 00:00:00 2001 From: hongwei Date: Wed, 20 Aug 2025 18:44:27 +0200 Subject: [PATCH 17/28] refactor/ update dependencies in pom.xml and clean up NewStyle.scala --- obp-api/pom.xml | 10 ++-------- obp-api/src/main/scala/code/api/util/NewStyle.scala | 2 -- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/obp-api/pom.xml b/obp-api/pom.xml index feb7d2054..9955f4ad7 100644 --- a/obp-api/pom.xml +++ b/obp-api/pom.xml @@ -531,14 +531,8 @@ com.sun.activation - javax.activation - 1.2.0 - - - - com.sun.mail - jakarta.mail - 1.6.7 + jakarta.activation + 1.2.2 diff --git a/obp-api/src/main/scala/code/api/util/NewStyle.scala b/obp-api/src/main/scala/code/api/util/NewStyle.scala index 216cee6e5..8a297199c 100644 --- a/obp-api/src/main/scala/code/api/util/NewStyle.scala +++ b/obp-api/src/main/scala/code/api/util/NewStyle.scala @@ -802,12 +802,10 @@ object NewStyle extends MdcLoggable{ Failure(s"$failMsg. Details: ${e.getMessage}", Full(e), Empty) } } map { - // Convert `Box[T]` to `Future[T]` using `unboxFullOrFail` x => unboxFullOrFail(x, callContext, failMsg, failCode) } } - def extractHttpParamsFromUrl(url: String): Future[List[HTTPParam]] = { createHttpParamsByUrlFuture(url) map { unboxFull(_) } } From bdb3f7c48b423e6007f0462a84e39629d72c6472 Mon Sep 17 00:00:00 2001 From: hongwei Date: Thu, 21 Aug 2025 13:11:17 +0200 Subject: [PATCH 18/28] refactor/ convert asset_name to hex format in CardanoConnector_vJun2025 --- .../cardano/CardanoConnector_vJun2025.scala | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/obp-api/src/main/scala/code/bankconnectors/cardano/CardanoConnector_vJun2025.scala b/obp-api/src/main/scala/code/bankconnectors/cardano/CardanoConnector_vJun2025.scala index 4572e34d3..a02d7cb21 100644 --- a/obp-api/src/main/scala/code/bankconnectors/cardano/CardanoConnector_vJun2025.scala +++ b/obp-api/src/main/scala/code/bankconnectors/cardano/CardanoConnector_vJun2025.scala @@ -135,9 +135,15 @@ trait CardanoConnector_vJun2025 extends Connector with MdcLoggable { 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": "${asset.asset_name}", + | "asset_name": "$hexAssetName", | "quantity": ${asset.quantity} | }""".stripMargin }.mkString(",\n") From 7c731506eccbc29a49a69e26d737287b4b4d71dd Mon Sep 17 00:00:00 2001 From: hongwei Date: Thu, 21 Aug 2025 13:11:17 +0200 Subject: [PATCH 19/28] refactor/ convert asset_name to hex format in CardanoConnector_vJun2025 --- .../cardano/CardanoConnector_vJun2025.scala | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/obp-api/src/main/scala/code/bankconnectors/cardano/CardanoConnector_vJun2025.scala b/obp-api/src/main/scala/code/bankconnectors/cardano/CardanoConnector_vJun2025.scala index 4572e34d3..afa71341b 100644 --- a/obp-api/src/main/scala/code/bankconnectors/cardano/CardanoConnector_vJun2025.scala +++ b/obp-api/src/main/scala/code/bankconnectors/cardano/CardanoConnector_vJun2025.scala @@ -135,9 +135,15 @@ trait CardanoConnector_vJun2025 extends Connector with MdcLoggable { 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": "${asset.asset_name}", + | "asset_name": "$hexAssetName", | "quantity": ${asset.quantity} | }""".stripMargin }.mkString(",\n") From 73d7cd3ab8c213d67a179c70901b8142be30a9cb Mon Sep 17 00:00:00 2001 From: hongwei Date: Thu, 21 Aug 2025 14:40:55 +0200 Subject: [PATCH 20/28] feature/ introduce v6.0.0 API version with Cardano transaction request enhancements and new JSON structures - Added new API version v6.0.0 with updated Cardano transaction request handling. - Introduced new JSON case classes for Cardano payment and transaction request bodies. - Updated existing references from v5.1.0 to v6.0.0 in relevant files. - Implemented tests for Cardano transaction requests, including various scenarios for validation and error handling. --- .../SwaggerDefinitionsJSON.scala | 21 +-- .../scala/code/api/v5_1_0/APIMethods510.scala | 45 +------ .../code/api/v5_1_0/JSONFactory5.1.0.scala | 31 +---- .../scala/code/api/v6_0_0/APIMethods600.scala | 83 ++++++++++++ .../code/api/v6_0_0/JSONFactory6.0.0.scala | 64 +++++++++ .../scala/code/api/v6_0_0/OBPAPI6_0_0.scala | 122 ++++++++++++++++++ .../LocalMappedConnectorInternal.scala | 8 +- .../cardano/CardanoConnector_vJun2025.scala | 10 +- .../CardanoTransactionRequestTest.scala | 118 ++++++++--------- .../code/api/v6_0_0/V600ServerSetup.scala | 13 ++ .../commons/util/ApiVersion.scala | 6 +- 11 files changed, 367 insertions(+), 154 deletions(-) create mode 100644 obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala create mode 100644 obp-api/src/main/scala/code/api/v6_0_0/JSONFactory6.0.0.scala create mode 100644 obp-api/src/main/scala/code/api/v6_0_0/OBPAPI6_0_0.scala rename obp-api/src/test/scala/code/api/{v5_1_0 => v6_0_0}/CardanoTransactionRequestTest.scala (89%) create mode 100644 obp-api/src/test/scala/code/api/v6_0_0/V600ServerSetup.scala diff --git a/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala b/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala index 8ac31f46d..34c42dd30 100644 --- a/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala +++ b/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala @@ -16,6 +16,7 @@ import code.api.v3_1_0._ import code.api.v4_0_0._ import code.api.v5_0_0._ import code.api.v5_1_0._ +import code.api.v6_0_0._ import code.branches.Branches.{Branch, DriveUpString, LobbyString} import code.connectormethod.{JsonConnectorMethod, JsonConnectorMethodMethodBody} import code.consent.ConsentStatus @@ -5699,13 +5700,13 @@ object SwaggerDefinitionsJSON { ) - lazy val cardanoPaymentJsonV510 = CardanoPaymentJsonV510( + lazy val cardanoPaymentJsonV600 = CardanoPaymentJsonV600( address = "addr_test1qpv3se9ghq87ud29l0a8asy8nlqwd765e5zt4rc2z4mktqulwagn832cuzcjknfyxwzxz2p2kumx6n58tskugny6mrqs7fd12", - amount = CardanoAmountJsonV510( + amount = CardanoAmountJsonV600( quantity = 1000000, unit = "lovelace" ), - assets = Some(List(CardanoAssetJsonV510( + assets = Some(List(CardanoAssetJsonV600( policy_id = "policy1234567890abcdef", asset_name = "4f47435241", quantity = 10 @@ -5713,29 +5714,29 @@ object SwaggerDefinitionsJSON { ) // Example for Send ADA with Token only (no ADA amount) - lazy val cardanoPaymentTokenOnlyJsonV510 = CardanoPaymentJsonV510( + lazy val cardanoPaymentTokenOnlyJsonV510 = CardanoPaymentJsonV600( address = "addr_test1qpv3se9ghq87ud29l0a8asy8nlqwd765e5zt4rc2z4mktqulwagn832cuzcjknfyxwzxz2p2kumx6n58tskugny6mrqs7fd12", - amount = CardanoAmountJsonV510( + amount = CardanoAmountJsonV600( quantity = 0, unit = "lovelace" ), - assets = Some(List(CardanoAssetJsonV510( + assets = Some(List(CardanoAssetJsonV600( policy_id = "policy1234567890abcdef", asset_name = "4f47435241", quantity = 10 ))) ) - lazy val cardanoMetadataStringJsonV510 = CardanoMetadataStringJsonV510( + lazy val cardanoMetadataStringJsonV600 = CardanoMetadataStringJsonV600( string = "Hello Cardano" ) - lazy val transactionRequestBodyCardanoJsonV510 = TransactionRequestBodyCardanoJsonV510( - to = cardanoPaymentJsonV510, + lazy val transactionRequestBodyCardanoJsonV600 = TransactionRequestBodyCardanoJsonV600( + to = cardanoPaymentJsonV600, value = amountOfMoneyJsonV121, passphrase = "password1234!", description = descriptionExample.value, - metadata = Some(Map("202507022319" -> cardanoMetadataStringJsonV510)) + metadata = Some(Map("202507022319" -> cardanoMetadataStringJsonV600)) ) //The common error or success format. diff --git a/obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala b/obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala index 711ad6643..1a327e81e 100644 --- a/obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala +++ b/obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala @@ -30,8 +30,7 @@ import code.api.v4_0_0._ import code.api.v5_0_0.JSONFactory500 import code.api.v5_1_0.JSONFactory510.{createConsentsInfoJsonV510, createConsentsJsonV510, createRegulatedEntitiesJson, createRegulatedEntityJson} import code.atmattribute.AtmAttribute -import code.bankconnectors.LocalMappedConnectorInternal._ -import code.bankconnectors.{Connector, LocalMappedConnectorInternal} +import code.bankconnectors.Connector import code.consent.{ConsentRequests, ConsentStatus, Consents, MappedConsent} import code.consumer.Consumers import code.entitlement.Entitlement @@ -5317,48 +5316,6 @@ trait APIMethods510 { } yield (true, HttpCode.`204`(cc.callContext)) } } - - 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, - transactionRequestBodyCardanoJsonV510, - 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) - } - - } } diff --git a/obp-api/src/main/scala/code/api/v5_1_0/JSONFactory5.1.0.scala b/obp-api/src/main/scala/code/api/v5_1_0/JSONFactory5.1.0.scala index b13618de7..0d15f3cd8 100644 --- a/obp-api/src/main/scala/code/api/v5_1_0/JSONFactory5.1.0.scala +++ b/obp-api/src/main/scala/code/api/v5_1_0/JSONFactory5.1.0.scala @@ -52,7 +52,7 @@ import com.openbankproject.commons.model._ import com.openbankproject.commons.util.ApiVersion import net.liftweb.common.{Box, Full} import net.liftweb.json -import net.liftweb.json.{JString, JValue, MappingException, parse, parseOpt} +import net.liftweb.json.{Meta, _} import java.text.SimpleDateFormat import java.util.Date @@ -580,35 +580,6 @@ case class ConsentRequestToAccountJson( limit: PostCounterpartyLimitV510 ) -case class CardanoPaymentJsonV510( - address: String, - amount: CardanoAmountJsonV510, - assets: Option[List[CardanoAssetJsonV510]] = None -) - -case class CardanoAmountJsonV510( - quantity: Long, - unit: String // "lovelace" -) - -case class CardanoAssetJsonV510( - policy_id: String, - asset_name: String, - quantity: Long -) - -case class CardanoMetadataStringJsonV510( - string: String -) - -case class TransactionRequestBodyCardanoJsonV510( - to: CardanoPaymentJsonV510, - value: AmountOfMoneyJsonV121, - passphrase: String, - description: String, - metadata: Option[Map[String, CardanoMetadataStringJsonV510]] = None -) extends TransactionRequestCommonBodyJSON - case class CreateViewPermissionJson( permission_name: String, extra_data: Option[List[String]] diff --git a/obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala b/obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala new file mode 100644 index 000000000..57e4b824c --- /dev/null +++ b/obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala @@ -0,0 +1,83 @@ +package code.api.v6_0_0 + +import code.api.ResourceDocs1_4_0.SwaggerDefinitionsJSON._ +import code.api.util.APIUtil._ +import code.api.util.ApiTag._ +import code.api.util.ErrorMessages.{$UserNotLoggedIn, InvalidJsonFormat, UnknownError, _} +import code.api.util.FutureUtil.EndpointContext +import code.bankconnectors.LocalMappedConnectorInternal +import code.bankconnectors.LocalMappedConnectorInternal._ +import com.github.dwickern.macros.NameOf.nameOf +import com.openbankproject.commons.model._ +import com.openbankproject.commons.util.{ApiVersion, ScannedApiVersion} +import net.liftweb.http.rest.RestHelper + +import scala.collection.immutable.{List, Nil} +import scala.collection.mutable.ArrayBuffer + +trait APIMethods600 { + self: RestHelper => + + val Implementations6_0_0 = new Implementations600() + + class Implementations600 { + + val implementedInApiVersion: ScannedApiVersion = ApiVersion.v6_0_0 + + private val staticResourceDocs = ArrayBuffer[ResourceDoc]() + def resourceDocs = staticResourceDocs + + val apiRelations = ArrayBuffer[ApiRelation]() + val codeContext = CodeContext(staticResourceDocs, apiRelations) + + staticResourceDocs += ResourceDoc( + createTransactionRequestCardano, + implementedInApiVersion, + nameOf(createTransactionRequestCardano), + "POST", + "/banks/BANK_ID/accounts/ACCOUNT_ID/owner/transaction-request-types/CARDANO/transaction-requests", + "Create Transaction Request (CARDANO)", + s""" + | + |For sandbox mode, it will use the Cardano Preprod Network. + |The accountId can be the wallet_id for now, as it uses cardano-wallet in the backend. + | + |${transactionRequestGeneralText} + | + """.stripMargin, + transactionRequestBodyCardanoJsonV600, + transactionRequestWithChargeJSON400, + List( + $UserNotLoggedIn, + $BankNotFound, + $BankAccountNotFound, + InsufficientAuthorisationToCreateTransactionRequest, + InvalidTransactionRequestType, + InvalidJsonFormat, + NotPositiveAmount, + InvalidTransactionRequestCurrency, + TransactionDisabled, + UnknownError + ), + List(apiTagTransactionRequest, apiTagPSD2PIS, apiTagPsd2) + ) + + lazy val createTransactionRequestCardano: OBPEndpoint = { + case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: ViewId(viewId) :: "transaction-request-types" :: + "CARDANO" :: "transaction-requests" :: Nil JsonPost json -> _ => + cc => implicit val ec = EndpointContext(Some(cc)) + val transactionRequestType = TransactionRequestType("CARDANO") + LocalMappedConnectorInternal.createTransactionRequest(bankId, accountId, viewId , transactionRequestType, json) + } + + } +} + + + +object APIMethods600 extends RestHelper with APIMethods600 { + lazy val newStyleEndpoints: List[(String, String)] = Implementations6_0_0.resourceDocs.map { + rd => (rd.partialFunctionName, rd.implementedInApiVersion.toString()) + }.toList +} + diff --git a/obp-api/src/main/scala/code/api/v6_0_0/JSONFactory6.0.0.scala b/obp-api/src/main/scala/code/api/v6_0_0/JSONFactory6.0.0.scala new file mode 100644 index 000000000..7f4dd441d --- /dev/null +++ b/obp-api/src/main/scala/code/api/v6_0_0/JSONFactory6.0.0.scala @@ -0,0 +1,64 @@ +/** + * Open Bank Project - API + * Copyright (C) 2011-2019, TESOBE GmbH + * * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * * + * Email: contact@tesobe.com + * TESOBE GmbH + * Osloerstrasse 16/17 + * Berlin 13359, Germany + * * + * This product includes software developed at + * TESOBE (http://www.tesobe.com/) + * + */ +package code.api.v6_0_0 + +import code.api.util._ +import code.util.Helper.MdcLoggable +import com.openbankproject.commons.model._ + +case class CardanoPaymentJsonV600( + address: String, + amount: CardanoAmountJsonV600, + assets: Option[List[CardanoAssetJsonV600]] = None +) + +case class CardanoAmountJsonV600( + quantity: Long, + unit: String // "lovelace" +) + +case class CardanoAssetJsonV600( + policy_id: String, + asset_name: String, + quantity: Long +) + +case class CardanoMetadataStringJsonV600( + string: String +) + +case class TransactionRequestBodyCardanoJsonV600( + to: CardanoPaymentJsonV600, + value: AmountOfMoneyJsonV121, + passphrase: String, + description: String, + metadata: Option[Map[String, CardanoMetadataStringJsonV600]] = None +) extends TransactionRequestCommonBodyJSON + +object JSONFactory600 extends CustomJsonFormats with MdcLoggable{ + +} \ No newline at end of file diff --git a/obp-api/src/main/scala/code/api/v6_0_0/OBPAPI6_0_0.scala b/obp-api/src/main/scala/code/api/v6_0_0/OBPAPI6_0_0.scala new file mode 100644 index 000000000..f5b7f6b43 --- /dev/null +++ b/obp-api/src/main/scala/code/api/v6_0_0/OBPAPI6_0_0.scala @@ -0,0 +1,122 @@ +/** +Open Bank Project - API +Copyright (C) 2011-2019, TESOBE GmbH. + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . + +Email: contact@tesobe.com +TESOBE GmbH. +Osloer Strasse 16/17 +Berlin 13359, Germany + +This product includes software developed at +TESOBE (http://www.tesobe.com/) + + */ +package code.api.v6_0_0 + +import code.api.OBPRestHelper +import code.api.util.APIUtil.{OBPEndpoint, getAllowedEndpoints} +import code.api.util.VersionedOBPApis +import code.api.v1_3_0.APIMethods130 +import code.api.v1_4_0.APIMethods140 +import code.api.v2_0_0.APIMethods200 +import code.api.v2_1_0.APIMethods210 +import code.api.v2_2_0.APIMethods220 +import code.api.v3_0_0.APIMethods300 +import code.api.v3_0_0.custom.CustomAPIMethods300 +import code.api.v3_1_0.APIMethods310 +import code.api.v4_0_0.APIMethods400 +import code.api.v5_0_0.APIMethods500 +import code.api.v5_1_0.{APIMethods510, OBPAPI5_1_0} +import code.util.Helper.MdcLoggable +import com.github.dwickern.macros.NameOf.nameOf +import com.openbankproject.commons.util.{ApiVersion, ApiVersionStatus} +import net.liftweb.common.{Box, Full} +import net.liftweb.http.{LiftResponse, PlainTextResponse} +import org.apache.http.HttpStatus + +/* +This file defines which endpoints from all the versions are available in v5.0.0 + */ +object OBPAPI6_0_0 extends OBPRestHelper + with APIMethods130 + with APIMethods140 + with APIMethods200 + with APIMethods210 + with APIMethods220 + with APIMethods300 + with CustomAPIMethods300 + with APIMethods310 + with APIMethods400 + with APIMethods500 + with APIMethods510 + with APIMethods600 + with MdcLoggable + with VersionedOBPApis{ + + val version : ApiVersion = ApiVersion.v6_0_0 + + val versionStatus = ApiVersionStatus.BLEEDING_EDGE.toString + + // Possible Endpoints from 5.1.0, exclude one endpoint use - method,exclude multiple endpoints use -- method, + // e.g getEndpoints(Implementations5_0_0) -- List(Implementations5_0_0.genericEndpoint, Implementations5_0_0.root) + lazy val endpointsOf6_0_0 = getEndpoints(Implementations6_0_0) + + lazy val excludeEndpoints = + nameOf(Implementations3_0_0.getUserByUsername) :: // following 4 endpoints miss Provider parameter in the URL, we introduce new ones in V600. + nameOf(Implementations3_1_0.getBadLoginStatus) :: + nameOf(Implementations3_1_0.unlockUser) :: + nameOf(Implementations4_0_0.lockUser) :: + nameOf(Implementations4_0_0.createUserWithAccountAccess) :: // following 3 endpoints miss ViewId parameter in the URL, we introduce new ones in V600. + nameOf(Implementations4_0_0.grantUserAccessToView) :: + nameOf(Implementations4_0_0.revokeUserAccessToView) :: + nameOf(Implementations4_0_0.revokeGrantUserAccessToViews) ::// this endpoint is forbidden in V600, we do not support multi views in one endpoint from V600. + Nil + + // if old version ResourceDoc objects have the same name endpoint with new version, omit old version ResourceDoc. + def allResourceDocs = collectResourceDocs( + OBPAPI5_1_0.allResourceDocs, + Implementations6_0_0.resourceDocs + ).filterNot(it => it.partialFunctionName.matches(excludeEndpoints.mkString("|"))) + + // all endpoints + private val endpoints: List[OBPEndpoint] = OBPAPI5_1_0.routes ++ endpointsOf6_0_0 + + // Filter the possible endpoints by the disabled / enabled Props settings and add them together + val routes : List[OBPEndpoint] = getAllowedEndpoints(endpoints, allResourceDocs) + + registerRoutes(routes, allResourceDocs, apiPrefix, true) + + + logger.info(s"version $version has been run! There are ${routes.length} routes, ${allResourceDocs.length} allResourceDocs.") + + // specified response for OPTIONS request. + private val corsResponse: Box[LiftResponse] = Full{ + val corsHeaders = List( + "Access-Control-Allow-Origin" -> "*", + "Access-Control-Allow-Methods" -> "GET, POST, OPTIONS, PUT, PATCH, DELETE", + "Access-Control-Allow-Headers" -> "*", + "Access-Control-Allow-Credentials" -> "true", + "Access-Control-Max-Age" -> "1728000" //Tell client that this pre-flight info is valid for 20 days + ) + PlainTextResponse("", corsHeaders, HttpStatus.SC_NO_CONTENT) + } + /* + * process OPTIONS http request, just return no content and status is 204 + */ + this.serve({ + case req if req.requestType.method == "OPTIONS" => corsResponse + }) +} diff --git a/obp-api/src/main/scala/code/bankconnectors/LocalMappedConnectorInternal.scala b/obp-api/src/main/scala/code/bankconnectors/LocalMappedConnectorInternal.scala index ef460f051..4c183d840 100644 --- a/obp-api/src/main/scala/code/bankconnectors/LocalMappedConnectorInternal.scala +++ b/obp-api/src/main/scala/code/bankconnectors/LocalMappedConnectorInternal.scala @@ -13,7 +13,7 @@ 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.v5_1_0.TransactionRequestBodyCardanoJsonV510 +import code.api.v6_0_0.TransactionRequestBodyCardanoJsonV600 import code.branches.MappedBranch import code.fx.fx import code.fx.fx.TTL @@ -758,7 +758,7 @@ object LocalMappedConnectorInternal extends MdcLoggable { // case CARDANO => // for{ // transactionRequestBodyCardanoJson <- NewStyle.function.tryons(s"${InvalidJsonFormat}, it should be $CARD json format", 400, callContext) { -// json.extract[TransactionRequestBodyCardanoJsonV510] +// json.extract[TransactionRequestBodyCardanoJsonV600] // } // (account, callContext) <- NewStyle.function.getBankAccountByRouting( // None, //No need for the bankId, only to address is enough @@ -1359,8 +1359,8 @@ object LocalMappedConnectorInternal extends MdcLoggable { 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 $TransactionRequestBodyCardanoJsonV510 json format", 400, callContext) { - json.extract[TransactionRequestBodyCardanoJsonV510] + transactionRequestBodyCardano <- NewStyle.function.tryons(s"${InvalidJsonFormat} It should be $TransactionRequestBodyCardanoJsonV600 json format", 400, callContext) { + json.extract[TransactionRequestBodyCardanoJsonV600] } // Validate Cardano specific fields diff --git a/obp-api/src/main/scala/code/bankconnectors/cardano/CardanoConnector_vJun2025.scala b/obp-api/src/main/scala/code/bankconnectors/cardano/CardanoConnector_vJun2025.scala index afa71341b..01dbcc262 100644 --- a/obp-api/src/main/scala/code/bankconnectors/cardano/CardanoConnector_vJun2025.scala +++ b/obp-api/src/main/scala/code/bankconnectors/cardano/CardanoConnector_vJun2025.scala @@ -25,7 +25,7 @@ Berlin 13359, Germany import code.api.util.APIUtil._ import code.api.util.{CallContext, ErrorMessages, NewStyle} -import code.api.v5_1_0.TransactionRequestBodyCardanoJsonV510 +import code.api.v6_0_0.TransactionRequestBodyCardanoJsonV600 import code.bankconnectors._ import code.util.AkkaHttpClient._ import code.util.Helper @@ -61,9 +61,9 @@ trait CardanoConnector_vJun2025 extends Connector with MdcLoggable { callContext: Option[CallContext]): OBPReturnType[Box[TransactionId]] = { for { - failMsg <- Future.successful(s"${ErrorMessages.InvalidJsonFormat} The transaction request body should be $TransactionRequestBodyCardanoJsonV510") + failMsg <- Future.successful(s"${ErrorMessages.InvalidJsonFormat} The transaction request body should be $TransactionRequestBodyCardanoJsonV600") transactionRequestBodyCardano <- NewStyle.function.tryons(failMsg, 400, callContext) { - transactionRequestCommonBody.asInstanceOf[TransactionRequestBodyCardanoJsonV510] + transactionRequestCommonBody.asInstanceOf[TransactionRequestBodyCardanoJsonV600] } walletId = fromAccount.accountId.value @@ -120,7 +120,7 @@ trait CardanoConnector_vJun2025 extends Connector with MdcLoggable { * Amount is always required in Cardano transactions * Supports different payment types: ADA only, Token only, ADA + Token */ - private def buildPaymentsArray(transactionRequestBodyCardano: TransactionRequestBodyCardanoJsonV510): String = { + private def buildPaymentsArray(transactionRequestBodyCardano: TransactionRequestBodyCardanoJsonV600): String = { val address = transactionRequestBodyCardano.to.address // Amount is always required in Cardano @@ -171,7 +171,7 @@ trait CardanoConnector_vJun2025 extends Connector with MdcLoggable { * Build metadata JSON for Cardano API * Supports simple string metadata format */ - private def buildMetadataJson(transactionRequestBodyCardano: TransactionRequestBodyCardanoJsonV510): String = { + private def buildMetadataJson(transactionRequestBodyCardano: TransactionRequestBodyCardanoJsonV600): String = { transactionRequestBodyCardano.metadata match { case Some(metadata) if metadata.nonEmpty => { val metadataEntries = metadata.map { case (label, metadataObj) => diff --git a/obp-api/src/test/scala/code/api/v5_1_0/CardanoTransactionRequestTest.scala b/obp-api/src/test/scala/code/api/v6_0_0/CardanoTransactionRequestTest.scala similarity index 89% rename from obp-api/src/test/scala/code/api/v5_1_0/CardanoTransactionRequestTest.scala rename to obp-api/src/test/scala/code/api/v6_0_0/CardanoTransactionRequestTest.scala index afc5b49e0..809dd3593 100644 --- a/obp-api/src/test/scala/code/api/v5_1_0/CardanoTransactionRequestTest.scala +++ b/obp-api/src/test/scala/code/api/v6_0_0/CardanoTransactionRequestTest.scala @@ -23,7 +23,7 @@ Berlin 13359, Germany This product includes software developed at TESOBE (http://www.tesobe.com/) */ -package code.api.v5_1_0 +package code.api.v6_0_0 import code.api.Constant import code.api.ResourceDocs1_4_0.SwaggerDefinitionsJSON @@ -32,7 +32,7 @@ import code.api.util.ApiRole import code.api.util.ApiRole._ import code.api.util.ErrorMessages._ import code.api.v4_0_0.TransactionRequestWithChargeJSON400 -import code.api.v5_1_0.OBPAPI5_1_0.Implementations5_1_0 +import code.api.v6_0_0.OBPAPI6_0_0.Implementations6_0_0 import code.entitlement.Entitlement import code.methodrouting.MethodRoutingCommons import com.github.dwickern.macros.NameOf.nameOf @@ -42,7 +42,7 @@ import net.liftweb.json.Serialization.write import org.scalatest.Tag -class CardanoTransactionRequestTest extends V510ServerSetup { +class CardanoTransactionRequestTest extends V600ServerSetup { /** * Test tags @@ -52,7 +52,7 @@ class CardanoTransactionRequestTest extends V510ServerSetup { * This is made possible by the scalatest maven plugin */ object VersionOfApi extends Tag(ApiVersion.v5_1_0.toString) - object CreateTransactionRequestCardano extends Tag(nameOf(Implementations5_1_0.createTransactionRequestCardano)) + object CreateTransactionRequestCardano extends Tag(nameOf(Implementations6_0_0.createTransactionRequestCardano)) val testBankId = testBankId1.value @@ -72,11 +72,11 @@ class CardanoTransactionRequestTest extends V510ServerSetup { scenario("We will create Cardano transaction request - user is NOT logged in", CreateTransactionRequestCardano, VersionOfApi) { When("We make a request v5.1.0") - val request510 = (v5_1_0_Request / "banks" / testBankId / "accounts" / testAccountId / Constant.SYSTEM_OWNER_VIEW_ID / "transaction-request-types" / "CARDANO" / "transaction-requests").POST - val cardanoTransactionRequestBody = TransactionRequestBodyCardanoJsonV510( - to = CardanoPaymentJsonV510( + val request510 = (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 = CardanoAmountJsonV510( + amount = CardanoAmountJsonV600( quantity = 1000000, unit = "lovelace" ) @@ -113,11 +113,11 @@ class CardanoTransactionRequestTest extends V510ServerSetup { response310.code should equal(201) When("We make a request v5.1.0") - val request510 = (v5_1_0_Request / "banks" / testBankId / "accounts" / testAccountId / Constant.SYSTEM_OWNER_VIEW_ID / "transaction-request-types" / "CARDANO" / "transaction-requests").POST <@(user1) - val cardanoTransactionRequestBody = TransactionRequestBodyCardanoJsonV510( - to = CardanoPaymentJsonV510( + val request510 = (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 = CardanoAmountJsonV510( + amount = CardanoAmountJsonV600( quantity = 1000000, unit = "lovelace" ) @@ -155,11 +155,11 @@ class CardanoTransactionRequestTest extends V510ServerSetup { response310.code should equal(201) When("We make a request v5.1.0 with metadata") - val request510 = (v5_1_0_Request / "banks" / testBankId / "accounts" / testAccountId / Constant.SYSTEM_OWNER_VIEW_ID / "transaction-request-types" / "CARDANO" / "transaction-requests").POST <@(user1) - val cardanoTransactionRequestBody = TransactionRequestBodyCardanoJsonV510( - to = CardanoPaymentJsonV510( + val request510 = (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 = CardanoAmountJsonV510( + amount = CardanoAmountJsonV600( quantity = 1000000, unit = "lovelace" ) @@ -167,7 +167,7 @@ class CardanoTransactionRequestTest extends V510ServerSetup { value = AmountOfMoneyJsonV121("lovelace", "1000000"), passphrase = passphrase, description = "ADA transfer with metadata", - metadata = Some(Map("202507022319" -> CardanoMetadataStringJsonV510("Hello Cardano"))) + metadata = Some(Map("202507022319" -> CardanoMetadataStringJsonV600("Hello Cardano"))) ) val response510 = makePostRequest(request510, write(cardanoTransactionRequestBody)) Then("We should get a 201") @@ -198,15 +198,15 @@ class CardanoTransactionRequestTest extends V510ServerSetup { response310.code should equal(201) When("We make a request v5.1.0 with token") - val request510 = (v5_1_0_Request / "banks" / testBankId / "accounts" / testAccountId / Constant.SYSTEM_OWNER_VIEW_ID / "transaction-request-types" / "CARDANO" / "transaction-requests").POST <@(user1) - val cardanoTransactionRequestBody = TransactionRequestBodyCardanoJsonV510( - to = CardanoPaymentJsonV510( + val request510 = (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 = CardanoAmountJsonV510( + amount = CardanoAmountJsonV600( quantity = 1000000, unit = "lovelace" ), - assets = Some(List(CardanoAssetJsonV510( + assets = Some(List(CardanoAssetJsonV600( policy_id = "policy1234567890abcdef", asset_name = "4f47435241", quantity = 10 @@ -226,15 +226,15 @@ class CardanoTransactionRequestTest extends V510ServerSetup { scenario("We will create Cardano transaction request with token and metadata - user is logged in", CreateTransactionRequestCardano, VersionOfApi) { When("We make a request v5.1.0 with token and metadata") - val request510 = (v5_1_0_Request / "banks" / testBankId / "accounts" / testAccountId / Constant.SYSTEM_OWNER_VIEW_ID / "transaction-request-types" / "CARDANO" / "transaction-requests").POST <@(user1) - val cardanoTransactionRequestBody = TransactionRequestBodyCardanoJsonV510( - to = CardanoPaymentJsonV510( + val request510 = (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 = CardanoAmountJsonV510( + amount = CardanoAmountJsonV600( quantity = 5000000, unit = "lovelace" ), - assets = Some(List(CardanoAssetJsonV510( + assets = Some(List(CardanoAssetJsonV600( policy_id = "policy1234567890abcdef", asset_name = "4f47435241", quantity = 10 @@ -243,7 +243,7 @@ class CardanoTransactionRequestTest extends V510ServerSetup { value = AmountOfMoneyJsonV121("lovelace", "1000000"), passphrase = passphrase, description = "ADA transfer with token and metadata", - metadata = Some(Map("202507022319" -> CardanoMetadataStringJsonV510("Hello Cardano with Token"))) + metadata = Some(Map("202507022319" -> CardanoMetadataStringJsonV600("Hello Cardano with Token"))) ) val response510 = makePostRequest(request510, write(cardanoTransactionRequestBody)) Then("We should get a 201") @@ -255,11 +255,11 @@ class CardanoTransactionRequestTest extends V510ServerSetup { scenario("We will try to create Cardano transaction request for someone else account - user is logged in", CreateTransactionRequestCardano, VersionOfApi) { When("We make a request v5.1.0") - val request510 = (v5_1_0_Request / "banks" / testBankId / "accounts" / testAccountId / Constant.SYSTEM_OWNER_VIEW_ID / "transaction-request-types" / "CARDANO" / "transaction-requests").POST <@(user2) - val cardanoTransactionRequestBody = TransactionRequestBodyCardanoJsonV510( - to = CardanoPaymentJsonV510( + val request510 = (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 = CardanoAmountJsonV510( + amount = CardanoAmountJsonV600( quantity = 1000000, unit = "lovelace" ) @@ -277,11 +277,11 @@ class CardanoTransactionRequestTest extends V510ServerSetup { scenario("We will try to create Cardano transaction request with invalid address format", CreateTransactionRequestCardano, VersionOfApi) { When("We make a request v5.1.0 with invalid address") - val request510 = (v5_1_0_Request / "banks" / testBankId / "accounts" / testAccountId / Constant.SYSTEM_OWNER_VIEW_ID / "transaction-request-types" / "CARDANO" / "transaction-requests").POST <@(user1) - val cardanoTransactionRequestBody = TransactionRequestBodyCardanoJsonV510( - to = CardanoPaymentJsonV510( + val request510 = (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 = CardanoAmountJsonV510( + amount = CardanoAmountJsonV600( quantity = 1000000, unit = "lovelace" ) @@ -299,7 +299,7 @@ class CardanoTransactionRequestTest extends V510ServerSetup { scenario("We will try to create Cardano transaction request with missing amount", CreateTransactionRequestCardano, VersionOfApi) { When("We make a request v5.1.0 with missing amount") - val request510 = (v5_1_0_Request / "banks" / testBankId / "accounts" / testAccountId / Constant.SYSTEM_OWNER_VIEW_ID / "transaction-request-types" / "CARDANO" / "transaction-requests").POST <@(user1) + val request510 = (v6_0_0_Request / "banks" / testBankId / "accounts" / testAccountId / Constant.SYSTEM_OWNER_VIEW_ID / "transaction-request-types" / "CARDANO" / "transaction-requests").POST <@(user1) val invalidJson = """ { "to": { @@ -322,11 +322,11 @@ class CardanoTransactionRequestTest extends V510ServerSetup { scenario("We will try to create Cardano transaction request with negative amount", CreateTransactionRequestCardano, VersionOfApi) { When("We make a request v5.1.0 with negative amount") - val request510 = (v5_1_0_Request / "banks" / testBankId / "accounts" / testAccountId / Constant.SYSTEM_OWNER_VIEW_ID / "transaction-request-types" / "CARDANO" / "transaction-requests").POST <@(user1) - val cardanoTransactionRequestBody = TransactionRequestBodyCardanoJsonV510( - to = CardanoPaymentJsonV510( + val request510 = (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 = CardanoAmountJsonV510( + amount = CardanoAmountJsonV600( quantity = -1000000, unit = "lovelace" ) @@ -344,11 +344,11 @@ class CardanoTransactionRequestTest extends V510ServerSetup { scenario("We will try to create Cardano transaction request with invalid amount unit", CreateTransactionRequestCardano, VersionOfApi) { When("We make a request v5.1.0 with invalid amount unit") - val request510 = (v5_1_0_Request / "banks" / testBankId / "accounts" / testAccountId / Constant.SYSTEM_OWNER_VIEW_ID / "transaction-request-types" / "CARDANO" / "transaction-requests").POST <@(user1) - val cardanoTransactionRequestBody = TransactionRequestBodyCardanoJsonV510( - to = CardanoPaymentJsonV510( + val request510 = (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 = CardanoAmountJsonV510( + amount = CardanoAmountJsonV600( quantity = 1000000, unit = "abc" // Invalid unit, should be "lovelace" ) @@ -366,11 +366,11 @@ class CardanoTransactionRequestTest extends V510ServerSetup { scenario("We will try to create Cardano transaction request with zero amount but no assets", CreateTransactionRequestCardano, VersionOfApi) { When("We make a request v5.1.0 with zero amount but no assets") - val request510 = (v5_1_0_Request / "banks" / testBankId / "accounts" / testAccountId / Constant.SYSTEM_OWNER_VIEW_ID / "transaction-request-types" / "CARDANO" / "transaction-requests").POST <@(user1) - val cardanoTransactionRequestBody = TransactionRequestBodyCardanoJsonV510( - to = CardanoPaymentJsonV510( + val request510 = (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 = CardanoAmountJsonV510( + amount = CardanoAmountJsonV600( quantity = 0, unit = "lovelace" ) @@ -388,15 +388,15 @@ class CardanoTransactionRequestTest extends V510ServerSetup { scenario("We will try to create Cardano transaction request with invalid assets", CreateTransactionRequestCardano, VersionOfApi) { When("We make a request v5.1.0 with invalid assets") - val request510 = (v5_1_0_Request / "banks" / testBankId / "accounts" / testAccountId / Constant.SYSTEM_OWNER_VIEW_ID / "transaction-request-types" / "CARDANO" / "transaction-requests").POST <@(user1) - val cardanoTransactionRequestBody = TransactionRequestBodyCardanoJsonV510( - to = CardanoPaymentJsonV510( + val request510 = (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 = CardanoAmountJsonV510( + amount = CardanoAmountJsonV600( quantity = 0, unit = "lovelace" ), - assets = Some(List(CardanoAssetJsonV510( + assets = Some(List(CardanoAssetJsonV600( policy_id = "", asset_name = "", quantity = 0 @@ -415,11 +415,11 @@ class CardanoTransactionRequestTest extends V510ServerSetup { scenario("We will try to create Cardano transaction request with invalid metadata", CreateTransactionRequestCardano, VersionOfApi) { When("We make a request v5.1.0 with invalid metadata") - val request510 = (v5_1_0_Request / "banks" / testBankId / "accounts" / testAccountId / Constant.SYSTEM_OWNER_VIEW_ID / "transaction-request-types" / "CARDANO" / "transaction-requests").POST <@(user1) - val cardanoTransactionRequestBody = TransactionRequestBodyCardanoJsonV510( - to = CardanoPaymentJsonV510( + val request510 = (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 = CardanoAmountJsonV510( + amount = CardanoAmountJsonV600( quantity = 1000000, unit = "lovelace" ) @@ -427,7 +427,7 @@ class CardanoTransactionRequestTest extends V510ServerSetup { value = AmountOfMoneyJsonV121("lovelace", "1000000"), passphrase = passphrase, description = "ADA transfer with invalid metadata", - metadata = Some(Map("" -> CardanoMetadataStringJsonV510(""))) + metadata = Some(Map("" -> CardanoMetadataStringJsonV600(""))) ) val response510 = makePostRequest(request510, write(cardanoTransactionRequestBody)) Then("We should get a 400") diff --git a/obp-api/src/test/scala/code/api/v6_0_0/V600ServerSetup.scala b/obp-api/src/test/scala/code/api/v6_0_0/V600ServerSetup.scala new file mode 100644 index 000000000..ef28eb975 --- /dev/null +++ b/obp-api/src/test/scala/code/api/v6_0_0/V600ServerSetup.scala @@ -0,0 +1,13 @@ +package code.api.v6_0_0 + +import code.setup.{DefaultUsers, ServerSetupWithTestData} +import dispatch.Req + +trait V600ServerSetup extends ServerSetupWithTestData with DefaultUsers { + + def v4_0_0_Request: Req = baseRequest / "obp" / "v4.0.0" + def v5_0_0_Request: Req = baseRequest / "obp" / "v5.0.0" + def v5_1_0_Request: Req = baseRequest / "obp" / "v5.1.0" + def v6_0_0_Request: Req = baseRequest / "obp" / "v5.1.0" + +} \ No newline at end of file diff --git a/obp-commons/src/main/scala/com/openbankproject/commons/util/ApiVersion.scala b/obp-commons/src/main/scala/com/openbankproject/commons/util/ApiVersion.scala index ad237ba7e..0a5617d18 100644 --- a/obp-commons/src/main/scala/com/openbankproject/commons/util/ApiVersion.scala +++ b/obp-commons/src/main/scala/com/openbankproject/commons/util/ApiVersion.scala @@ -1,9 +1,8 @@ package com.openbankproject.commons.util -import com.openbankproject.commons.util.ApiShortVersions.Value +import net.liftweb.json._ import java.util.concurrent.ConcurrentHashMap -import net.liftweb.json.{Formats, JField, JObject, JString, JsonAST} object ApiStandards extends Enumeration { type ApiStandards = Value @@ -23,6 +22,7 @@ object ApiShortVersions extends Enumeration { val `v4.0.0` = Value("v4.0.0") val `v5.0.0` = Value("v5.0.0") val `v5.1.0` = Value("v5.1.0") + val `v6.0.0` = Value("v6.0.0") val `dynamic-endpoint` = Value("dynamic-endpoint") val `dynamic-entity` = Value("dynamic-entity") } @@ -113,6 +113,7 @@ object ApiVersion { val v4_0_0 = ScannedApiVersion(urlPrefix,ApiStandards.obp.toString,ApiShortVersions.`v4.0.0`.toString) val v5_0_0 = ScannedApiVersion(urlPrefix,ApiStandards.obp.toString,ApiShortVersions.`v5.0.0`.toString) val v5_1_0 = ScannedApiVersion(urlPrefix,ApiStandards.obp.toString,ApiShortVersions.`v5.1.0`.toString) + val v6_0_0 = ScannedApiVersion(urlPrefix,ApiStandards.obp.toString,ApiShortVersions.`v6.0.0`.toString) val `dynamic-endpoint` = ScannedApiVersion(urlPrefix,ApiStandards.obp.toString,ApiShortVersions.`dynamic-endpoint`.toString) val `dynamic-entity` = ScannedApiVersion(urlPrefix,ApiStandards.obp.toString,ApiShortVersions.`dynamic-entity`.toString) @@ -129,6 +130,7 @@ object ApiVersion { v4_0_0 :: v5_0_0 :: v5_1_0 :: + v6_0_0 :: `dynamic-endpoint` :: `dynamic-entity`:: Nil From 1c5332719227d49aeb1b6b7cb9e41e132c56272f Mon Sep 17 00:00:00 2001 From: hongwei Date: Thu, 21 Aug 2025 17:26:52 +0200 Subject: [PATCH 21/28] feature/enhance v6.0.0 API with new Cardano transaction request handling - Added support for the new API version v6.0.0, including the introduction of ResourceDocs600. - Updated Boot.scala and OBPRestHelper.scala to enable version v6.0.0. - Enhanced validation logic to include v6.0.0 in the API methods. - Implemented new test cases for Cardano transaction requests in the V600ServerSetup. - Refactored existing code to ensure compatibility with the new API version. --- .../main/scala/bootstrap/liftweb/Boot.scala | 5 +- .../main/scala/code/api/OBPRestHelper.scala | 18 +- .../ResourceDocs1_4_0/ResourceDocs140.scala | 18 +- .../main/scala/code/api/util/APIUtil.scala | 4 +- .../scala/code/api/v6_0_0/APIMethods600.scala | 1 + .../bankconnectors/LocalMappedConnector.scala | 4 +- .../LocalMappedConnectorInternal.scala | 4 +- .../CardanoTransactionRequestTest.scala | 706 +++++++++--------- .../code/api/v6_0_0/V600ServerSetup.scala | 2 +- 9 files changed, 385 insertions(+), 377 deletions(-) diff --git a/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala b/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala index 9ae42172e..525b20a74 100644 --- a/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala +++ b/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala @@ -35,7 +35,7 @@ import code.accountattribute.MappedAccountAttribute import code.accountholders.MapperAccountHolders import code.actorsystem.ObpActorSystem import code.api.Constant._ -import code.api.ResourceDocs1_4_0.ResourceDocs300.{ResourceDocs310, ResourceDocs400, ResourceDocs500, ResourceDocs510} +import code.api.ResourceDocs1_4_0.ResourceDocs300.{ResourceDocs310, ResourceDocs400, ResourceDocs500, ResourceDocs510, ResourceDocs600} import code.api.ResourceDocs1_4_0._ import code.api._ import code.api.attributedefinition.AttributeDefinition @@ -46,7 +46,6 @@ import code.api.util.ApiRole.CanCreateEntitlementAtAnyBank import code.api.util.ErrorMessages.MandatoryPropertyIsNotSet import code.api.util._ import code.api.util.migration.Migration -import code.api.util.CommonsEmailWrapper import code.api.util.migration.Migration.DbFunction import code.apicollection.ApiCollection import code.apicollectionendpoint.ApiCollectionEndpoint @@ -467,6 +466,7 @@ class Boot extends MdcLoggable { enableVersionIfAllowed(ApiVersion.v4_0_0) enableVersionIfAllowed(ApiVersion.v5_0_0) enableVersionIfAllowed(ApiVersion.v5_1_0) + enableVersionIfAllowed(ApiVersion.v6_0_0) enableVersionIfAllowed(ApiVersion.`dynamic-endpoint`) enableVersionIfAllowed(ApiVersion.`dynamic-entity`) @@ -525,6 +525,7 @@ class Boot extends MdcLoggable { LiftRules.statelessDispatch.append(ResourceDocs400) LiftRules.statelessDispatch.append(ResourceDocs500) LiftRules.statelessDispatch.append(ResourceDocs510) + LiftRules.statelessDispatch.append(ResourceDocs600) //////////////////////////////////////////////////// diff --git a/obp-api/src/main/scala/code/api/OBPRestHelper.scala b/obp-api/src/main/scala/code/api/OBPRestHelper.scala index f707e265b..6a61413eb 100644 --- a/obp-api/src/main/scala/code/api/OBPRestHelper.scala +++ b/obp-api/src/main/scala/code/api/OBPRestHelper.scala @@ -27,36 +27,32 @@ TESOBE (http://www.tesobe.com/) package code.api -import java.net.URLDecoder import code.api.Constant._ import code.api.OAuthHandshake._ -import code.api.builder.AccountInformationServiceAISApi.APIMethods_AccountInformationServiceAISApi -import code.api.util.APIUtil.{getClass, _} +import code.api.util.APIUtil._ import code.api.util.ErrorMessages.{InvalidDAuthHeaderToken, UserIsDeleted, UsernameHasBeenLocked, attemptedToOpenAnEmptyBox} import code.api.util._ -import code.api.v3_0_0.APIMethods300 -import code.api.v3_1_0.APIMethods310 -import code.api.v4_0_0.{APIMethods400, OBPAPI4_0_0} +import code.api.v4_0_0.OBPAPI4_0_0 import code.api.v5_0_0.OBPAPI5_0_0 import code.api.v5_1_0.OBPAPI5_1_0 +import code.api.v6_0_0.OBPAPI6_0_0 import code.loginattempts.LoginAttempt import code.model.dataAccess.AuthUser import code.util.Helper.{MdcLoggable, ObpS} import com.alibaba.ttl.TransmittableThreadLocal import com.openbankproject.commons.model.ErrorMessage import com.openbankproject.commons.util.{ApiVersion, ReflectUtils, ScannedApiVersion} -import net.liftweb.common.{Box, Full, _} +import net.liftweb.common._ import net.liftweb.http.rest.RestHelper import net.liftweb.http.{JsonResponse, LiftResponse, LiftRules, Req, S, TransientRequestMemoize} import net.liftweb.json.Extraction import net.liftweb.json.JsonAST.JValue -import net.liftweb.util.{Helpers, NamedPF, Props, ThreadGlobal} import net.liftweb.util.Helpers.tryo +import net.liftweb.util.{Helpers, NamedPF, Props, ThreadGlobal} +import java.net.URLDecoder import java.util.{Locale, ResourceBundle} -import scala.collection.immutable.List import scala.collection.mutable.ArrayBuffer -import scala.math.Ordering import scala.util.control.NoStackTrace import scala.xml.{Node, NodeSeq} @@ -648,7 +644,7 @@ trait OBPRestHelper extends RestHelper with MdcLoggable { 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)) + doc.isValidateEnabled || (autoValidateAll && !doc.isValidateDisabled && List(OBPAPI6_0_0.version,OBPAPI5_1_0.version,OBPAPI5_0_0.version,OBPAPI4_0_0.version).contains(doc.implementedInApiVersion)) } for(route <- routes) { diff --git a/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/ResourceDocs140.scala b/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/ResourceDocs140.scala index c53650ef7..d7d3cc31a 100644 --- a/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/ResourceDocs140.scala +++ b/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/ResourceDocs140.scala @@ -1,8 +1,8 @@ package code.api.ResourceDocs1_4_0 import code.api.OBPRestHelper -import com.openbankproject.commons.util.{ApiVersion,ApiVersionStatus} import code.util.Helper.MdcLoggable +import com.openbankproject.commons.util.{ApiVersion, ApiVersionStatus} object ResourceDocs140 extends OBPRestHelper with ResourceDocsAPIMethods with MdcLoggable { @@ -136,5 +136,21 @@ object ResourceDocs300 extends OBPRestHelper with ResourceDocsAPIMethods with Md }) }) } + + object ResourceDocs600 extends OBPRestHelper with ResourceDocsAPIMethods with MdcLoggable { + val version: ApiVersion = ApiVersion.v6_0_0 + val versionStatus = ApiVersionStatus.BLEEDING_EDGE.toString + val routes = List( + ImplementationsResourceDocs.getResourceDocsObpV400, + ImplementationsResourceDocs.getResourceDocsSwagger, + ImplementationsResourceDocs.getBankLevelDynamicResourceDocsObp, +// ImplementationsResourceDocs.getStaticResourceDocsObp + ) + routes.foreach(route => { + oauthServe(apiPrefix { + route + }) + }) + } } \ No newline at end of file diff --git a/obp-api/src/main/scala/code/api/util/APIUtil.scala b/obp-api/src/main/scala/code/api/util/APIUtil.scala index 176726d85..8d824c085 100644 --- a/obp-api/src/main/scala/code/api/util/APIUtil.scala +++ b/obp-api/src/main/scala/code/api/util/APIUtil.scala @@ -71,7 +71,6 @@ import code.util.{Helper, JsonSchemaUtil} import code.views.system.AccountAccess import code.views.{MapperViews, Views} import code.webuiprops.MappedWebUiPropsProvider.getWebUiPropsValue -import javassist.CannotCompileException import com.github.dwickern.macros.NameOf.{nameOf, nameOfType} import com.openbankproject.commons.ExecutionContext.Implicits.global import com.openbankproject.commons.model._ @@ -81,7 +80,7 @@ import com.openbankproject.commons.util.Functions.Implicits._ import com.openbankproject.commons.util._ import dispatch.url import javassist.expr.{ExprEditor, MethodCall} -import javassist.{ClassPool, LoaderClassPath} +import javassist.{CannotCompileException, ClassPool, LoaderClassPath} import net.liftweb.actor.LAFuture import net.liftweb.common._ import net.liftweb.http._ @@ -2747,6 +2746,7 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{ case ApiVersion.v4_0_0 => LiftRules.statelessDispatch.append(v4_0_0.OBPAPI4_0_0) case ApiVersion.v5_0_0 => LiftRules.statelessDispatch.append(v5_0_0.OBPAPI5_0_0) case ApiVersion.v5_1_0 => LiftRules.statelessDispatch.append(v5_1_0.OBPAPI5_1_0) + case ApiVersion.v6_0_0 => LiftRules.statelessDispatch.append(v6_0_0.OBPAPI6_0_0) case ApiVersion.`dynamic-endpoint` => LiftRules.statelessDispatch.append(OBPAPIDynamicEndpoint) case ApiVersion.`dynamic-entity` => LiftRules.statelessDispatch.append(OBPAPIDynamicEntity) case version: ScannedApiVersion => LiftRules.statelessDispatch.append(ScannedApis.versionMapScannedApis(version)) diff --git a/obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala b/obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala index 57e4b824c..e251b9430 100644 --- a/obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala +++ b/obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala @@ -15,6 +15,7 @@ import net.liftweb.http.rest.RestHelper import scala.collection.immutable.{List, Nil} import scala.collection.mutable.ArrayBuffer + trait APIMethods600 { self: RestHelper => diff --git a/obp-api/src/main/scala/code/bankconnectors/LocalMappedConnector.scala b/obp-api/src/main/scala/code/bankconnectors/LocalMappedConnector.scala index a2fee1efe..7784852dd 100644 --- a/obp-api/src/main/scala/code/bankconnectors/LocalMappedConnector.scala +++ b/obp-api/src/main/scala/code/bankconnectors/LocalMappedConnector.scala @@ -78,8 +78,8 @@ import net.liftweb.common._ import net.liftweb.json import net.liftweb.json.{JArray, JBool, JObject, JValue} import net.liftweb.mapper._ -import net.liftweb.util.Helpers.{hours, now, time, tryo} import net.liftweb.util.Helpers +import net.liftweb.util.Helpers.{hours, now, time, tryo} import org.mindrot.jbcrypt.BCrypt import scalikejdbc.DB.CPContext import scalikejdbc.{ConnectionPool, ConnectionPoolSettings, MultipleConnectionPoolContext, DB => scalikeDB, _} @@ -163,7 +163,7 @@ 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.equals("lovelace")||(currency.equals("ada")))) => + 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 { diff --git a/obp-api/src/main/scala/code/bankconnectors/LocalMappedConnectorInternal.scala b/obp-api/src/main/scala/code/bankconnectors/LocalMappedConnectorInternal.scala index 4c183d840..4849942e9 100644 --- a/obp-api/src/main/scala/code/bankconnectors/LocalMappedConnectorInternal.scala +++ b/obp-api/src/main/scala/code/bankconnectors/LocalMappedConnectorInternal.scala @@ -1382,9 +1382,9 @@ object LocalMappedConnectorInternal extends MdcLoggable { transactionRequestBodyCardano.to.amount.quantity >= 0 } - // Validate amount unit must be 'lovelace' + // Validate amount unit must be 'lovelace' (case insensitive) _ <- Helper.booleanToFuture(s"$InvalidJsonValue Cardano amount unit must be 'lovelace'", cc=callContext) { - transactionRequestBodyCardano.to.amount.unit == "lovelace" + transactionRequestBodyCardano.to.amount.unit.toLowerCase == "lovelace" } // Validate assets if provided diff --git a/obp-api/src/test/scala/code/api/v6_0_0/CardanoTransactionRequestTest.scala b/obp-api/src/test/scala/code/api/v6_0_0/CardanoTransactionRequestTest.scala index 809dd3593..f135126f0 100644 --- a/obp-api/src/test/scala/code/api/v6_0_0/CardanoTransactionRequestTest.scala +++ b/obp-api/src/test/scala/code/api/v6_0_0/CardanoTransactionRequestTest.scala @@ -27,14 +27,8 @@ package code.api.v6_0_0 import code.api.Constant import code.api.ResourceDocs1_4_0.SwaggerDefinitionsJSON -import code.api.util.APIUtil.OAuth._ -import code.api.util.ApiRole -import code.api.util.ApiRole._ import code.api.util.ErrorMessages._ -import code.api.v4_0_0.TransactionRequestWithChargeJSON400 import code.api.v6_0_0.OBPAPI6_0_0.Implementations6_0_0 -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 @@ -51,7 +45,7 @@ class CardanoTransactionRequestTest extends V600ServerSetup { * * This is made possible by the scalatest maven plugin */ - object VersionOfApi extends Tag(ApiVersion.v5_1_0.toString) + object VersionOfApi extends Tag(ApiVersion.v6_0_0.toString) object CreateTransactionRequestCardano extends Tag(nameOf(Implementations6_0_0.createTransactionRequestCardano)) @@ -68,11 +62,11 @@ class CardanoTransactionRequestTest extends V600ServerSetup { ) - feature("Create Cardano Transaction Request - v5.1.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 v5.1.0") - val request510 = (v6_0_0_Request / "banks" / testBankId / "accounts" / testAccountId / Constant.SYSTEM_OWNER_VIEW_ID / "transaction-request-types" / "CARDANO" / "transaction-requests").POST + 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", @@ -85,355 +79,355 @@ class CardanoTransactionRequestTest extends V600ServerSetup { passphrase = passphrase, description = "Basic ADA transfer" ) - val response510 = makePostRequest(request510, write(cardanoTransactionRequestBody)) + val response600 = makePostRequest(request600, write(cardanoTransactionRequestBody)) Then("We should get a 401") - response510.code should equal(401) + response600.code should equal(401) And("error should be " + UserNotLoggedIn) - response510.body.extract[ErrorMessage].message should equal(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 = (v5_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 = (v5_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 v5.1.0") - val request510 = (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 response510 = makePostRequest(request510, write(cardanoTransactionRequestBody)) - Then("We should get a 201") - response510.code should equal(201) - And("response should contain transaction request") - val transactionRequest = response510.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 = (v5_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 = (v5_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 v5.1.0 with metadata") - val request510 = (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 response510 = makePostRequest(request510, write(cardanoTransactionRequestBody)) - Then("We should get a 201") - response510.code should equal(201) - And("response should contain transaction request") - val transactionRequest = response510.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 = (v5_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 = (v5_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 v5.1.0 with token") - val request510 = (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 = "policy1234567890abcdef", - asset_name = "4f47435241", - quantity = 10 - ))) - ), - value = AmountOfMoneyJsonV121("lovelace", "1000000"), - passphrase = passphrase, - description = "Token-only transfer" - ) - val response510 = makePostRequest(request510, write(cardanoTransactionRequestBody)) - Then("We should get a 201") - response510.code should equal(201) - And("response should contain transaction request") - val transactionRequest = response510.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 v5.1.0 with token and metadata") - val request510 = (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 = "policy1234567890abcdef", - asset_name = "4f47435241", - 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 response510 = makePostRequest(request510, write(cardanoTransactionRequestBody)) - Then("We should get a 201") - response510.code should equal(201) - And("response should contain transaction request") - val transactionRequest = response510.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 v5.1.0") - val request510 = (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 response510 = makePostRequest(request510, write(cardanoTransactionRequestBody)) - Then("We should get a 403") - response510.code should equal(403) - And("error should be " + UserNoPermissionAccessView) - response510.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 v5.1.0 with invalid address") - val request510 = (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 response510 = makePostRequest(request510, write(cardanoTransactionRequestBody)) - Then("We should get a 400") - response510.code should equal(400) - And("error should contain invalid address message") - response510.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 v5.1.0 with missing amount") - val request510 = (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 response510 = makePostRequest(request510, invalidJson) - Then("We should get a 400") - response510.code should equal(400) - And("error should contain invalid json format message") - response510.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 v5.1.0 with negative amount") - val request510 = (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 response510 = makePostRequest(request510, write(cardanoTransactionRequestBody)) - Then("We should get a 400") - response510.code should equal(400) - And("error should contain invalid amount message") - response510.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 v5.1.0 with invalid amount unit") - val request510 = (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 response510 = makePostRequest(request510, write(cardanoTransactionRequestBody)) - Then("We should get a 400") - response510.code should equal(400) - And("error should contain invalid unit message") - response510.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 v5.1.0 with zero amount but no assets") - val request510 = (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 response510 = makePostRequest(request510, write(cardanoTransactionRequestBody)) - Then("We should get a 400") - response510.code should equal(400) - And("error should contain invalid amount message") - response510.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 v5.1.0 with invalid assets") - val request510 = (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 response510 = makePostRequest(request510, write(cardanoTransactionRequestBody)) - Then("We should get a 400") - response510.code should equal(400) - And("error should contain invalid assets message") - response510.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 v5.1.0 with invalid metadata") - val request510 = (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 response510 = makePostRequest(request510, write(cardanoTransactionRequestBody)) - Then("We should get a 400") - response510.code should equal(400) - And("error should contain invalid metadata message") - response510.body.extract[ErrorMessage].message should include("Cardano metadata must have valid structure") - } +// scenario("We will create Cardano transaction request - user is logged in", CreateTransactionRequestCardano, VersionOfApi) { +// Entitlement.entitlement.vend.addEntitlement(testBankId, resourceUser1.userId, ApiRole.canCreateAccount.toString()) +// val request = (v6_0_0_Request / "banks" / testBankId / "accounts" / testAccountId ).PUT <@(user1) +// val response = makePutRequest(request, write(putCreateAccountJSONV310)) +// Then("We should get a 201") +// response.code should equal(201) +// +// When("We create a method routing for makePaymentv210 to cardano_vJun2025") +// val cardanoMethodRouting = MethodRoutingCommons( +// methodName = "makePaymentv210", +// connectorName = "cardano_vJun2025", +// isBankIdExactMatch = false, +// bankIdPattern = Some("*"), +// parameters = List() +// ) +// val request310 = (v6_0_0_Request / "management" / "method_routings").POST <@(user1) +// Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanCreateMethodRouting.toString) +// val response310 = makePostRequest(request310, write(cardanoMethodRouting)) +// response310.code should equal(201) +// +// When("We make a request v6.0.0") +// val request600 = (v6_0_0_Request / "banks" / testBankId / "accounts" / testAccountId / Constant.SYSTEM_OWNER_VIEW_ID / "transaction-request-types" / "CARDANO" / "transaction-requests").POST <@(user1) +// val cardanoTransactionRequestBody = TransactionRequestBodyCardanoJsonV600( +// to = CardanoPaymentJsonV600( +// address = "addr_test1qpv3se9ghq87ud29l0a8asy8nlqwd765e5zt4rc2z4mktqulwagn832cuzcjknfyxwzxz2p2kumx6n58tskugny6mrqs7fd23z", +// amount = CardanoAmountJsonV600( +// quantity = 1000000, +// unit = "lovelace" +// ) +// ), +// value = AmountOfMoneyJsonV121("lovelace", "1000000"), +// passphrase = passphrase, +// description = "Basic ADA transfer" +// ) +// val response600 = makePostRequest(request600, write(cardanoTransactionRequestBody)) +// Then("We should get a 201") +// response600.code should equal(201) +// And("response should contain transaction request") +// val transactionRequest = response600.body.extract[TransactionRequestWithChargeJSON400] +// transactionRequest.status should not be empty +// } +// +// scenario("We will create Cardano transaction request with metadata - user is logged in", CreateTransactionRequestCardano, VersionOfApi) { +// Entitlement.entitlement.vend.addEntitlement(testBankId, resourceUser1.userId, ApiRole.canCreateAccount.toString()) +// val request = (v6_0_0_Request / "banks" / testBankId / "accounts" / testAccountId ).PUT <@(user1) +// val response = makePutRequest(request, write(putCreateAccountJSONV310)) +// Then("We should get a 201") +// response.code should equal(201) +// +// When("We create a method routing for makePaymentv210 to cardano_vJun2025") +// val cardanoMethodRouting = MethodRoutingCommons( +// methodName = "makePaymentv210", +// connectorName = "cardano_vJun2025", +// isBankIdExactMatch = false, +// bankIdPattern = Some("*"), +// parameters = List() +// ) +// val request310 = (v6_0_0_Request / "management" / "method_routings").POST <@(user1) +// Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanCreateMethodRouting.toString) +// val response310 = makePostRequest(request310, write(cardanoMethodRouting)) +// response310.code should equal(201) +// +// When("We make a request v6.0.0 with metadata") +// val request600 = (v6_0_0_Request / "banks" / testBankId / "accounts" / testAccountId / Constant.SYSTEM_OWNER_VIEW_ID / "transaction-request-types" / "CARDANO" / "transaction-requests").POST <@(user1) +// val cardanoTransactionRequestBody = TransactionRequestBodyCardanoJsonV600( +// to = CardanoPaymentJsonV600( +// address = "addr_test1qpv3se9ghq87ud29l0a8asy8nlqwd765e5zt4rc2z4mktqulwagn832cuzcjknfyxwzxz2p2kumx6n58tskugny6mrqs7fd23z", +// amount = CardanoAmountJsonV600( +// quantity = 1000000, +// unit = "lovelace" +// ) +// ), +// value = AmountOfMoneyJsonV121("lovelace", "1000000"), +// passphrase = passphrase, +// description = "ADA transfer with metadata", +// metadata = Some(Map("202507022319" -> CardanoMetadataStringJsonV600("Hello Cardano"))) +// ) +// val response600 = makePostRequest(request600, write(cardanoTransactionRequestBody)) +// Then("We should get a 201") +// response600.code should equal(201) +// And("response should contain transaction request") +// val transactionRequest = response600.body.extract[TransactionRequestWithChargeJSON400] +// transactionRequest.status should not be empty +// } +// +// scenario("We will create Cardano transaction request with token - user is logged in", CreateTransactionRequestCardano, VersionOfApi) { +// Entitlement.entitlement.vend.addEntitlement(testBankId, resourceUser1.userId, ApiRole.canCreateAccount.toString()) +// val request = (v6_0_0_Request / "banks" / testBankId / "accounts" / testAccountId ).PUT <@(user1) +// val response = makePutRequest(request, write(putCreateAccountJSONV310)) +// Then("We should get a 201") +// response.code should equal(201) +// +// When("We create a method routing for makePaymentv210 to cardano_vJun2025") +// val cardanoMethodRouting = MethodRoutingCommons( +// methodName = "makePaymentv210", +// connectorName = "cardano_vJun2025", +// isBankIdExactMatch = false, +// bankIdPattern = Some("*"), +// parameters = List() +// ) +// val request310 = (v6_0_0_Request / "management" / "method_routings").POST <@(user1) +// Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanCreateMethodRouting.toString) +// val response310 = makePostRequest(request310, write(cardanoMethodRouting)) +// response310.code should equal(201) +// +// When("We make a request v6.0.0 with token") +// val request600 = (v6_0_0_Request / "banks" / testBankId / "accounts" / testAccountId / Constant.SYSTEM_OWNER_VIEW_ID / "transaction-request-types" / "CARDANO" / "transaction-requests").POST <@(user1) +// val cardanoTransactionRequestBody = TransactionRequestBodyCardanoJsonV600( +// to = CardanoPaymentJsonV600( +// address = "addr_test1qpv3se9ghq87ud29l0a8asy8nlqwd765e5zt4rc2z4mktqulwagn832cuzcjknfyxwzxz2p2kumx6n58tskugny6mrqs7fd23z", +// amount = CardanoAmountJsonV600( +// quantity = 1000000, +// unit = "lovelace" +// ), +// assets = Some(List(CardanoAssetJsonV600( +// policy_id = "ef1954d3a058a96d89d959939aeb5b2948a3df2eb40f9a78d61e3d4f", +// asset_name = "OGCRA", +// quantity = 10 +// ))) +// ), +// value = AmountOfMoneyJsonV121("lovelace", "1000000"), +// passphrase = passphrase, +// description = "Token-only transfer" +// ) +// val response600 = makePostRequest(request600, write(cardanoTransactionRequestBody)) +// Then("We should get a 201") +// response600.code should equal(201) +// And("response should contain transaction request") +// val transactionRequest = response600.body.extract[TransactionRequestWithChargeJSON400] +// transactionRequest.status should not be empty +// } +// +// scenario("We will create Cardano transaction request with token and metadata - user is logged in", CreateTransactionRequestCardano, VersionOfApi) { +// When("We make a request v6.0.0 with token and metadata") +// val request600 = (v6_0_0_Request / "banks" / testBankId / "accounts" / testAccountId / Constant.SYSTEM_OWNER_VIEW_ID / "transaction-request-types" / "CARDANO" / "transaction-requests").POST <@(user1) +// val cardanoTransactionRequestBody = TransactionRequestBodyCardanoJsonV600( +// to = CardanoPaymentJsonV600( +// address = "addr_test1qpv3se9ghq87ud29l0a8asy8nlqwd765e5zt4rc2z4mktqulwagn832cuzcjknfyxwzxz2p2kumx6n58tskugny6mrqs7fd23z", +// amount = CardanoAmountJsonV600( +// quantity = 5000000, +// unit = "lovelace" +// ), +// assets = Some(List(CardanoAssetJsonV600( +// policy_id = "ef1954d3a058a96d89d959939aeb5b2948a3df2eb40f9a78d61e3d4f", +// asset_name = "OGCRA", +// quantity = 10 +// ))) +// ), +// value = AmountOfMoneyJsonV121("lovelace", "1000000"), +// passphrase = passphrase, +// description = "ADA transfer with token and metadata", +// metadata = Some(Map("202507022319" -> CardanoMetadataStringJsonV600("Hello Cardano with Token"))) +// ) +// val response600 = makePostRequest(request600, write(cardanoTransactionRequestBody)) +// Then("We should get a 201") +// response600.code should equal(201) +// And("response should contain transaction request") +// val transactionRequest = response600.body.extract[TransactionRequestWithChargeJSON400] +// transactionRequest.status should not be empty +// } +// +// scenario("We will try to create Cardano transaction request for someone else account - user is logged in", CreateTransactionRequestCardano, VersionOfApi) { +// When("We make a request v6.0.0") +// val request600 = (v6_0_0_Request / "banks" / testBankId / "accounts" / testAccountId / Constant.SYSTEM_OWNER_VIEW_ID / "transaction-request-types" / "CARDANO" / "transaction-requests").POST <@(user2) +// val cardanoTransactionRequestBody = TransactionRequestBodyCardanoJsonV600( +// to = CardanoPaymentJsonV600( +// address = "addr_test1qpv3se9ghq87ud29l0a8asy8nlqwd765e5zt4rc2z4mktqulwagn832cuzcjknfyxwzxz2p2kumx6n58tskugny6mrqs7fd23z", +// amount = CardanoAmountJsonV600( +// quantity = 1000000, +// unit = "lovelace" +// ) +// ), +// value = AmountOfMoneyJsonV121("lovelace", "1000000"), +// passphrase = passphrase, +// description = "Basic ADA transfer" +// ) +// val response600 = makePostRequest(request600, write(cardanoTransactionRequestBody)) +// Then("We should get a 403") +// response600.code should equal(403) +// And("error should be " + UserNoPermissionAccessView) +// response600.body.extract[ErrorMessage].message contains (UserNoPermissionAccessView) shouldBe (true) +// } +// +// scenario("We will try to create Cardano transaction request with invalid address format", CreateTransactionRequestCardano, VersionOfApi) { +// When("We make a request v6.0.0 with invalid address") +// val request600 = (v6_0_0_Request / "banks" / testBankId / "accounts" / testAccountId / Constant.SYSTEM_OWNER_VIEW_ID / "transaction-request-types" / "CARDANO" / "transaction-requests").POST <@(user1) +// val cardanoTransactionRequestBody = TransactionRequestBodyCardanoJsonV600( +// to = CardanoPaymentJsonV600( +// address = "invalid_address_format", +// amount = CardanoAmountJsonV600( +// quantity = 1000000, +// unit = "lovelace" +// ) +// ), +// value = AmountOfMoneyJsonV121("lovelace", "1000000"), +// passphrase = passphrase, +// description = "Basic ADA transfer" +// ) +// val response600 = makePostRequest(request600, write(cardanoTransactionRequestBody)) +// Then("We should get a 400") +// response600.code should equal(400) +// And("error should contain invalid address message") +// response600.body.extract[ErrorMessage].message should include("Cardano address format is invalid") +// } +// +// scenario("We will try to create Cardano transaction request with missing amount", CreateTransactionRequestCardano, VersionOfApi) { +// When("We make a request v6.0.0 with missing amount") +// val request600 = (v6_0_0_Request / "banks" / testBankId / "accounts" / testAccountId / Constant.SYSTEM_OWNER_VIEW_ID / "transaction-request-types" / "CARDANO" / "transaction-requests").POST <@(user1) +// val invalidJson = """ +// { +// "to": { +// "address": "addr_test1qpv3se9ghq87ud29l0a8asy8nlqwd765e5zt4rc2z4mktqulwagn832cuzcjknfyxwzxz2p2kumx6n58tskugny6mrqs7fd23z" +// }, +// "value": { +// "currency": "lovelace", +// "amount": "1000000" +// }, +// "passphrase": "StrongPassword123!", +// "description": "Basic ADA transfer" +// } +// """ +// val response600 = makePostRequest(request600, invalidJson) +// Then("We should get a 400") +// response600.code should equal(400) +// And("error should contain invalid json format message") +// response600.body.extract[ErrorMessage].message should include("InvalidJsonFormat") +// } +// +// scenario("We will try to create Cardano transaction request with negative amount", CreateTransactionRequestCardano, VersionOfApi) { +// When("We make a request v6.0.0 with negative amount") +// val request600 = (v6_0_0_Request / "banks" / testBankId / "accounts" / testAccountId / Constant.SYSTEM_OWNER_VIEW_ID / "transaction-request-types" / "CARDANO" / "transaction-requests").POST <@(user1) +// val cardanoTransactionRequestBody = TransactionRequestBodyCardanoJsonV600( +// to = CardanoPaymentJsonV600( +// address = "addr_test1qpv3se9ghq87ud29l0a8asy8nlqwd765e5zt4rc2z4mktqulwagn832cuzcjknfyxwzxz2p2kumx6n58tskugny6mrqs7fd23z", +// amount = CardanoAmountJsonV600( +// quantity = -1000000, +// unit = "lovelace" +// ) +// ), +// value = AmountOfMoneyJsonV121("lovelace", "1000000"), +// passphrase = passphrase, +// description = "Basic ADA transfer" +// ) +// val response600 = makePostRequest(request600, write(cardanoTransactionRequestBody)) +// Then("We should get a 400") +// response600.code should equal(400) +// And("error should contain invalid amount message") +// response600.body.extract[ErrorMessage].message should include("Cardano amount quantity must be non-negative") +// } +// +// scenario("We will try to create Cardano transaction request with invalid amount unit", CreateTransactionRequestCardano, VersionOfApi) { +// When("We make a request v6.0.0 with invalid amount unit") +// val request600 = (v6_0_0_Request / "banks" / testBankId / "accounts" / testAccountId / Constant.SYSTEM_OWNER_VIEW_ID / "transaction-request-types" / "CARDANO" / "transaction-requests").POST <@(user1) +// val cardanoTransactionRequestBody = TransactionRequestBodyCardanoJsonV600( +// to = CardanoPaymentJsonV600( +// address = "addr_test1qpv3se9ghq87ud29l0a8asy8nlqwd765e5zt4rc2z4mktqulwagn832cuzcjknfyxwzxz2p2kumx6n58tskugny6mrqs7fd23z", +// amount = CardanoAmountJsonV600( +// quantity = 1000000, +// unit = "abc" // Invalid unit, should be "lovelace" +// ) +// ), +// value = AmountOfMoneyJsonV121("lovelace", "1000000"), +// passphrase = passphrase, +// description = "Basic ADA transfer" +// ) +// val response600 = makePostRequest(request600, write(cardanoTransactionRequestBody)) +// Then("We should get a 400") +// response600.code should equal(400) +// And("error should contain invalid unit message") +// response600.body.extract[ErrorMessage].message should include("Cardano amount unit must be 'lovelace'") +// } +// +// scenario("We will try to create Cardano transaction request with zero amount but no assets", CreateTransactionRequestCardano, VersionOfApi) { +// When("We make a request v6.0.0 with zero amount but no assets") +// val request600 = (v6_0_0_Request / "banks" / testBankId / "accounts" / testAccountId / Constant.SYSTEM_OWNER_VIEW_ID / "transaction-request-types" / "CARDANO" / "transaction-requests").POST <@(user1) +// val cardanoTransactionRequestBody = TransactionRequestBodyCardanoJsonV600( +// to = CardanoPaymentJsonV600( +// address = "addr_test1qpv3se9ghq87ud29l0a8asy8nlqwd765e5zt4rc2z4mktqulwagn832cuzcjknfyxwzxz2p2kumx6n58tskugny6mrqs7fd23z", +// amount = CardanoAmountJsonV600( +// quantity = 0, +// unit = "lovelace" +// ) +// ), +// value = AmountOfMoneyJsonV121("lovelace", "0.0"), +// passphrase = passphrase, +// description = "Zero amount without assets" +// ) +// val response600 = makePostRequest(request600, write(cardanoTransactionRequestBody)) +// Then("We should get a 400") +// response600.code should equal(400) +// And("error should contain invalid amount message") +// response600.body.extract[ErrorMessage].message should include("Cardano transfer with zero amount must include assets") +// } +// +// scenario("We will try to create Cardano transaction request with invalid assets", CreateTransactionRequestCardano, VersionOfApi) { +// When("We make a request v6.0.0 with invalid assets") +// val request600 = (v6_0_0_Request / "banks" / testBankId / "accounts" / testAccountId / Constant.SYSTEM_OWNER_VIEW_ID / "transaction-request-types" / "CARDANO" / "transaction-requests").POST <@(user1) +// val cardanoTransactionRequestBody = TransactionRequestBodyCardanoJsonV600( +// to = CardanoPaymentJsonV600( +// address = "addr_test1qpv3se9ghq87ud29l0a8asy8nlqwd765e5zt4rc2z4mktqulwagn832cuzcjknfyxwzxz2p2kumx6n58tskugny6mrqs7fd23z", +// amount = CardanoAmountJsonV600( +// quantity = 0, +// unit = "lovelace" +// ), +// assets = Some(List(CardanoAssetJsonV600( +// policy_id = "", +// asset_name = "", +// quantity = 0 +// ))) +// ), +// value = AmountOfMoneyJsonV121("lovelace", "0.0"), +// passphrase = passphrase, +// description = "Invalid assets" +// ) +// val response600 = makePostRequest(request600, write(cardanoTransactionRequestBody)) +// Then("We should get a 400") +// response600.code should equal(400) +// And("error should contain invalid assets message") +// response600.body.extract[ErrorMessage].message should include("Cardano assets must have valid policy_id and asset_name") +// } +// +// scenario("We will try to create Cardano transaction request with invalid metadata", CreateTransactionRequestCardano, VersionOfApi) { +// When("We make a request v6.0.0 with invalid metadata") +// val request600 = (v6_0_0_Request / "banks" / testBankId / "accounts" / testAccountId / Constant.SYSTEM_OWNER_VIEW_ID / "transaction-request-types" / "CARDANO" / "transaction-requests").POST <@(user1) +// val cardanoTransactionRequestBody = TransactionRequestBodyCardanoJsonV600( +// to = CardanoPaymentJsonV600( +// address = "addr_test1qpv3se9ghq87ud29l0a8asy8nlqwd765e5zt4rc2z4mktqulwagn832cuzcjknfyxwzxz2p2kumx6n58tskugny6mrqs7fd23z", +// amount = CardanoAmountJsonV600( +// quantity = 1000000, +// unit = "lovelace" +// ) +// ), +// value = AmountOfMoneyJsonV121("lovelace", "1000000"), +// passphrase = passphrase, +// description = "ADA transfer with invalid metadata", +// metadata = Some(Map("" -> CardanoMetadataStringJsonV600(""))) +// ) +// val response600 = makePostRequest(request600, write(cardanoTransactionRequestBody)) +// Then("We should get a 400") +// response600.code should equal(400) +// And("error should contain invalid metadata message") +// response600.body.extract[ErrorMessage].message should include("Cardano metadata must have valid structure") +// } } } \ No newline at end of file diff --git a/obp-api/src/test/scala/code/api/v6_0_0/V600ServerSetup.scala b/obp-api/src/test/scala/code/api/v6_0_0/V600ServerSetup.scala index ef28eb975..ae9e71ced 100644 --- a/obp-api/src/test/scala/code/api/v6_0_0/V600ServerSetup.scala +++ b/obp-api/src/test/scala/code/api/v6_0_0/V600ServerSetup.scala @@ -8,6 +8,6 @@ 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" / "v5.1.0" + def v6_0_0_Request: Req = baseRequest / "obp" / "v6.0.0" } \ No newline at end of file From 839304087e59e66402459ac863e561f0dbf0aab9 Mon Sep 17 00:00:00 2001 From: hongwei Date: Thu, 21 Aug 2025 18:24:41 +0200 Subject: [PATCH 22/28] refactor/ enhance isAutoValidate logic in OBPRestHelper to support v4.0.0 and later versions - Updated the isAutoValidate method to automatically support API versions v4.0.0 and later. - Improved version comparison logic to handle version strings more robustly. - Ensured compatibility with existing validation mechanisms while extending functionality. --- .../main/scala/code/api/OBPRestHelper.scala | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/obp-api/src/main/scala/code/api/OBPRestHelper.scala b/obp-api/src/main/scala/code/api/OBPRestHelper.scala index 6a61413eb..009dcb228 100644 --- a/obp-api/src/main/scala/code/api/OBPRestHelper.scala +++ b/obp-api/src/main/scala/code/api/OBPRestHelper.scala @@ -643,8 +643,26 @@ trait OBPRestHelper extends RestHelper with MdcLoggable { 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(OBPAPI6_0_0.version,OBPAPI5_1_0.version,OBPAPI5_0_0.version,OBPAPI4_0_0.version).contains(doc.implementedInApiVersion)) + def isAutoValidate(doc: ResourceDoc): 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 + } + }) } for(route <- routes) { From b07d08029f4bd4b848c575c3a5aeddc030348918 Mon Sep 17 00:00:00 2001 From: hongwei Date: Thu, 21 Aug 2025 18:50:05 +0200 Subject: [PATCH 23/28] refactor/ implement unit tests for isAutoValidate method in OBPRestHelper - Added a new test suite for the isAutoValidate method to cover various scenarios including validation flags and version comparisons. - Ensured comprehensive testing for API versions v4.0.0 and later, including edge cases for malformed version strings. - Improved code structure and readability in the OBPRestHelperTest class. --- .../main/scala/code/api/OBPRestHelper.scala | 47 +++---- .../scala/code/api/OBPRestHelperTest.scala | 132 ++++++++++++++++++ 2 files changed, 155 insertions(+), 24 deletions(-) create mode 100644 obp-api/src/test/scala/code/api/OBPRestHelperTest.scala diff --git a/obp-api/src/main/scala/code/api/OBPRestHelper.scala b/obp-api/src/main/scala/code/api/OBPRestHelper.scala index 009dcb228..d78be8924 100644 --- a/obp-api/src/main/scala/code/api/OBPRestHelper.scala +++ b/obp-api/src/main/scala/code/api/OBPRestHelper.scala @@ -638,33 +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: 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 - } - }) - } - 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) @@ -672,7 +671,7 @@ trait OBPRestHelper extends RestHelper with MdcLoggable { if(resourceDocs.isEmpty) { oauthServe(apiPrefix(route), None) } else { - val (autoValidateDocs, other) = resourceDocs.partition(isAutoValidate) + val (autoValidateDocs, other) = resourceDocs.partition(isAutoValidate(_, autoValidateAll)) // autoValidateAll or doc isAutoValidate, just wrapped to auth check endpoint autoValidateDocs.foreach { doc => val wrappedEndpoint = doc.wrappedWithAuthCheck(route) diff --git a/obp-api/src/test/scala/code/api/OBPRestHelperTest.scala b/obp-api/src/test/scala/code/api/OBPRestHelperTest.scala new file mode 100644 index 000000000..a8bfc06c1 --- /dev/null +++ b/obp-api/src/test/scala/code/api/OBPRestHelperTest.scala @@ -0,0 +1,132 @@ +package code.api + +import code.api.util.APIUtil.{ResourceDoc, EmptyBody} +import code.api.OBPRestHelper +import com.openbankproject.commons.util.{ApiVersion, ScannedApiVersion} +import org.scalatest.{FlatSpec, Matchers, Tag} + +/** + * Unit tests for OBPRestHelper.isAutoValidate method + * + * This test suite covers basic scenarios for the isAutoValidate function: + * - When doc.isValidateEnabled is true + * - When autoValidateAll is false + * - When doc.isValidateDisabled is true + * - When doc.implementedInApiVersion is not ScannedApiVersion + * - Basic version comparison logic + */ +class OBPRestHelperTest extends FlatSpec with Matchers { + + object tag extends Tag("OBPRestHelper") + + // Create a test instance of OBPRestHelper + private val testHelper = new OBPRestHelper { + val version: com.openbankproject.commons.util.ApiVersion = ScannedApiVersion("obp", "OBP", "v4.0.0") + val versionStatus: String = "stable" + } + + // Helper method to create a ResourceDoc with specific validation settings + private def createResourceDoc( + version: ScannedApiVersion, + isValidateEnabled: Boolean = false, + isValidateDisabled: Boolean = false + ): ResourceDoc = { + // Create a minimal ResourceDoc for testing + val doc = new ResourceDoc( + partialFunction = null, // Not used in our tests + implementedInApiVersion = version, + partialFunctionName = "testFunction", + requestVerb = "GET", + requestUrl = "/test", + summary = "Test endpoint", + description = "Test description", + exampleRequestBody = EmptyBody, + successResponseBody = EmptyBody, + errorResponseBodies = List(), + tags = List() + ) + + // Set validation flags using reflection or direct method calls + if (isValidateEnabled) { + doc.enableAutoValidate() + } + if (isValidateDisabled) { + doc.disableAutoValidate() + } + + doc + } + + "isAutoValidate" should "return true when doc.isValidateEnabled is true" taggedAs tag in { + val v4_0_0 = ScannedApiVersion("obp", "OBP", "v4.0.0") + val doc = createResourceDoc(v4_0_0, isValidateEnabled = true) + val result = testHelper.isAutoValidate(doc, autoValidateAll = false) + result shouldBe true + } + + it should "return false when autoValidateAll is false and doc.isValidateEnabled is false" taggedAs tag in { + val v4_0_0 = ScannedApiVersion("obp", "OBP", "v4.0.0") + val doc = createResourceDoc(v4_0_0, isValidateEnabled = false) + val result = testHelper.isAutoValidate(doc, autoValidateAll = false) + result shouldBe false + } + + it should "return false when doc.isValidateDisabled is true" taggedAs tag in { + val v4_0_0 = ScannedApiVersion("obp", "OBP", "v4.0.0") + val doc = createResourceDoc(v4_0_0, isValidateDisabled = true) + val result = testHelper.isAutoValidate(doc, autoValidateAll = true) + result shouldBe false + } + + + + it should "return false for versions before v4.0.0" taggedAs tag in { + val v3_1_0 = ScannedApiVersion("obp", "OBP", "v3.1.0") + val doc = createResourceDoc(v3_1_0) + val result = testHelper.isAutoValidate(doc, autoValidateAll = true) + result shouldBe false + } + + it should "return true for v4.0.0" taggedAs tag in { + val v4_0_0 = ScannedApiVersion("obp", "OBP", "v4.0.0") + val doc = createResourceDoc(v4_0_0) + val result = testHelper.isAutoValidate(doc, autoValidateAll = true) + result shouldBe true + } + + it should "return true for versions after v4.0.0" taggedAs tag in { + val v5_0_0 = ScannedApiVersion("obp", "OBP", "v5.0.0") + val doc = createResourceDoc(v5_0_0) + val result = testHelper.isAutoValidate(doc, autoValidateAll = true) + result shouldBe true + } + + it should "return true for v4.1.0 (major=4, minor=1)" taggedAs tag in { + val v4_1_0 = ScannedApiVersion("obp", "OBP", "v4.1.0") + val doc = createResourceDoc(v4_1_0) + val result = testHelper.isAutoValidate(doc, autoValidateAll = true) + result shouldBe true + } + + it should "return false for malformed version strings" taggedAs tag in { + val malformedVersion = ScannedApiVersion("obp", "OBP", "v4") // Missing minor version + val doc = createResourceDoc(malformedVersion) + val result = testHelper.isAutoValidate(doc, autoValidateAll = true) + result shouldBe false + } + + it should "prioritize isValidateEnabled over autoValidateAll" taggedAs tag in { + val v3_1_0 = ScannedApiVersion("obp", "OBP", "v3.1.0") // v3.1.0 normally wouldn't auto-validate + val doc = createResourceDoc(v3_1_0, isValidateEnabled = true) + val result = testHelper.isAutoValidate(doc, autoValidateAll = true) + result shouldBe true // Should be true because isValidateEnabled is true + } + + it should "prioritize isValidateDisabled over autoValidateAll" taggedAs tag in { + val v4_0_0 = ScannedApiVersion("obp", "OBP", "v4.0.0") // v4.0.0 normally would auto-validate + val doc = createResourceDoc(v4_0_0, isValidateDisabled = true) + val result = testHelper.isAutoValidate(doc, autoValidateAll = true) + result shouldBe false // Should be false because isValidateDisabled is true + } +} + From e5f2f785ef5b89b0bfccf53e4714f93a54359b8a Mon Sep 17 00:00:00 2001 From: hongwei Date: Mon, 25 Aug 2025 09:59:04 +0200 Subject: [PATCH 24/28] Refactor/ Revert MappedTransactionRequest fields length --- .../MappedTransactionRequestProvider.scala | 42 +++++++++---------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/obp-api/src/main/scala/code/transactionrequests/MappedTransactionRequestProvider.scala b/obp-api/src/main/scala/code/transactionrequests/MappedTransactionRequestProvider.scala index 9a763192c..73b69db43 100644 --- a/obp-api/src/main/scala/code/transactionrequests/MappedTransactionRequestProvider.scala +++ b/obp-api/src/main/scala/code/transactionrequests/MappedTransactionRequestProvider.scala @@ -234,24 +234,24 @@ class MappedTransactionRequest extends LongKeyedMapper[MappedTransactionRequest] //transaction request fields: object mTransactionRequestId extends UUIDString(this) - object mType extends MappedString(this, 2000) + object mType extends MappedString(this, 32) //transaction fields: object mTransactionIDs extends MappedString(this, 2000) - object mStatus extends MappedString(this, 2000) + object mStatus extends MappedString(this, 32) object mStartDate extends MappedDate(this) object mEndDate extends MappedDate(this) - object mChallenge_Id extends MappedString(this, 2000) + object mChallenge_Id extends MappedString(this, 64) object mChallenge_AllowedAttempts extends MappedInt(this) - object mChallenge_ChallengeType extends MappedString(this, 2000) - object mCharge_Summary extends MappedString(this, 2000) - object mCharge_Amount extends MappedString(this, 2000) - object mCharge_Currency extends MappedString(this, 2000) - object mcharge_Policy extends MappedString(this, 2000) + 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_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, 2000) - object mBody_Value_Amount extends MappedString(this, 2000) + object mBody_Value_Currency extends MappedString(this, 3) + 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) // Note:this need to be a longer string, defaults is 2000, maybe not enough @@ -268,28 +268,28 @@ class MappedTransactionRequest extends LongKeyedMapper[MappedTransactionRequest] object mTo_AccountId extends AccountIdString(this) //toCounterparty fields - object mName extends MappedString(this, 2000) + object mName extends MappedString(this, 64) object mThisBankId extends UUIDString(this) object mThisAccountId extends AccountIdString(this) object mThisViewId extends UUIDString(this) object mCounterpartyId extends UUIDString(this) - object mOtherAccountRoutingScheme extends MappedString(this, 2000) // TODO Add class for Scheme and Address - object mOtherAccountRoutingAddress extends MappedString(this, 2000) - object mOtherBankRoutingScheme extends MappedString(this, 2000) - object mOtherBankRoutingAddress extends MappedString(this, 2000) + object mOtherAccountRoutingScheme extends MappedString(this, 32) // TODO Add class for Scheme and Address + object mOtherAccountRoutingAddress extends MappedString(this, 64) + object mOtherBankRoutingScheme extends MappedString(this, 32) + object mOtherBankRoutingAddress extends MappedString(this, 64) object mIsBeneficiary extends MappedBoolean(this) //Here are for Berlin Group V1.3 object mPaymentStartDate extends MappedDate(this) //BGv1.3 Open API Document example value: "startDate":"2024-08-12" object mPaymentEndDate extends MappedDate(this) //BGv1.3 Open API Document example value: "startDate":"2025-08-01" - object mPaymentExecutionRule extends MappedString(this, 2000) //BGv1.3 Open API Document example value: "executionRule":"preceding" - object mPaymentFrequency extends MappedString(this, 2000) //BGv1.3 Open API Document example value: "frequency":"Monthly", - object mPaymentDayOfExecution extends MappedString(this, 2000)//BGv1.3 Open API Document example value: "dayOfExecution":"01" + object mPaymentExecutionRule extends MappedString(this, 64) //BGv1.3 Open API Document example value: "executionRule":"preceding" + object mPaymentFrequency extends MappedString(this, 64) //BGv1.3 Open API Document example value: "frequency":"Monthly", + object mPaymentDayOfExecution extends MappedString(this, 64)//BGv1.3 Open API Document example value: "dayOfExecution":"01" - object mConsentReferenceId extends MappedString(this, 2000) + object mConsentReferenceId extends MappedString(this, 64) - object mApiStandard extends MappedString(this, 2000) - object mApiVersion extends MappedString(this, 2000) + object mApiStandard extends MappedString(this, 50) + object mApiVersion extends MappedString(this, 50) def updateStatus(newStatus: String) = { mStatus.set(newStatus) From 8e41be8cf74d291369c43b45e52c645bb1845109 Mon Sep 17 00:00:00 2001 From: hongwei Date: Mon, 25 Aug 2025 11:15:00 +0200 Subject: [PATCH 25/28] refactor/Add migration for altering MappedTransactionRequest fields length - Introduced `alterMappedTransactionRequestFieldsLengthMigration` method to handle migration logic. - Created `MigrationOfMappedTransactionRequestFieldsLength` object to define SQL alterations for currency and account routing fields. - Updated `MappedTransactionRequest` fields to support longer lengths for currency and account identifiers. --- .../code/api/util/migration/Migration.scala | 14 ++++ ...MappedTransactionRequestFieldsLength.scala | 74 +++++++++++++++++++ .../MappedTransactionRequestProvider.scala | 8 +- 3 files changed, 92 insertions(+), 4 deletions(-) create mode 100644 obp-api/src/main/scala/code/api/util/migration/MigrationOfMappedTransactionRequestFieldsLength.scala diff --git a/obp-api/src/main/scala/code/api/util/migration/Migration.scala b/obp-api/src/main/scala/code/api/util/migration/Migration.scala index 7da81d82c..166a54342 100644 --- a/obp-api/src/main/scala/code/api/util/migration/Migration.scala +++ b/obp-api/src/main/scala/code/api/util/migration/Migration.scala @@ -86,6 +86,7 @@ object Migration extends MdcLoggable { addFastFirehoseAccountsView(startedBeforeSchemifier) addFastFirehoseAccountsMaterializedView(startedBeforeSchemifier) alterUserAuthContextColumnKeyAndValueLength(startedBeforeSchemifier) + alterMappedTransactionRequestFieldsLengthMigration(startedBeforeSchemifier) dropIndexAtColumnUsernameAtTableAuthUser(startedBeforeSchemifier) dropIndexAtUserAuthContext() alterWebhookColumnUrlLength() @@ -403,6 +404,19 @@ object Migration extends MdcLoggable { } } } + + private def alterMappedTransactionRequestFieldsLengthMigration(startedBeforeSchemifier: Boolean): Boolean = { + if(startedBeforeSchemifier == true) { + logger.warn(s"Migration.database.alterMappedTransactionRequestFieldsLengthMigration(true) cannot be run before Schemifier.") + true + } else { + val name = nameOf(alterMappedTransactionRequestFieldsLengthMigration(startedBeforeSchemifier)) + runOnce(name) { + MigrationOfMappedTransactionRequestFieldsLength.alterMappedTransactionRequestFieldsLength(name) + } + } + } + private def dropIndexAtColumnUsernameAtTableAuthUser(startedBeforeSchemifier: Boolean): Boolean = { if(startedBeforeSchemifier == true) { logger.warn(s"Migration.database.dropIndexAtColumnUsernameAtTableAuthUser(true) cannot be run before Schemifier.") diff --git a/obp-api/src/main/scala/code/api/util/migration/MigrationOfMappedTransactionRequestFieldsLength.scala b/obp-api/src/main/scala/code/api/util/migration/MigrationOfMappedTransactionRequestFieldsLength.scala new file mode 100644 index 000000000..252f066fb --- /dev/null +++ b/obp-api/src/main/scala/code/api/util/migration/MigrationOfMappedTransactionRequestFieldsLength.scala @@ -0,0 +1,74 @@ +package code.api.util.migration + +import code.api.util.APIUtil +import code.api.util.migration.Migration.{DbFunction, saveLog} +import code.transactionrequests.MappedTransactionRequest +import net.liftweb.common.Full +import net.liftweb.mapper.Schemifier + +import java.time.format.DateTimeFormatter +import java.time.{ZoneId, ZonedDateTime} + +object MigrationOfMappedTransactionRequestFieldsLength { + + val oneDayAgo = ZonedDateTime.now(ZoneId.of("UTC")).minusDays(1) + val oneYearInFuture = ZonedDateTime.now(ZoneId.of("UTC")).plusYears(1) + val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm'Z'") + + def alterMappedTransactionRequestFieldsLength(name: String): Boolean = { + DbFunction.tableExists(MappedTransactionRequest) match { + case true => + val startDate = System.currentTimeMillis() + val commitId: String = APIUtil.gitCommit + var isSuccessful = false + + val executedSql = + DbFunction.maybeWrite(true, Schemifier.infoF _) { + APIUtil.getPropsValue("db.driver") match { + case Full(dbDriver) if dbDriver.contains("com.microsoft.sqlserver.jdbc.SQLServerDriver") => + () => + s""" + |-- Currency fields: support longer currency names (e.g., "lovelace") + |ALTER TABLE MappedTransactionRequest ALTER COLUMN mCharge_Currency varchar(16); + |ALTER TABLE MappedTransactionRequest ALTER COLUMN mBody_Value_Currency varchar(16); + | + |-- Account routing fields: support Cardano addresses (108 characters) + |ALTER TABLE MappedTransactionRequest ALTER COLUMN mTo_AccountId varchar(128); + |ALTER TABLE MappedTransactionRequest ALTER COLUMN mOtherAccountRoutingAddress varchar(128); + |""".stripMargin + case _ => + () => + """ + |-- Currency fields: support longer currency names (e.g., "lovelace") + |ALTER TABLE MappedTransactionRequest ALTER COLUMN mCharge_Currency TYPE varchar(16); + |ALTER TABLE MappedTransactionRequest ALTER COLUMN mBody_Value_Currency TYPE varchar(16); + | + |-- Account routing fields: support Cardano addresses (108 characters) + |ALTER TABLE MappedTransactionRequest ALTER COLUMN mTo_AccountId TYPE varchar(128); + |ALTER TABLE MappedTransactionRequest ALTER COLUMN mOtherAccountRoutingAddress TYPE varchar(128); + |""".stripMargin + } + } + + val endDate = System.currentTimeMillis() + val comment: String = + s"""Executed SQL: + |$executedSql + |""".stripMargin + isSuccessful = true + saveLog(name, commitId, isSuccessful, startDate, endDate, comment) + isSuccessful + + case false => + val startDate = System.currentTimeMillis() + val commitId: String = APIUtil.gitCommit + val isSuccessful = false + val endDate = System.currentTimeMillis() + val comment: String = + s"""${MappedTransactionRequest._dbTableNameLC} table does not exist""".stripMargin + saveLog(name, commitId, isSuccessful, startDate, endDate, comment) + isSuccessful + } + } +} + diff --git a/obp-api/src/main/scala/code/transactionrequests/MappedTransactionRequestProvider.scala b/obp-api/src/main/scala/code/transactionrequests/MappedTransactionRequestProvider.scala index 73b69db43..824b1376a 100644 --- a/obp-api/src/main/scala/code/transactionrequests/MappedTransactionRequestProvider.scala +++ b/obp-api/src/main/scala/code/transactionrequests/MappedTransactionRequestProvider.scala @@ -246,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) @@ -265,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) @@ -274,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) From b4bfc70e65e44ee8e037d294fece9085839b3a78 Mon Sep 17 00:00:00 2001 From: hongwei Date: Mon, 25 Aug 2025 11:15:42 +0200 Subject: [PATCH 26/28] refactor/Enhance CardanoTransactionRequestTest with additional imports and cleanup --- .../api/v6_0_0/CardanoTransactionRequestTest.scala | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/obp-api/src/test/scala/code/api/v6_0_0/CardanoTransactionRequestTest.scala b/obp-api/src/test/scala/code/api/v6_0_0/CardanoTransactionRequestTest.scala index f135126f0..7348a3821 100644 --- a/obp-api/src/test/scala/code/api/v6_0_0/CardanoTransactionRequestTest.scala +++ b/obp-api/src/test/scala/code/api/v6_0_0/CardanoTransactionRequestTest.scala @@ -27,13 +27,22 @@ 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.v6_0_0.OBPAPI6_0_0.Implementations6_0_0 +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 { @@ -105,7 +114,7 @@ class CardanoTransactionRequestTest extends V600ServerSetup { // 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( From 196cc8cdcb44233b467216bb419cacba93c51dfd Mon Sep 17 00:00:00 2001 From: hongwei Date: Mon, 25 Aug 2025 11:30:28 +0200 Subject: [PATCH 27/28] refactor/Remove commented-out Cardano transaction handling code in LocalMappedConnectorInternal.scala --- .../LocalMappedConnectorInternal.scala | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/obp-api/src/main/scala/code/bankconnectors/LocalMappedConnectorInternal.scala b/obp-api/src/main/scala/code/bankconnectors/LocalMappedConnectorInternal.scala index 4849942e9..e25d12d75 100644 --- a/obp-api/src/main/scala/code/bankconnectors/LocalMappedConnectorInternal.scala +++ b/obp-api/src/main/scala/code/bankconnectors/LocalMappedConnectorInternal.scala @@ -755,20 +755,6 @@ object LocalMappedConnectorInternal extends MdcLoggable { } yield{ (cardFromCbs.account, callContext) } -// case CARDANO => -// for{ -// transactionRequestBodyCardanoJson <- NewStyle.function.tryons(s"${InvalidJsonFormat}, it should be $CARD json format", 400, callContext) { -// json.extract[TransactionRequestBodyCardanoJsonV600] -// } -// (account, callContext) <- NewStyle.function.getBankAccountByRouting( -// None, //No need for the bankId, only to address is enough -// "cardano_wallet_id", -// transactionRequestBodyCardanoJson.to.address, -// callContext -// ) -// } yield -// (account, callContext) - case _ => NewStyle.function.getBankAccount(bankId,accountId, callContext) } _ <- NewStyle.function.isEnabledTransactionRequests(callContext) From 5e1c6107c972ba61eaead549a671384094870f11 Mon Sep 17 00:00:00 2001 From: hongwei Date: Mon, 25 Aug 2025 18:15:34 +0200 Subject: [PATCH 28/28] refactor/Update Cardano currency names in ISOCurrencyCodes.xml for clarity and consistency --- obp-api/src/main/webapp/media/xml/ISOCurrencyCodes.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/obp-api/src/main/webapp/media/xml/ISOCurrencyCodes.xml b/obp-api/src/main/webapp/media/xml/ISOCurrencyCodes.xml index a6fece789..eec889297 100644 --- a/obp-api/src/main/webapp/media/xml/ISOCurrencyCodes.xml +++ b/obp-api/src/main/webapp/media/xml/ISOCurrencyCodes.xml @@ -1947,7 +1947,7 @@ - ZZ12_Cardano + Cardano Cardano ada null @@ -1955,7 +1955,7 @@ - ZZ12_Cardano_Lovelace + Cardano_Lovelace Lovelace lovelace null