diff --git a/obp-api/src/main/resources/props/sample.props.template b/obp-api/src/main/resources/props/sample.props.template index 6dcc829bf0..55e25fc50c 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 @@ -1124,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/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/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/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..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) @@ -136,14 +134,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 +414,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 +449,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 +462,7 @@ object JSONFactory_BERLIN_GROUP_1_3 extends CustomJsonFormats { val creditorAccount = CreditorAccountJson( iban = iban, + currency = Some(bankAccount.currency) ) TransactionsJsonV13( FromAccount( @@ -647,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/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/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) 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..0dd184a670 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) @@ -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) + } } } 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/ConsentUtil.scala b/obp-api/src/main/scala/code/api/util/ConsentUtil.scala index 27f1f180ec..db2c6ea75c 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)}" @@ -767,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(""), @@ -778,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/api/util/ErrorMessages.scala b/obp-api/src/main/scala/code/api/util/ErrorMessages.scala index b008c7e3f4..ea74ba5bc3 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." 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 19bf324f71..100576bf77 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/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/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..a3df0b335b 100644 --- a/obp-api/src/main/scala/code/scheduler/ConsentScheduler.scala +++ b/obp-api/src/main/scala/code/scheduler/ConsentScheduler.scala @@ -1,62 +1,50 @@ package code.scheduler -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_<} -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 + // 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 + 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") } 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 + 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") } - + // Open Bank Project + APIUtil.getPropsAsIntValue("obp_expired_consents_interval_in_seconds") match { + case Full(interval) if interval > 0 => + 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 { @@ -65,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") @@ -110,5 +98,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/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 + } + +} diff --git a/obp-api/src/main/scala/code/snippet/BerlinGroupConsent.scala b/obp-api/src/main/scala/code/snippet/BerlinGroupConsent.scala index 10f6a0c772..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 @@ -291,7 +298,7 @@ class BerlinGroupConsent extends MdcLoggable with RestHelper with APIMethods510
Read account details
@@ -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/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 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 @@ + + +