From 80d595842abda70ef13c4d80a48c1fce32032001 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Thu, 10 Apr 2025 17:42:38 +0200 Subject: [PATCH 01/10] feature/Add error message OBP-20088: An access must be requested. --- .../group/v1_3/AccountInformationServiceAISApi.scala | 9 +++++++-- .../src/main/scala/code/api/util/BerlinGroupError.scala | 1 + obp-api/src/main/scala/code/api/util/ErrorMessages.scala | 4 +++- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/obp-api/src/main/scala/code/api/berlin/group/v1_3/AccountInformationServiceAISApi.scala b/obp-api/src/main/scala/code/api/berlin/group/v1_3/AccountInformationServiceAISApi.scala index cc2a81b241..a3f51da3f8 100644 --- a/obp-api/src/main/scala/code/api/berlin/group/v1_3/AccountInformationServiceAISApi.scala +++ b/obp-api/src/main/scala/code/api/berlin/group/v1_3/AccountInformationServiceAISApi.scala @@ -159,14 +159,19 @@ recurringIndicator: consentJson <- NewStyle.function.tryons(failMsg, 400, callContext) { json.extract[PostConsentJson] } + _ <- Helper.booleanToFuture(failMsg = BerlinGroupConsentAccessIsEmpty, cc=callContext) { + consentJson.access.accounts.isDefined || + consentJson.access.balances.isDefined || + consentJson.access.transactions.isDefined + } upperLimit = APIUtil.getPropsAsIntValue("berlin_group_frequency_per_day_upper_limit", 4) _ <- Helper.booleanToFuture(failMsg = FrequencyPerDayError, cc=callContext) { consentJson.frequencyPerDay > 0 && consentJson.frequencyPerDay <= upperLimit } _ <- Helper.booleanToFuture(failMsg = FrequencyPerDayMustBeOneError, cc=callContext) { - consentJson.recurringIndicator == true || - (consentJson.recurringIndicator == false && consentJson.frequencyPerDay == 1) + consentJson.recurringIndicator || + !consentJson.recurringIndicator && consentJson.frequencyPerDay == 1 } failMsg = BgSpecValidation.getErrorMessage(consentJson.validUntil) diff --git a/obp-api/src/main/scala/code/api/util/BerlinGroupError.scala b/obp-api/src/main/scala/code/api/util/BerlinGroupError.scala index 320f6ec17b..e390659073 100644 --- a/obp-api/src/main/scala/code/api/util/BerlinGroupError.scala +++ b/obp-api/src/main/scala/code/api/util/BerlinGroupError.scala @@ -90,6 +90,7 @@ object BerlinGroupError { case "400" if message.contains("OBP-20063") => "FORMAT_ERROR" case "400" if message.contains("OBP-20252") => "FORMAT_ERROR" case "400" if message.contains("OBP-20251") => "FORMAT_ERROR" + case "400" if message.contains("OBP-20088") => "FORMAT_ERROR" case "429" if message.contains("OBP-10018") => "ACCESS_EXCEEDED" case _ => code 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 8bf52372f5..48afde5e0c 100644 --- a/obp-api/src/main/scala/code/api/util/ErrorMessages.scala +++ b/obp-api/src/main/scala/code/api/util/ErrorMessages.scala @@ -242,6 +242,8 @@ object ErrorMessages { val UserLacksPermissionCanRevokeAccessToCustomViewForTargetAccount = s"OBP-20087: The current source view.can_revoke_access_to_custom_views is false." + + val BerlinGroupConsentAccessIsEmpty = s"OBP-20088: An access must be requested." val UserNotSuperAdminOrMissRole = "OBP-20101: Current User is not super admin or is missing entitlements:" val CannotGetOrCreateUser = "OBP-20102: Cannot get or create user." @@ -280,7 +282,7 @@ object ErrorMessages { val X509ActionIsNotAllowed = "OBP-20307: PEM Encoded Certificate does not provide the proper role for the action has been taken." val X509ThereAreNoPsd2Roles = "OBP-20308: PEM Encoded Certificate does not contain PSD2 roles." val X509CannotGetPublicKey = "OBP-20309: Public key cannot be found in the PEM Encoded Certificate." - val X509PublicKeyCannotVerify = "OBP-20310: Certificate's public key cannot be used to verify signed request." + val X509PublicKeyCannotVerify = "OBP-20310: The signed request cannot be verified by certificate's public key." val X509PublicKeyCannotBeValidated = "OBP-20312: Certificate's public key cannot be validated." val X509RequestIsNotSigned = "OBP-20311: The Request is not signed." From 546323c98e6c8be59a0e931ad6916454964ee811 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Fri, 11 Apr 2025 15:36:18 +0200 Subject: [PATCH 02/10] feature/Tweak json response of Read transaction list of an account --- .../berlin/group/v1_3/BgSpecValidation.scala | 8 ++++++++ .../v1_3/JSONFactory_BERLIN_GROUP_1_3.scala | 18 ++++++++++-------- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/obp-api/src/main/scala/code/api/berlin/group/v1_3/BgSpecValidation.scala b/obp-api/src/main/scala/code/api/berlin/group/v1_3/BgSpecValidation.scala index 363cbce1c8..e480710f7b 100644 --- a/obp-api/src/main/scala/code/api/berlin/group/v1_3/BgSpecValidation.scala +++ b/obp-api/src/main/scala/code/api/berlin/group/v1_3/BgSpecValidation.scala @@ -45,6 +45,14 @@ object BgSpecValidation { } } + def formatToISODate(date: Date): String = { + if (date == null) "" + else { + val localDate: LocalDate = date.toInstant.atZone(ZoneId.systemDefault()).toLocalDate + localDate.format(DateTimeFormatter.ISO_LOCAL_DATE) + } + } + // Example usage def main(args: Array[String]): Unit = { val testDates = Seq( diff --git a/obp-api/src/main/scala/code/api/berlin/group/v1_3/JSONFactory_BERLIN_GROUP_1_3.scala b/obp-api/src/main/scala/code/api/berlin/group/v1_3/JSONFactory_BERLIN_GROUP_1_3.scala index 913cf24aec..8013b4eceb 100644 --- a/obp-api/src/main/scala/code/api/berlin/group/v1_3/JSONFactory_BERLIN_GROUP_1_3.scala +++ b/obp-api/src/main/scala/code/api/berlin/group/v1_3/JSONFactory_BERLIN_GROUP_1_3.scala @@ -136,14 +136,15 @@ object JSONFactory_BERLIN_GROUP_1_3 extends CustomJsonFormats { ) case class CreditorAccountJson( iban: String, + currency : Option[String] = None, ) case class TransactionJsonV13( transactionId: String, creditorName: String, creditorAccount: CreditorAccountJson, transactionAmount: AmountOfMoneyV13, - bookingDate: Date, - valueDate: Date, + bookingDate: String, + valueDate: String, remittanceInformationUnstructured: String, ) case class SingleTransactionJsonV13( @@ -415,9 +416,9 @@ object JSONFactory_BERLIN_GROUP_1_3 extends CustomJsonFormats { transactionId = transaction.id.value, creditorName = creditorName, creditorAccount = creditorAccount, - transactionAmount = AmountOfMoneyV13(APIUtil.stringOptionOrNull(transaction.currency), transaction.amount.get.toString()), - bookingDate = bookingDate, - valueDate = valueDate, + transactionAmount = AmountOfMoneyV13(APIUtil.stringOptionOrNull(transaction.currency), transaction.amount.get.toString().trim.stripPrefix("-")), + bookingDate = BgSpecValidation.formatToISODate(bookingDate) , + valueDate = BgSpecValidation.formatToISODate(valueDate), remittanceInformationUnstructured = APIUtil.stringOptionOrNull(transaction.description) ) } @@ -450,9 +451,9 @@ object JSONFactory_BERLIN_GROUP_1_3 extends CustomJsonFormats { transactionId = transactionRequest.id.value, creditorName = creditorName, creditorAccount = creditorAccount, - transactionAmount = AmountOfMoneyV13(transactionRequest.charge.value.currency, transactionRequest.charge.value.amount), - bookingDate = transactionRequest.start_date, - valueDate = transactionRequest.end_date, + transactionAmount = AmountOfMoneyV13(transactionRequest.charge.value.currency, transactionRequest.charge.value.amount.trim.stripPrefix("-")), + bookingDate = BgSpecValidation.formatToISODate(transactionRequest.start_date), + valueDate = BgSpecValidation.formatToISODate(transactionRequest.end_date), remittanceInformationUnstructured = remittanceInformationUnstructured ) } @@ -463,6 +464,7 @@ object JSONFactory_BERLIN_GROUP_1_3 extends CustomJsonFormats { val creditorAccount = CreditorAccountJson( iban = iban, + currency = Some(bankAccount.currency) ) TransactionsJsonV13( FromAccount( From 502ca1dfc4d840b1b61ca30291e262349617d19a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Mon, 14 Apr 2025 18:33:30 +0200 Subject: [PATCH 03/10] feature/set consent status to 'EXPIRED' when consents reach end of life via background job --- .../resources/props/sample.props.template | 4 ++ .../scala/code/api/v3_1_0/APIMethods310.scala | 4 ++ .../scala/code/api/v5_0_0/APIMethods500.scala | 4 ++ .../scala/code/api/v5_1_0/APIMethods510.scala | 5 ++- .../scala/code/consent/ConsentProvider.scala | 3 +- .../scala/code/consent/MappedConsent.scala | 16 ++++++- .../code/scheduler/ConsentScheduler.scala | 43 +++++++++++++++++-- obp-api/src/main/scala/code/util/Helper.scala | 19 ++++++++ 8 files changed, 92 insertions(+), 6 deletions(-) diff --git a/obp-api/src/main/resources/props/sample.props.template b/obp-api/src/main/resources/props/sample.props.template index 6dcc829bf0..c4fc55a054 100644 --- a/obp-api/src/main/resources/props/sample.props.template +++ b/obp-api/src/main/resources/props/sample.props.template @@ -174,6 +174,10 @@ jwt.use.ssl=false # berlin_group_expired_consents_interval_in_seconds = +## Expire OBP consents with status "ACCEPTED" +# obp_expired_consents_interval_in_seconds = + + ## Enable writing API metrics (which APIs are called) to RDBMS write_metrics=true ## Enable writing connector metrics (which methods are called)to RDBMS diff --git a/obp-api/src/main/scala/code/api/v3_1_0/APIMethods310.scala b/obp-api/src/main/scala/code/api/v3_1_0/APIMethods310.scala index 019bf75dcf..7902050e8c 100644 --- a/obp-api/src/main/scala/code/api/v3_1_0/APIMethods310.scala +++ b/obp-api/src/main/scala/code/api/v3_1_0/APIMethods310.scala @@ -3580,6 +3580,10 @@ trait APIMethods310 { _ <- Future(Consents.consentProvider.vend.setJsonWebToken(createdConsent.consentId, consentJWT)) map { i => connectorEmptyResponse(i, callContext) } + validUntil = Helper.calculateValidTo(consentJson.valid_from, consentJson.time_to_live.getOrElse(3600)) + _ <- Future(Consents.consentProvider.vend.setValidUntil(createdConsent.consentId, validUntil)) map { + i => connectorEmptyResponse(i, callContext) + } //we need to check `skip_consent_sca_for_consumer_id_pairs` props, to see if we really need the SCA flow. //this is from callContext grantorConsumerId = callContext.map(_.consumer.toOption.map(_.consumerId.get)).flatten.getOrElse("Unknown") diff --git a/obp-api/src/main/scala/code/api/v5_0_0/APIMethods500.scala b/obp-api/src/main/scala/code/api/v5_0_0/APIMethods500.scala index 3b962d2ef6..700963671f 100644 --- a/obp-api/src/main/scala/code/api/v5_0_0/APIMethods500.scala +++ b/obp-api/src/main/scala/code/api/v5_0_0/APIMethods500.scala @@ -1234,6 +1234,10 @@ trait APIMethods500 { _ <- Future(Consents.consentProvider.vend.setJsonWebToken(createdConsent.consentId, consentJWT)) map { i => connectorEmptyResponse(i, callContext) } + validUntil = Helper.calculateValidTo(postConsentBodyCommonJson.valid_from, postConsentBodyCommonJson.time_to_live.getOrElse(3600)) + _ <- Future(Consents.consentProvider.vend.setValidUntil(createdConsent.consentId, validUntil)) map { + i => connectorEmptyResponse(i, callContext) + } //we need to check `skip_consent_sca_for_consumer_id_pairs` props, to see if we really need the SCA flow. //this is from callContext grantorConsumerId = callContext.map(_.consumer.toOption.map(_.consumerId.get)).flatten.getOrElse("Unknown") 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 75bdb1803a..bdafcf55f3 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 @@ -2090,7 +2090,10 @@ trait APIMethods510 { _ <- Future(Consents.consentProvider.vend.setJsonWebToken(createdConsent.consentId, consentJWT)) map { i => connectorEmptyResponse(i, callContext) } - + validUntil = Helper.calculateValidTo(consentJson.valid_from, consentJson.time_to_live.getOrElse(3600)) + _ <- Future(Consents.consentProvider.vend.setValidUntil(createdConsent.consentId, validUntil)) map { + i => connectorEmptyResponse(i, callContext) + } //we need to check `skip_consent_sca_for_consumer_id_pairs` props, to see if we really need the SCA flow. //this is from callContext grantorConsumerId = callContext.map(_.consumer.toOption.map(_.consumerId.get)).flatten.getOrElse("Unknown") diff --git a/obp-api/src/main/scala/code/consent/ConsentProvider.scala b/obp-api/src/main/scala/code/consent/ConsentProvider.scala index 158cd1056c..9f5ae7a3e2 100644 --- a/obp-api/src/main/scala/code/consent/ConsentProvider.scala +++ b/obp-api/src/main/scala/code/consent/ConsentProvider.scala @@ -25,6 +25,7 @@ trait ConsentProvider { def getConsentsByUser(userId: String): List[MappedConsent] def createObpConsent(user: User, challengeAnswer: String, consentRequestId:Option[String], consumer: Option[Consumer] = None): Box[MappedConsent] def setJsonWebToken(consentId: String, jwt: String): Box[MappedConsent] + def setValidUntil(consentId: String, validUntil: Date): Box[MappedConsent] def revoke(consentId: String): Box[MappedConsent] def revokeBerlinGroupConsent(consentId: String): Box[MappedConsent] def checkAnswer(consentId: String, challenge: String): Box[MappedConsent] @@ -190,7 +191,7 @@ trait ConsentTrait { object ConsentStatus extends Enumeration { type ConsentStatus = Value - val INITIATED, ACCEPTED, REJECTED, rejected, REVOKED, + val INITIATED, ACCEPTED, REJECTED, rejected, REVOKED, EXPIRED, // The following one only exist in case of BerlinGroup received, valid, revokedByPsu, expired, terminatedByTpp, //these added for UK Open Banking diff --git a/obp-api/src/main/scala/code/consent/MappedConsent.scala b/obp-api/src/main/scala/code/consent/MappedConsent.scala index 5a9ca2dae2..2b10dba672 100644 --- a/obp-api/src/main/scala/code/consent/MappedConsent.scala +++ b/obp-api/src/main/scala/code/consent/MappedConsent.scala @@ -213,7 +213,21 @@ object MappedConsentProvider extends ConsentProvider { case _ => Failure(ErrorMessages.UnknownError) } - } + } + override def setValidUntil(consentId: String, validUntil: Date): Box[MappedConsent] = { + MappedConsent.find(By(MappedConsent.mConsentId, consentId)) match { + case Full(consent) => + tryo(consent + .mValidUntil(validUntil) + .saveMe()) + case Empty => + Empty ?~! ErrorMessages.ConsentNotFound + case Failure(msg, _, _) => + Failure(msg) + case _ => + Failure(ErrorMessages.UnknownError) + } + } override def revoke(consentId: String): Box[MappedConsent] = { MappedConsent.find(By(MappedConsent.mConsentId, consentId)) match { case Full(consent) if consent.status == ConsentStatus.REVOKED.toString => diff --git a/obp-api/src/main/scala/code/scheduler/ConsentScheduler.scala b/obp-api/src/main/scala/code/scheduler/ConsentScheduler.scala index f3079ca0ca..f1f46f88f9 100644 --- a/obp-api/src/main/scala/code/scheduler/ConsentScheduler.scala +++ b/obp-api/src/main/scala/code/scheduler/ConsentScheduler.scala @@ -4,7 +4,7 @@ import code.actorsystem.ObpLookupSystem import code.api.util.APIUtil import code.consent.{ConsentStatus, MappedConsent} import code.util.Helper.MdcLoggable -import com.openbankproject.commons.util.ApiVersion +import com.openbankproject.commons.util.{ApiStandards, ApiVersion} import net.liftweb.common.Full import net.liftweb.mapper.{By, By_<} @@ -22,22 +22,33 @@ object ConsentScheduler extends MdcLoggable { // Starts multiple scheduled tasks with different intervals def startAll(): Unit = { + var initialDelay = 0 + // Berlin Group APIUtil.getPropsAsIntValue("berlin_group_outdated_consents_interval_in_seconds") match { case Full(interval) if interval > 0 => val time = APIUtil.getPropsAsIntValue("berlin_group_outdated_consents_time_in_seconds", 300) startTask(interval = interval, () => unfinishedBerlinGroupConsents(time)) // Runs periodically + initialDelay = initialDelay + 10 case _ => logger.warn("|---> Skipping unfinishedBerlinGroupConsents task: berlin_group_outdated_consents_interval_in_seconds not set or invalid") } APIUtil.getPropsAsIntValue("berlin_group_expired_consents_interval_in_seconds") match { case Full(interval) if interval > 0 => - startTask(interval = interval, () => expiredBerlinGroupConsents(), 10) // Delay for 10 seconds + startTask(interval = interval, () => expiredBerlinGroupConsents(), initialDelay) // Delay for 10 seconds + initialDelay = initialDelay + 10 case _ => logger.warn("|---> Skipping expiredBerlinGroupConsents task: berlin_group_expired_consents_interval_in_seconds not set or invalid") } - + // Open Bank Project + APIUtil.getPropsAsIntValue("obp_expired_consents_interval_in_seconds") match { + case Full(interval) if interval > 0 => + startTask(interval = interval, () => expiredObpConsents(), initialDelay) // Delay for 10 seconds + initialDelay = initialDelay + 10 + case _ => + logger.warn("|---> Skipping expiredObpConsents task: obp_expired_consents_interval_in_seconds not set or invalid") + } } // Generic method to schedule a task @@ -110,5 +121,31 @@ object ConsentScheduler extends MdcLoggable { case Success(_) => logger.debug("|---> Task executed successfully") } } + private def expiredObpConsents(): Unit = { + Try { + logger.debug("|---> Checking for expired OBP consents...") + + val expiredConsents = MappedConsent.findAll( + By(MappedConsent.mStatus, ConsentStatus.ACCEPTED.toString), + By(MappedConsent.mApiStandard, ApiStandards.obp.toString), + By_<(MappedConsent.mValidUntil, new Date()) + ) + + logger.debug(s"|---> Found ${expiredConsents.size} expired consents") + + expiredConsents.foreach { consent => + Try { + consent.mStatus(ConsentStatus.EXPIRED.toString).save + logger.warn(s"|---> Changed status to ${ConsentStatus.EXPIRED.toString} for consent ID: ${consent.id}") + } match { + case Failure(ex) => logger.error(s"Failed to update consent ID: ${consent.id}", ex) + case Success(_) => // Already logged + } + } + } match { + case Failure(ex) => logger.error("Error in expiredObpConsents!", ex) + case Success(_) => logger.debug("|---> Task executed successfully") + } + } } diff --git a/obp-api/src/main/scala/code/util/Helper.scala b/obp-api/src/main/scala/code/util/Helper.scala index 2d8bde1110..6e17f682ea 100644 --- a/obp-api/src/main/scala/code/util/Helper.scala +++ b/obp-api/src/main/scala/code/util/Helper.scala @@ -583,4 +583,23 @@ object Helper extends Loggable { } + import java.util.Date + import java.util.Calendar + + def calculateValidTo( + validFrom: Option[Date], + timeToLive: Long // milliseconds + ): Date = { + val baseTime = validFrom.getOrElse(new Date()) + + val calendar = Calendar.getInstance() + calendar.setTime(baseTime) + calendar.add(Calendar.SECOND, timeToLive.toInt / 1000) + + calendar.getTime + } + + + + } \ No newline at end of file From d9af24f5648d547f5a428725f120db4ad616974e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Tue, 15 Apr 2025 07:57:47 +0200 Subject: [PATCH 04/10] refactor/Rename props to berlin_group_v1.3_alias.path --- obp-api/src/main/resources/props/sample.props.template | 2 +- obp-api/src/main/scala/code/api/util/APIUtil.scala | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/obp-api/src/main/resources/props/sample.props.template b/obp-api/src/main/resources/props/sample.props.template index c4fc55a054..55e25fc50c 100644 --- a/obp-api/src/main/resources/props/sample.props.template +++ b/obp-api/src/main/resources/props/sample.props.template @@ -1128,7 +1128,7 @@ default_auth_context_update_request_key=CUSTOMER_NUMBER #featured_api_collection_ids= # the alias prefix path for BerlinGroupV1.3 (OBP built-in is berlin-group/v1.3), the format must be xxx/yyy, eg: 0.6/v1 -#berlin_group_v1.3_alias.path= +#berlin_group_v1_3_alias_path= # Show the path inside of Berlin Group error message #berlin_group_error_message_show_path = true 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 6cdbe03345..9e559dc906 100644 --- a/obp-api/src/main/scala/code/api/util/APIUtil.scala +++ b/obp-api/src/main/scala/code/api/util/APIUtil.scala @@ -4754,7 +4754,7 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{ } } - val berlinGroupV13AliasPath = APIUtil.getPropsValue("berlin_group_v1.3_alias.path","").split("/").toList.map(_.trim) + val berlinGroupV13AliasPath = APIUtil.getPropsValue("berlin_group_v1_3_alias_path","").split("/").toList.map(_.trim) val getAtmsIsPublic = APIUtil.getPropsAsBoolValue("apiOptions.getAtmsIsPublic", true) From 796cc24758d1db9e224196b63082d5eb0a7c4e0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Tue, 15 Apr 2025 09:53:52 +0200 Subject: [PATCH 05/10] refactor/Factor out common code for Consent scheduler --- .../code/scheduler/ConsentScheduler.scala | 33 +++---------------- .../scala/code/scheduler/SchedulerUtil.scala | 33 +++++++++++++++++++ 2 files changed, 38 insertions(+), 28 deletions(-) create mode 100644 obp-api/src/main/scala/code/scheduler/SchedulerUtil.scala diff --git a/obp-api/src/main/scala/code/scheduler/ConsentScheduler.scala b/obp-api/src/main/scala/code/scheduler/ConsentScheduler.scala index f1f46f88f9..a3df0b335b 100644 --- a/obp-api/src/main/scala/code/scheduler/ConsentScheduler.scala +++ b/obp-api/src/main/scala/code/scheduler/ConsentScheduler.scala @@ -1,6 +1,5 @@ package code.scheduler -import code.actorsystem.ObpLookupSystem import code.api.util.APIUtil import code.consent.{ConsentStatus, MappedConsent} import code.util.Helper.MdcLoggable @@ -8,18 +7,12 @@ import com.openbankproject.commons.util.{ApiStandards, ApiVersion} import net.liftweb.common.Full import net.liftweb.mapper.{By, By_<} -import java.util.concurrent.TimeUnit -import java.util.{Calendar, Date} -import scala.concurrent.duration._ +import java.util.Date import scala.util.{Failure, Success, Try} object ConsentScheduler extends MdcLoggable { - private lazy val actorSystem = ObpLookupSystem.obpLookupSystem - implicit lazy val executor = actorSystem.dispatcher - private lazy val scheduler = actorSystem.scheduler - // Starts multiple scheduled tasks with different intervals def startAll(): Unit = { var initialDelay = 0 @@ -27,7 +20,7 @@ object ConsentScheduler extends MdcLoggable { APIUtil.getPropsAsIntValue("berlin_group_outdated_consents_interval_in_seconds") match { case Full(interval) if interval > 0 => val time = APIUtil.getPropsAsIntValue("berlin_group_outdated_consents_time_in_seconds", 300) - startTask(interval = interval, () => unfinishedBerlinGroupConsents(time)) // Runs periodically + SchedulerUtil.startTask(interval = interval, () => unfinishedBerlinGroupConsents(time)) // Runs periodically initialDelay = initialDelay + 10 case _ => logger.warn("|---> Skipping unfinishedBerlinGroupConsents task: berlin_group_outdated_consents_interval_in_seconds not set or invalid") @@ -35,7 +28,7 @@ object ConsentScheduler extends MdcLoggable { APIUtil.getPropsAsIntValue("berlin_group_expired_consents_interval_in_seconds") match { case Full(interval) if interval > 0 => - startTask(interval = interval, () => expiredBerlinGroupConsents(), initialDelay) // Delay for 10 seconds + SchedulerUtil.startTask(interval = interval, () => expiredBerlinGroupConsents(), initialDelay) // Delay for 10 seconds initialDelay = initialDelay + 10 case _ => logger.warn("|---> Skipping expiredBerlinGroupConsents task: berlin_group_expired_consents_interval_in_seconds not set or invalid") @@ -44,30 +37,14 @@ object ConsentScheduler extends MdcLoggable { // Open Bank Project APIUtil.getPropsAsIntValue("obp_expired_consents_interval_in_seconds") match { case Full(interval) if interval > 0 => - startTask(interval = interval, () => expiredObpConsents(), initialDelay) // Delay for 10 seconds + SchedulerUtil.startTask(interval = interval, () => expiredObpConsents(), initialDelay) // Delay for 10 seconds initialDelay = initialDelay + 10 case _ => logger.warn("|---> Skipping expiredObpConsents task: obp_expired_consents_interval_in_seconds not set or invalid") } } - // Generic method to schedule a task - private def startTask(interval: Long, task: () => Unit, initialDelay: Long = 0): Unit = { - scheduler.schedule( - initialDelay = Duration(initialDelay, TimeUnit.SECONDS), - interval = Duration(interval, TimeUnit.SECONDS), - runnable = new Runnable { - def run(): Unit = task() - } - ) - } - // Calculate the timestamp 5 minutes ago - private def someSecondsAgo(seconds: Int): Date = { - val cal = Calendar.getInstance() - cal.add(Calendar.SECOND, -seconds) - cal.getTime - } private def unfinishedBerlinGroupConsents(seconds: Int): Unit = { Try { @@ -76,7 +53,7 @@ object ConsentScheduler extends MdcLoggable { val outdatedConsents = MappedConsent.findAll( By(MappedConsent.mStatus, ConsentStatus.received.toString), By(MappedConsent.mApiStandard, ApiVersion.berlinGroupV13.apiStandard), - By_<(MappedConsent.updatedAt, someSecondsAgo(seconds)) + By_<(MappedConsent.updatedAt, SchedulerUtil.someSecondsAgo(seconds)) ) logger.debug(s"|---> Found ${outdatedConsents.size} outdated consents") diff --git a/obp-api/src/main/scala/code/scheduler/SchedulerUtil.scala b/obp-api/src/main/scala/code/scheduler/SchedulerUtil.scala new file mode 100644 index 0000000000..63fce1e1fb --- /dev/null +++ b/obp-api/src/main/scala/code/scheduler/SchedulerUtil.scala @@ -0,0 +1,33 @@ +package code.scheduler + + +import code.actorsystem.ObpLookupSystem + +import java.util.concurrent.TimeUnit +import java.util.{Calendar, Date} +import scala.concurrent.duration._ +object SchedulerUtil { + + private lazy val actorSystem = ObpLookupSystem.obpLookupSystem + implicit lazy val executor = actorSystem.dispatcher + private lazy val scheduler = actorSystem.scheduler + + // Generic method to schedule a task + def startTask(interval: Long, task: () => Unit, initialDelay: Long = 0): Unit = { + scheduler.schedule( + initialDelay = Duration(initialDelay, TimeUnit.SECONDS), + interval = Duration(interval, TimeUnit.SECONDS), + runnable = new Runnable { + def run(): Unit = task() + } + ) + } + + // Calculate the timestamp 5 minutes ago + def someSecondsAgo(seconds: Int): Date = { + val cal = Calendar.getInstance() + cal.add(Calendar.SECOND, -seconds) + cal.getTime + } + +} From f316a9f55f4ad4a1fcd68dffa156f2a7817403cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Tue, 15 Apr 2025 13:47:14 +0200 Subject: [PATCH 06/10] refactor/Add type safe transaction statuses of Berlin Group --- .../v1_3/JSONFactory_BERLIN_GROUP_1_3.scala | 15 +-- .../v1_3/PaymentInitiationServicePISApi.scala | 46 ++++----- .../group/v1_3/model/TransactionStatus.scala | 94 ++++++++++++++++++- .../LocalMappedConnectorInternal.scala | 3 +- .../PaymentInitiationServicePISApiTest.scala | 32 +++---- 5 files changed, 129 insertions(+), 61 deletions(-) diff --git a/obp-api/src/main/scala/code/api/berlin/group/v1_3/JSONFactory_BERLIN_GROUP_1_3.scala b/obp-api/src/main/scala/code/api/berlin/group/v1_3/JSONFactory_BERLIN_GROUP_1_3.scala index 8013b4eceb..44315adbb6 100644 --- a/obp-api/src/main/scala/code/api/berlin/group/v1_3/JSONFactory_BERLIN_GROUP_1_3.scala +++ b/obp-api/src/main/scala/code/api/berlin/group/v1_3/JSONFactory_BERLIN_GROUP_1_3.scala @@ -1,22 +1,20 @@ package code.api.berlin.group.v1_3 -import java.text.SimpleDateFormat -import java.util.Date +import code.api.berlin.group.v1_3.model.TransactionStatus.mapTransactionStatus import code.api.berlin.group.v1_3.model._ import code.api.util.APIUtil._ import code.api.util.ErrorMessages.MissingPropsValueAtThisInstance import code.api.util.{APIUtil, ConsentJWT, CustomJsonFormats, JwtUtil} -import code.bankconnectors.Connector import code.consent.ConsentTrait import code.model.ModeratedTransaction import com.openbankproject.commons.model.enums.AccountRoutingScheme -import com.openbankproject.commons.model.{BankAccount, TransactionRequest, User, _} +import com.openbankproject.commons.model._ import net.liftweb.common.Box.tryo import net.liftweb.common.{Box, Full} -import net.liftweb.json import net.liftweb.json.{JValue, parse} -import scala.collection.immutable.List +import java.text.SimpleDateFormat +import java.util.Date case class JvalueCaseClass(jvalueToCaseclass: JValue) @@ -649,10 +647,7 @@ object JSONFactory_BERLIN_GROUP_1_3 extends CustomJsonFormats { val scaRedirectUrl = getPropsValue("psu_make_payment_sca_redirect_url") .openOr(MissingPropsValueAtThisInstance + "psu_make_payment_sca_redirect_url") InitiatePaymentResponseJson( - transactionStatus = transactionRequest.status match { - case "COMPLETED" => "ACCP" - case "INITIATED" => "RCVD" - }, + transactionStatus = mapTransactionStatus(transactionRequest.status), paymentId = paymentId, _links = InitiatePaymentResponseLinks( scaRedirect = LinkHrefJson(s"$scaRedirectUrl/$paymentId"), diff --git a/obp-api/src/main/scala/code/api/berlin/group/v1_3/PaymentInitiationServicePISApi.scala b/obp-api/src/main/scala/code/api/berlin/group/v1_3/PaymentInitiationServicePISApi.scala index 01b008a802..6472e7b489 100644 --- a/obp-api/src/main/scala/code/api/berlin/group/v1_3/PaymentInitiationServicePISApi.scala +++ b/obp-api/src/main/scala/code/api/berlin/group/v1_3/PaymentInitiationServicePISApi.scala @@ -1,27 +1,22 @@ package code.api.builder.PaymentInitiationServicePISApi -import code.api.Constant import code.api.berlin.group.v1_3.JSONFactory_BERLIN_GROUP_1_3.{CancelPaymentResponseJson, CancelPaymentResponseLinks, LinkHrefJson, UpdatePaymentPsuDataJson, checkAuthorisationConfirmation, checkSelectPsuAuthenticationMethod, checkTransactionAuthorisation, checkUpdatePsuAuthentication, createCancellationTransactionRequestJson} -import code.api.berlin.group.v1_3.{JSONFactory_BERLIN_GROUP_1_3, JvalueCaseClass, OBP_BERLIN_GROUP_1_3} +import code.api.berlin.group.v1_3.model.TransactionStatus.mapTransactionStatus +import code.api.berlin.group.v1_3.model._ +import code.api.berlin.group.v1_3.{JSONFactory_BERLIN_GROUP_1_3, JvalueCaseClass} import code.api.util.APIUtil._ import code.api.util.ApiTag._ import code.api.util.ErrorMessages._ import code.api.util.NewStyle.HttpCode -import code.api.util.{ApiRole, ApiTag, CallContext, NewStyle} -import code.api.berlin.group.v1_3.model._ -import code.bankconnectors.Connector +import code.api.util.{ApiTag, CallContext, NewStyle} import code.fx.fx -import code.api.Constant._ import code.util.Helper -import code.views.Views 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.ChallengeType.BERLIN_GROUP_PAYMENT_CHALLENGE import com.openbankproject.commons.model.enums.TransactionRequestStatus._ -import com.openbankproject.commons.model.enums.{ChallengeType, StrongCustomerAuthenticationStatus, SuppliedAnswerType, TransactionRequestStatus,TransactionRequestTypes,PaymentServiceTypes} import com.openbankproject.commons.model.enums.TransactionRequestTypes._ -import com.openbankproject.commons.model.enums.PaymentServiceTypes._ +import com.openbankproject.commons.model.enums.{TransactionRequestStatus, _} import com.openbankproject.commons.util.ApiVersion import net.liftweb import net.liftweb.common.Box.tryo @@ -29,10 +24,8 @@ import net.liftweb.common.Full import net.liftweb.http.js.JE.JsRaw import net.liftweb.http.rest.RestHelper import net.liftweb.json -import net.liftweb.json.Serialization.write import net.liftweb.json._ -import scala.collection.immutable.Nil import scala.collection.mutable.ArrayBuffer import scala.concurrent.Future @@ -136,7 +129,7 @@ or * access method is generally applicable, but further authorisation processes (canBeCancelled, _, startSca) <- transactionRequestTypes match { case TransactionRequestTypes.SEPA_CREDIT_TRANSFERS => { transactionRequest.status.toUpperCase() match { - case "COMPLETED" => + case TransactionStatus.ACCP.code => NewStyle.function.cancelPaymentV400(TransactionId(transactionRequest.transaction_ids), callContext) map { x => x._1 match { case CancelPayment(true, Some(startSca)) if startSca == true => @@ -399,8 +392,8 @@ This method returns the SCA status of a payment initiation's authorisation sub-r s"""${mockedDataText(false)} Check the transaction status of a payment initiation.""", EmptyBody, - json.parse("""{ - "transactionStatus": "ACCP" + json.parse(s"""{ + "transactionStatus": "${TransactionStatus.ACCP.code}" }"""), List(UserNotLoggedIn, UnknownError), ApiTag("Payment Initiation Service (PIS)") :: apiTagBerlinGroupM :: Nil @@ -420,12 +413,9 @@ Check the transaction status of a payment initiation.""", } (transactionRequest, callContext) <- NewStyle.function.getTransactionRequestImpl(TransactionRequestId(paymentId), callContext) - transactionRequestStatus = transactionRequest.status match { - case "COMPLETED" => "ACCP" - case "INITIATED" => "RCVD" - } + transactionRequestStatus = mapTransactionStatus(transactionRequest.status) - transactionRequestAmount <- NewStyle.function.tryons(s"${UnknownError} transction request amount can not convert to a Decimal",400, callContext) { + transactionRequestAmount <- NewStyle.function.tryons(s"${UnknownError} transaction request amount can not convert to a Decimal",400, callContext) { BigDecimal(transactionRequest.body.to_sepa_credit_transfers.get.instructedAmount.amount) } transactionRequestCurrency <- NewStyle.function.tryons(s"${UnknownError} can not get currency from this paymentId(${paymentId})",400, callContext) { @@ -450,7 +440,7 @@ Check the transaction status of a payment initiation.""", fundsAvailable = (fromAccountBalance >= requestChangedCurrencyAmount) - transactionRequestStatusChekedFunds = if(fundsAvailable) transactionRequestStatus else "RCVD" + transactionRequestStatusChekedFunds = if(fundsAvailable) transactionRequestStatus else TransactionStatus.RCVD.code } yield { (json.parse(s"""{ @@ -534,7 +524,7 @@ Check the transaction status of a payment initiation.""", } //Berlin Group PaymentProduct is OBP transaction request type - transacitonRequestType <- NewStyle.function.tryons(checkPaymentProductError(paymentProduct), 400, callContext) { + transactionRequestType <- NewStyle.function.tryons(checkPaymentProductError(paymentProduct), 400, callContext) { TransactionRequestTypes.withName(paymentProduct.replaceAll("-", "_").toUpperCase) } @@ -565,13 +555,13 @@ Check the transaction status of a payment initiation.""", _ <- NewStyle.function.isEnabledTransactionRequests(callContext) - (createdTransactionRequest, callContext) <- transacitonRequestType match { + (createdTransactionRequest, callContext) <- transactionRequestType match { case TransactionRequestTypes.SEPA_CREDIT_TRANSFERS => { for { (createdTransactionRequest, callContext) <- NewStyle.function.createTransactionRequestBGV1( initiator = u, paymentServiceType, - transacitonRequestType, + transactionRequestType, transactionRequestBody = sepaCreditTransfersBerlinGroupV13, callContext ) @@ -606,7 +596,7 @@ Check the transaction status of a payment initiation.""", "creditorName": "70charname" }"""), json.parse(s"""{ - "transactionStatus": "RCVD", + "transactionStatus": "${TransactionStatus.RCVD.code}", "paymentId": "1234-wertiq-983", "_links": { @@ -655,7 +645,7 @@ Check the transaction status of a payment initiation.""", "dayOfExecution": "01" }"""), json.parse(s"""{ - "transactionStatus": "RCVD", + "transactionStatus": "${TransactionStatus.RCVD.code}", "paymentId": "1234-wertiq-983", "_links": { @@ -717,7 +707,7 @@ Check the transaction status of a payment initiation.""", ] }"""), json.parse(s"""{ - "transactionStatus": "RCVD", + "transactionStatus": "${TransactionStatus.RCVD.code}", "paymentId": "1234-wertiq-983", "_links": { @@ -1449,7 +1439,7 @@ There are the following request types on this access path: transactionRequestId = TransactionRequestId(paymentId) (existingTransactionRequest, callContext) <- NewStyle.function.getTransactionRequestImpl(transactionRequestId, callContext) _ <- Helper.booleanToFuture(failMsg= CannotUpdatePSUData, cc=callContext) { - existingTransactionRequest.status == TransactionRequestStatus.INITIATED.toString + existingTransactionRequest.status == TransactionStatus.RCVD.code } (_, callContext) <- NewStyle.function.getChallenge(authorisationId, callContext) (challenge, callContext) <- NewStyle.function.validateChallengeAnswerC4( diff --git a/obp-api/src/main/scala/code/api/berlin/group/v1_3/model/TransactionStatus.scala b/obp-api/src/main/scala/code/api/berlin/group/v1_3/model/TransactionStatus.scala index 2b5291b68c..eb69ccc555 100644 --- a/obp-api/src/main/scala/code/api/berlin/group/v1_3/model/TransactionStatus.scala +++ b/obp-api/src/main/scala/code/api/berlin/group/v1_3/model/TransactionStatus.scala @@ -11,8 +11,98 @@ */ package code.api.berlin.group.v1_3.model +sealed trait TransactionStatus { + def code: String + def description: String +} +object TransactionStatus extends ApiModel { + + case object ACCC extends TransactionStatus { + val code = "ACCC" + val description = "AcceptedSettlementCompleted - Settlement on the creditor's account has been completed." + } + + case object ACCP extends TransactionStatus { + val code = "ACCP" + val description = "AcceptedCustomerProfile - Technical validation and customer profile check successful." + } + + case object ACSC extends TransactionStatus { + val code = "ACSC" + val description = "AcceptedSettlementCompleted - Settlement on the debtor’s account has been completed." + } + + case object ACSP extends TransactionStatus { + val code = "ACSP" + val description = "AcceptedSettlementInProcess - Payment initiation accepted for execution." + } + + case object ACTC extends TransactionStatus { + val code = "ACTC" + val description = "AcceptedTechnicalValidation - Authentication and validation successful." + } + + case object ACWC extends TransactionStatus { + val code = "ACWC" + val description = "AcceptedWithChange - Instruction accepted but changes made (e.g. date or remittance)." + } + + case object ACWP extends TransactionStatus { + val code = "ACWP" + val description = "AcceptedWithoutPosting - Accepted but not posted to creditor’s account." + } + + case object RCVD extends TransactionStatus { + val code = "RCVD" + val description = "Received - Payment initiation received by receiving agent." + } + + case object PDNG extends TransactionStatus { + val code = "PDNG" + val description = "Pending - Further checks pending before completion." + } + + case object RJCT extends TransactionStatus { + val code = "RJCT" + val description = "Rejected - Payment initiation or transaction has been rejected." + } + + case object CANC extends TransactionStatus { + val code = "CANC" + val description = "Cancelled - Payment initiation cancelled before execution." + } + + case object ACFC extends TransactionStatus { + val code = "ACFC" + val description = "AcceptedFundsChecked - Technical, profile, and funds check successful." + } + + case object PATC extends TransactionStatus { + val code = "PATC" + val description = "PartiallyAcceptedTechnical - Some required authentications performed." + } + + case object PART extends TransactionStatus { + val code = "PART" + val description = "PartiallyAccepted - Some transactions accepted in a bulk payment." + } + + val values: List[TransactionStatus] = List( + ACCC, ACCP, ACSC, ACSP, ACTC, ACWC, ACWP, RCVD, + PDNG, RJCT, CANC, ACFC, PATC, PART + ) + + def fromCode(code: String): Option[TransactionStatus] = values.find(_.code == code) + + def mapTransactionStatus(status: String): String = { + status match { + case "COMPLETED" => TransactionStatus.ACCP.code + case "INITIATED" => TransactionStatus.RCVD.code + case other => other + } + } + +} -case class TransactionStatus ( -) extends ApiModel diff --git a/obp-api/src/main/scala/code/bankconnectors/LocalMappedConnectorInternal.scala b/obp-api/src/main/scala/code/bankconnectors/LocalMappedConnectorInternal.scala index 6e9a71ee2c..ddf1f8a9b3 100644 --- a/obp-api/src/main/scala/code/bankconnectors/LocalMappedConnectorInternal.scala +++ b/obp-api/src/main/scala/code/bankconnectors/LocalMappedConnectorInternal.scala @@ -2,6 +2,7 @@ package code.bankconnectors import code.fx.fx.TTL import code.api.Constant._ +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._ @@ -153,7 +154,7 @@ object LocalMappedConnectorInternal extends MdcLoggable { "" ), transDetailsSerialized, - status.toString, + mapTransactionStatus(status.toString), charge, "", // chargePolicy is not used in BG so far. Some(paymentServiceType.toString), diff --git a/obp-api/src/test/scala/code/api/berlin/group/v1_3/PaymentInitiationServicePISApiTest.scala b/obp-api/src/test/scala/code/api/berlin/group/v1_3/PaymentInitiationServicePISApiTest.scala index e9385aed7e..ae8f100787 100644 --- a/obp-api/src/test/scala/code/api/berlin/group/v1_3/PaymentInitiationServicePISApiTest.scala +++ b/obp-api/src/test/scala/code/api/berlin/group/v1_3/PaymentInitiationServicePISApiTest.scala @@ -1,31 +1,23 @@ package code.api.berlin.group.v1_3 import code.api.BerlinGroup.ScaStatus -import code.api.Constant -import code.api.Constant.{SYSTEM_INITIATE_PAYMENTS_BERLIN_GROUP_VIEW_ID, SYSTEM_READ_TRANSACTIONS_BERLIN_GROUP_VIEW_ID} -import code.api.berlin.group.v1_3.JSONFactory_BERLIN_GROUP_1_3.{CancellationJsonV13, InitiatePaymentResponseJson, StartPaymentAuthorisationJson} -import code.api.berlin.group.v1_3.model.{PsuData, ScaStatusResponse, UpdatePsuAuthenticationResponse} +import code.api.Constant.SYSTEM_INITIATE_PAYMENTS_BERLIN_GROUP_VIEW_ID import code.api.berlin.group.v1_3.JSONFactory_BERLIN_GROUP_1_3.{CancellationJsonV13, ErrorMessagesBG, InitiatePaymentResponseJson, StartPaymentAuthorisationJson} +import code.api.berlin.group.v1_3.model.{ScaStatusResponse, TransactionStatus, UpdatePsuAuthenticationResponse} import code.api.builder.PaymentInitiationServicePISApi.APIMethods_PaymentInitiationServicePISApi import code.api.util.APIUtil.OAuth._ import code.api.util.APIUtil.extractErrorMessageCode -import code.api.util.ErrorMessages.{AuthorisationNotFound, InvalidJsonFormat, NotPositiveAmount, _} +import code.api.util.ErrorMessages._ import code.model.dataAccess.{BankAccountRouting, MappedBankAccount} import code.setup.{APIResponse, DefaultUsers} -import com.openbankproject.commons.model.enums.TransactionRequestTypes -import com.openbankproject.commons.model.enums.TransactionRequestTypes._ -import com.openbankproject.commons.model.enums.PaymentServiceTypes -import com.openbankproject.commons.model.enums.PaymentServiceTypes._ import code.views.Views import com.github.dwickern.macros.NameOf.nameOf -import com.openbankproject.commons.model.enums.AccountRoutingScheme -import com.openbankproject.commons.model.{ErrorMessage, SepaCreditTransfers, SepaCreditTransfersBerlinGroupV13, ViewId} +import com.openbankproject.commons.model.enums.{AccountRoutingScheme, PaymentServiceTypes, TransactionRequestTypes} +import com.openbankproject.commons.model.{SepaCreditTransfers, SepaCreditTransfersBerlinGroupV13, ViewId} import net.liftweb.json.Serialization.write import net.liftweb.mapper.By import org.scalatest.Tag -import scala.collection.immutable.List - class PaymentInitiationServicePISApiTest extends BerlinGroupServerSetupV1_3 with DefaultUsers { object PIS extends Tag("Payment Initiation Service (PIS)") @@ -137,7 +129,7 @@ class PaymentInitiationServicePISApiTest extends BerlinGroupServerSetupV1_3 with Then("We should get a 201 ") response.code should equal(201) val payment = response.body.extract[InitiatePaymentResponseJson] - payment.transactionStatus should be ("ACCP") + payment.transactionStatus should be (TransactionStatus.ACCP.code) payment.paymentId should not be null payment._links.scaStatus should not be null @@ -190,7 +182,7 @@ class PaymentInitiationServicePISApiTest extends BerlinGroupServerSetupV1_3 with Then("We should get a 201 ") response.code should equal(201) val payment = response.body.extract[InitiatePaymentResponseJson] - payment.transactionStatus should be ("RCVD") + payment.transactionStatus should be (TransactionStatus.RCVD.code) payment.paymentId should not be null payment._links.scaStatus should not be null @@ -248,7 +240,7 @@ class PaymentInitiationServicePISApiTest extends BerlinGroupServerSetupV1_3 with Then("We should get a 201 ") response.code should equal(201) val payment = response.body.extract[InitiatePaymentResponseJson] - payment.transactionStatus should be ("ACCP") + payment.transactionStatus should be (TransactionStatus.ACCP.code) payment.paymentId should not be null Then(s"we test the ${getPaymentInformation.name}") @@ -292,7 +284,7 @@ class PaymentInitiationServicePISApiTest extends BerlinGroupServerSetupV1_3 with Then("We should get a 201 ") response.code should equal(201) val payment = response.body.extract[InitiatePaymentResponseJson] - payment.transactionStatus should be ("RCVD") + payment.transactionStatus should be (TransactionStatus.RCVD.code) payment.paymentId should not be null payment._links.scaStatus should not be null @@ -301,7 +293,7 @@ class PaymentInitiationServicePISApiTest extends BerlinGroupServerSetupV1_3 with val requestGet = (V1_3_BG / PaymentServiceTypes.payments.toString / TransactionRequestTypes.SEPA_CREDIT_TRANSFERS.toString / paymentId / "status").GET <@ (user1) val responseGet: APIResponse = makeGetRequest(requestGet) responseGet.code should be (200) - (responseGet.body \ "transactionStatus").extract[String] should be ("RCVD") + (responseGet.body \ "transactionStatus").extract[String] should be (TransactionStatus.RCVD.code) (responseGet.body \ "fundsAvailable").extract[Boolean] should be (true) } } @@ -351,7 +343,7 @@ class PaymentInitiationServicePISApiTest extends BerlinGroupServerSetupV1_3 with Then("We should get a 201 ") responseInitiatePaymentJson.code should equal(201) val paymentResponseInitiatePaymentJson = responseInitiatePaymentJson.body.extract[InitiatePaymentResponseJson] - paymentResponseInitiatePaymentJson.transactionStatus should be ("RCVD") + paymentResponseInitiatePaymentJson.transactionStatus should be (TransactionStatus.RCVD.code) paymentResponseInitiatePaymentJson.paymentId should not be null val paymentId = paymentResponseInitiatePaymentJson.paymentId @@ -502,7 +494,7 @@ class PaymentInitiationServicePISApiTest extends BerlinGroupServerSetupV1_3 with Then("We should get a 201 ") responseInitiatePaymentJson.code should equal(201) val paymentResponseInitiatePaymentJson = responseInitiatePaymentJson.body.extract[InitiatePaymentResponseJson] - paymentResponseInitiatePaymentJson.transactionStatus should be ("ACCP") + paymentResponseInitiatePaymentJson.transactionStatus should be (TransactionStatus.ACCP.code) val paymentId = paymentResponseInitiatePaymentJson.paymentId From b8202ecf468464f726481185d70239d723d29a32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Tue, 15 Apr 2025 15:34:31 +0200 Subject: [PATCH 07/10] bugfix/TPP-Redirect-URI is mandatory header in case of create consent BG --- obp-api/src/main/scala/code/api/util/BerlinGroupCheck.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/obp-api/src/main/scala/code/api/util/BerlinGroupCheck.scala b/obp-api/src/main/scala/code/api/util/BerlinGroupCheck.scala index 27a3a9807b..2bf05e2b8d 100644 --- a/obp-api/src/main/scala/code/api/util/BerlinGroupCheck.scala +++ b/obp-api/src/main/scala/code/api/util/BerlinGroupCheck.scala @@ -27,7 +27,7 @@ object BerlinGroupCheck extends MdcLoggable { private def validateHeaders(verb: String, url: String, reqHeaders: List[HTTPParam], forwardResult: (Box[User], Option[CallContext])): (Box[User], Option[CallContext]) = { val headerMap = reqHeaders.map(h => h.name.toLowerCase -> h).toMap - val missingHeaders = if(url.contains(ApiVersion.berlinGroupV13.urlPrefix) && url.endsWith("/consent")) + val missingHeaders = if(url.contains(ApiVersion.berlinGroupV13.urlPrefix) && url.endsWith("/consents")) (berlinGroupMandatoryHeaders ++ berlinGroupMandatoryHeaderConsent).filterNot(headerMap.contains) else berlinGroupMandatoryHeaders.filterNot(headerMap.contains) From 76e53faf622dad830d535eea25cb64e476ae40b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Wed, 16 Apr 2025 07:37:46 +0200 Subject: [PATCH 08/10] docfix/Tweak error message regarding singular/plural case --- obp-api/src/main/scala/code/api/util/BerlinGroupCheck.scala | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/obp-api/src/main/scala/code/api/util/BerlinGroupCheck.scala b/obp-api/src/main/scala/code/api/util/BerlinGroupCheck.scala index 2bf05e2b8d..0dd184a670 100644 --- a/obp-api/src/main/scala/code/api/util/BerlinGroupCheck.scala +++ b/obp-api/src/main/scala/code/api/util/BerlinGroupCheck.scala @@ -35,7 +35,11 @@ object BerlinGroupCheck extends MdcLoggable { if (missingHeaders.isEmpty) { forwardResult // All mandatory headers are present } else { - (fullBoxOrException(Empty ~> APIFailureNewStyle(s"${ErrorMessages.MissingMandatoryBerlinGroupHeaders}(${missingHeaders.mkString(", ")})", 400, forwardResult._2.map(_.toLight))), forwardResult._2) + if(missingHeaders.size == 1) { + (fullBoxOrException(Empty ~> APIFailureNewStyle(s"${ErrorMessages.MissingMandatoryBerlinGroupHeaders.replace("headers", "header")}(${missingHeaders.mkString(", ")})", 400, forwardResult._2.map(_.toLight))), forwardResult._2) + } else { + (fullBoxOrException(Empty ~> APIFailureNewStyle(s"${ErrorMessages.MissingMandatoryBerlinGroupHeaders}(${missingHeaders.mkString(", ")})", 400, forwardResult._2.map(_.toLight))), forwardResult._2) + } } } From 0b203ca0db20452288f7692a4ac85c5de9291e9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Wed, 16 Apr 2025 10:37:40 +0200 Subject: [PATCH 09/10] bugfix/Berlin Group Balances and Transaction access imply and Account access as well --- obp-api/src/main/scala/code/api/util/ConsentUtil.scala | 5 ++++- obp-api/src/main/scala/code/snippet/BerlinGroupConsent.scala | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/obp-api/src/main/scala/code/api/util/ConsentUtil.scala b/obp-api/src/main/scala/code/api/util/ConsentUtil.scala index 27f1f180ec..56f34d3d61 100644 --- a/obp-api/src/main/scala/code/api/util/ConsentUtil.scala +++ b/obp-api/src/main/scala/code/api/util/ConsentUtil.scala @@ -731,7 +731,10 @@ object Consent extends MdcLoggable { } // 1. Add access - val accounts: List[Future[ConsentView]] = consent.access.accounts.getOrElse(Nil) map { account => + val allAccesses = consent.access.accounts.getOrElse(Nil) ::: + consent.access.balances.getOrElse(Nil) ::: // Balances access implies and Account access as well + consent.access.transactions.getOrElse(Nil) // Transactions access implies and Account access as well + val accounts: List[Future[ConsentView]] = allAccesses.distinct map { account => Connector.connector.vend.getBankAccountByIban(account.iban.getOrElse(""), callContext) map { bankAccount => logger.debug(s"createBerlinGroupConsentJWT.accounts.bankAccount: $bankAccount") val error = s"${InvalidConnectorResponse} IBAN: ${account.iban.getOrElse("")} ${handleBox(bankAccount._1)}" diff --git a/obp-api/src/main/scala/code/snippet/BerlinGroupConsent.scala b/obp-api/src/main/scala/code/snippet/BerlinGroupConsent.scala index 10f6a0c772..920865a0f7 100644 --- a/obp-api/src/main/scala/code/snippet/BerlinGroupConsent.scala +++ b/obp-api/src/main/scala/code/snippet/BerlinGroupConsent.scala @@ -291,7 +291,7 @@ class BerlinGroupConsent extends MdcLoggable with RestHelper with APIMethods510

Read account details

- {scala.xml.Unparsed(canReadAccountsIbans.map(iban => s"- $iban").mkString("
"))} + {scala.xml.Unparsed(ibansFromGetConsentResponseJson.map(iban => s"- $iban").mkString("
"))}

From 9f33b9f362576c4f410ad710916246d357be6a00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Wed, 16 Apr 2025 13:24:16 +0200 Subject: [PATCH 10/10] feature/Berlin Group Explicit TPP redirect URI page --- .../main/scala/bootstrap/liftweb/Boot.scala | 1 + .../scala/code/api/constant/constant.scala | 1 + .../scala/code/api/util/ConsentUtil.scala | 5 +- .../code/snippet/BerlinGroupConsent.scala | 26 ++++++- ...nfirm-bg-consent-request-redirect-uri.html | 74 +++++++++++++++++++ 5 files changed, 104 insertions(+), 3 deletions(-) create mode 100644 obp-api/src/main/webapp/confirm-bg-consent-request-redirect-uri.html diff --git a/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala b/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala index 38f3964810..7ecef624f1 100644 --- a/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala +++ b/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala @@ -591,6 +591,7 @@ class Boot extends MdcLoggable { Menu.i("confirm-user-auth-context-update-request") / "confirm-user-auth-context-update-request", Menu.i("confirm-bg-consent-request") / "confirm-bg-consent-request" >> AuthUser.loginFirst,//OAuth consent page, Menu.i("confirm-bg-consent-request-sca") / "confirm-bg-consent-request-sca" >> AuthUser.loginFirst,//OAuth consent page, + Menu.i("confirm-bg-consent-request-redirect-uri") / "confirm-bg-consent-request-redirect-uri" >> AuthUser.loginFirst,//OAuth consent page, Menu.i("confirm-vrp-consent-request") / "confirm-vrp-consent-request" >> AuthUser.loginFirst,//OAuth consent page, Menu.i("confirm-vrp-consent") / "confirm-vrp-consent" >> AuthUser.loginFirst //OAuth consent page ) ++ accountCreation ++ Admin.menus diff --git a/obp-api/src/main/scala/code/api/constant/constant.scala b/obp-api/src/main/scala/code/api/constant/constant.scala index 2920def29f..97fa6ddab1 100644 --- a/obp-api/src/main/scala/code/api/constant/constant.scala +++ b/obp-api/src/main/scala/code/api/constant/constant.scala @@ -151,6 +151,7 @@ object RequestHeader { final lazy val `X-Request-ID` = "X-Request-ID" // Berlin Group final lazy val `TPP-Redirect-URI` = "TPP-Redirect-URI" // Berlin Group + final lazy val `TPP-Nok-Redirect-URI` = "TPP-Nok-Redirect-URI" // Redirect URI in case of an error. final lazy val Date = "Date" // Berlin Group // Headers to support the signature function of Berlin Group final lazy val Digest = "Digest" // Berlin Group diff --git a/obp-api/src/main/scala/code/api/util/ConsentUtil.scala b/obp-api/src/main/scala/code/api/util/ConsentUtil.scala index 56f34d3d61..db2c6ea75c 100644 --- a/obp-api/src/main/scala/code/api/util/ConsentUtil.scala +++ b/obp-api/src/main/scala/code/api/util/ConsentUtil.scala @@ -770,7 +770,8 @@ object Consent extends MdcLoggable { ) } } - val tppRedirectUrl: Option[HTTPParam] = callContext.map(_.requestHeaders).getOrElse(Nil).find(_.name == RequestHeader.`TPP-Redirect-URI`) + val tppRedirectUri: Option[HTTPParam] = callContext.map(_.requestHeaders).getOrElse(Nil).find(_.name == RequestHeader.`TPP-Redirect-URI`) + val tppNokRedirectUri: Option[HTTPParam] = callContext.map(_.requestHeaders).getOrElse(Nil).find(_.name == RequestHeader.`TPP-Nok-Redirect-URI`) Future.sequence(accounts ::: balances ::: transactions) map { views => val json = ConsentJWT( createdByUserId = user.map(_.userId).getOrElse(""), @@ -781,7 +782,7 @@ object Consent extends MdcLoggable { iat = currentTimeInSeconds, nbf = currentTimeInSeconds, exp = validUntilTimeInSeconds, - request_headers = tppRedirectUrl.toList, + request_headers = tppRedirectUri.toList ::: tppNokRedirectUri.toList, name = None, email = None, entitlements = Nil, diff --git a/obp-api/src/main/scala/code/snippet/BerlinGroupConsent.scala b/obp-api/src/main/scala/code/snippet/BerlinGroupConsent.scala index 920865a0f7..6f73a00346 100644 --- a/obp-api/src/main/scala/code/snippet/BerlinGroupConsent.scala +++ b/obp-api/src/main/scala/code/snippet/BerlinGroupConsent.scala @@ -64,6 +64,8 @@ class BerlinGroupConsent extends MdcLoggable with RestHelper with APIMethods510 // Session variables to store OTP, redirect URI, and other consent-related data private object otpValue extends SessionVar("123") // Stores the OTP value for SCA (Strong Customer Authentication) private object redirectUriValue extends SessionVar("") // Stores the redirect URI for post-consent actions + private object tppNokRedirectUriValue extends SessionVar("") + private object consumerNameValue extends SessionVar("") // Stores the redirect URI for post-consent actions private object updateConsentPayloadValue extends SessionVar(false) // Flag to indicate if consent payload needs updating private object userIsOwnerOfAccountsValue extends SessionVar(true) // Flag to check if the user owns the accounts @@ -148,6 +150,10 @@ class BerlinGroupConsent extends MdcLoggable with RestHelper with APIMethods510 val tppRedirectUri: immutable.Seq[String] = consentJwt.map { h => h.request_headers.filter(h => h.name == RequestHeader.`TPP-Redirect-URI`) }.getOrElse(Nil).map((_.values.mkString(""))) + val tppNokRedirectUri: immutable.Seq[String] = consentJwt.map { h => + h.request_headers.filter(h => h.name == RequestHeader.`TPP-Nok-Redirect-URI`) + }.getOrElse(Nil).map((_.values.mkString(""))) + tppNokRedirectUriValue.set(tppNokRedirectUri.headOption.getOrElse("/")) val consumerRedirectUri: Option[String] = consumer.map(_.redirectURL.get).toOption val uri: String = tppRedirectUri.headOption.orElse(consumerRedirectUri).getOrElse("https://not.defined.com") redirectUriValue.set(uri) @@ -272,6 +278,7 @@ class BerlinGroupConsent extends MdcLoggable with RestHelper with APIMethods510 val firstName = currentUser.map(_.firstName.get).getOrElse("") val lastName = currentUser.map(_.lastName.get).getOrElse("") val consumerName = consumer.map(_.name.get).getOrElse("") + consumerNameValue.set(consumerName) val formText = s"""I, $firstName $lastName, consent to the service provider $consumerName making the following actions on my behalf: |""".stripMargin @@ -393,13 +400,18 @@ class BerlinGroupConsent extends MdcLoggable with RestHelper with APIMethods510 case Full(consent) if otpValue.is == consent.challenge => Consents.consentProvider.vend.updateConsentStatus(consentId, ConsentStatus.valid) S.redirectTo( - s"$redirectUriValue?CONSENT_ID=${consentId}" + s"/confirm-bg-consent-request-redirect-uri?CONSENT_ID=${consentId}" ) case _ => S.error("Wrong OTP value") } } + private def getTppRedirectUri() = { + val consentId = ObpS.param("CONSENT_ID") openOr ("") + s"$redirectUriValue?CONSENT_ID=${consentId}" + } + /** * Renders the SCA confirmation form for Berlin Group consent. * @@ -409,5 +421,17 @@ class BerlinGroupConsent extends MdcLoggable with RestHelper with APIMethods510 "#otp-value" #> SHtml.text(otpValue, otpValue(_)) & "type=submit" #> SHtml.onSubmitUnit(confirmConsentRequestProcessSca) } + + /** + * Renders the TPP Redirect URI form for Berlin Group consent. + * + * @return CssSel for rendering the form. + */ + def setTppRedirectUri: CssSel = { + "#confirm-bg-consent-redirect-uri-submit-button a [href]" #> getTppRedirectUri() & + "#confirm-bg-consent-redirect-uri-submit-button a [data-fallback]" #> tppNokRedirectUriValue.is & + "#confirm-bg-consent-redirect-uri-text *" #> s"""Consent has been created with success and now the user will be redirected back to his original app ${consumerNameValue.is}""" + + } } diff --git a/obp-api/src/main/webapp/confirm-bg-consent-request-redirect-uri.html b/obp-api/src/main/webapp/confirm-bg-consent-request-redirect-uri.html new file mode 100644 index 0000000000..aa7d424fad --- /dev/null +++ b/obp-api/src/main/webapp/confirm-bg-consent-request-redirect-uri.html @@ -0,0 +1,74 @@ + + +

+ +
+