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 @@
+
+
+
+