diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 55a6d87f59..0000000000 --- a/Dockerfile +++ /dev/null @@ -1,13 +0,0 @@ -FROM maven:3-eclipse-temurin-11 as maven -# Build the source using maven, source is copied from the 'repo' build. -COPY . /usr/src/OBP-API -RUN cp /usr/src/OBP-API/obp-api/pom.xml /tmp/pom.xml # For Packaging a local repository within the image -WORKDIR /usr/src/OBP-API -RUN cp obp-api/src/main/resources/props/test.default.props.template obp-api/src/main/resources/props/test.default.props -RUN cp obp-api/src/main/resources/props/sample.props.template obp-api/src/main/resources/props/default.props -RUN --mount=type=cache,target=$HOME/.m2 MAVEN_OPTS="-Xmx3G -Xss2m" mvn install -pl .,obp-commons -RUN --mount=type=cache,target=$HOME/.m2 MAVEN_OPTS="-Xmx3G -Xss2m" mvn install -DskipTests -pl obp-api - -FROM jetty:9.4-jdk11-alpine - -COPY --from=maven /usr/src/OBP-API/obp-api/target/obp-api-1.*.war /var/lib/jetty/webapps/ROOT.war \ No newline at end of file diff --git a/certificates.sh b/certificates.sh new file mode 100644 index 0000000000..be555b6a47 --- /dev/null +++ b/certificates.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env sh +CERT_DIR="/certificates" +KEYSTORE="/var/lib/jetty/keystore.jks" +TRUSTSTORE="/var/lib/jetty/truststore.jks" +PASSWORD="changeit" +for CERT in "$CERT_DIR"/*.crt; do +ALIAS=$(basename "$CERT" | sed 's/\.[^.]*$//') +keytool -importcert -noprompt -file "$CERT" -alias "$ALIAS" -keystore "$KEYSTORE" -storepass "$PASSWORD" +keytool -importcert -noprompt -file "$CERT" -alias "$ALIAS" -keystore "$TRUSTSTORE" -storepass "$PASSWORD" +done +chown jetty:jetty $TRUSTSTORE +chown jetty:jetty $KEYSTORE \ No newline at end of file diff --git a/obp-api/pom.xml b/obp-api/pom.xml index 9955f4ad7d..f3a86cbe01 100644 --- a/obp-api/pom.xml +++ b/obp-api/pom.xml @@ -1,6 +1,6 @@ + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 @@ -27,7 +27,7 @@ org.sonatype.oss.groups.public - Sonatype Public + Sonatype Public https://oss.sonatype.org/content/groups/public @@ -55,10 +55,25 @@ org.everit.json.schema 1.6.1 + + org.json + json + 20231013 + ch.qos.logback logback-classic - 1.2.13 + 1.5.19 + + + com.google.protobuf + protobuf-java + 3.25.5 + + + com.google.code.gson + gson + 2.8.9 net.liftweb @@ -93,8 +108,14 @@ org.apache.commons commons-lang3 - 3.12.0 + 3.18.0 + + commons-beanutils + commons-beanutils + 1.11.0 + + org.apache.commons @@ -155,13 +176,14 @@ ${jetty.version} test + org.eclipse.jetty jetty-webapp ${jetty.version} test - + cglib cglib @@ -193,21 +215,21 @@ amqp_3.1_${scala.version} 1.5.0 - - - - - - - - - - - + + + + + + + + + + + org.elasticsearch elasticsearch - 8.14.0 + 8.18.8 @@ -236,7 +258,37 @@ com.typesafe.akka akka-http-core_${scala.version} - 10.1.6 + 10.5.3 + + + org.xerial.snappy + snappy-java + 1.1.10.4 + + + net.minidev + json-smart + 2.4.9 + + + com.typesafe.akka + akka-stream_${scala.version} + 2.5.32 + + + io.prometheus + simpleclient + 0.16.0 + + + io.prometheus + simpleclient_common + 0.16.0 + + + io.prometheus + simpleclient_hotspot + 0.16.0 com.typesafe.akka @@ -251,7 +303,7 @@ com.sksamuel.avro4s avro4s-core_${scala.version} - ${avro.version} + 1.8.2 org.apache.commons @@ -295,7 +347,7 @@ com.nimbusds nimbus-jose-jwt - 9.37.2 + 10.0.2 com.github.OpenBankProject @@ -391,23 +443,33 @@ io.grpc grpc-all - 1.48.1 + 1.75.0 io.netty netty-tcnative-boringssl-static 2.0.27.Final + + io.netty + netty-codec + 4.1.125.Final + + + io.netty + netty-codec-http + 4.1.125.Final + org.asynchttpclient async-http-client - 2.10.4 - - - javax.activation - com.sun.activation - - + 2.12.4 + + + javax.activation + com.sun.activation + + @@ -430,7 +492,7 @@ com.microsoft.sqlserver mssql-jdbc - 11.2.0.jre${java.version} + 11.2.4.jre${java.version} @@ -482,11 +544,21 @@ Java Client for ORY Hydra https://github.com/ory/hydra-client-java --> + + commons-fileupload + commons-fileupload + 1.6.0 + sh.ory.hydra hydra-client 1.7.0 + + com.fasterxml.jackson.core + jackson-core + 2.15.0 + com.fasterxml.jackson.core jackson-databind @@ -520,14 +592,14 @@ - com.sun.mail - jakarta.mail - 2.0.1 + com.sun.mail + jakarta.mail + 2.0.1 - jakarta.activation - jakarta.activation-api - 2.0.1 + jakarta.activation + jakarta.activation-api + 2.0.1 com.sun.activation @@ -559,11 +631,11 @@ scalatest-maven-plugin ${project.build.directory}/surefire-reports - once - . - WDF TestSuite.txt - -Drun.mode=test -XX:MaxMetaspaceSize=512m -Xms512m -Xmx512m - code.external + once + . + WDF TestSuite.txt + -Drun.mode=test -XX:MaxMetaspaceSize=512m -Xms512m -Xmx512m + code.external @@ -574,116 +646,116 @@ - - - org.codehaus.mojo - build-helper-maven-plugin - 3.6.0 - - - generate-sources - - add-source - - - - src/main/java - - - - - - - net.alchim31.maven - scala-maven-plugin - - - org.apache.maven.plugins - maven-war-plugin - 3.4.0 - - ${webXmlPath} - - - - org.apache.maven.plugins - maven-resources-plugin - 3.0.1 - - - default-copy-resources - process-resources - - copy-resources - - - true - ${project.build.directory} - - - ${project.basedir}/src - - packageLinkDefs.properties - - true - - - - - - - - org.mortbay.jetty - maven-jetty-plugin - - / - 5 - - - - org.apache.maven.plugins - maven-idea-plugin - 2.2.1 - - true - - - - org.apache.maven.plugins - maven-eclipse-plugin - 2.10 - - true - - ch.epfl.lamp.sdt.core.scalanature - - - ch.epfl.lamp.sdt.core.scalabuilder - - - ch.epfl.lamp.sdt.launching.SCALA_CONTAINER - org.eclipse.jdt.launching.JRE_CONTAINER - - - - - pl.project13.maven - git-commit-id-plugin - 4.9.10 - - - - revision - - - - - ${project.basedir}/.git - true - src/main/resources/git.properties - false - - + + + org.codehaus.mojo + build-helper-maven-plugin + 3.6.0 + + + generate-sources + + add-source + + + + src/main/java + + + + + + + net.alchim31.maven + scala-maven-plugin + + + org.apache.maven.plugins + maven-war-plugin + 3.4.0 + + ${webXmlPath} + + + + org.apache.maven.plugins + maven-resources-plugin + 3.0.1 + + + default-copy-resources + process-resources + + copy-resources + + + true + ${project.build.directory} + + + ${project.basedir}/src + + packageLinkDefs.properties + + true + + + + + + + + org.mortbay.jetty + maven-jetty-plugin + + / + 5 + + + + org.apache.maven.plugins + maven-idea-plugin + 2.2.1 + + true + + + + org.apache.maven.plugins + maven-eclipse-plugin + 2.10 + + true + + ch.epfl.lamp.sdt.core.scalanature + + + ch.epfl.lamp.sdt.core.scalabuilder + + + ch.epfl.lamp.sdt.launching.SCALA_CONTAINER + org.eclipse.jdt.launching.JRE_CONTAINER + + + + + pl.project13.maven + git-commit-id-plugin + 4.9.10 + + + + revision + + + + + ${project.basedir}/.git + true + src/main/resources/git.properties + false + + org.apache.maven.plugins maven-compiler-plugin @@ -693,24 +765,24 @@ - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + - + diff --git a/obp-api/src/main/resources/props/sample.props.template b/obp-api/src/main/resources/props/sample.props.template index a3474f613e..fc7bc3c2df 100644 --- a/obp-api/src/main/resources/props/sample.props.template +++ b/obp-api/src/main/resources/props/sample.props.template @@ -916,7 +916,7 @@ featured_apis=elasticSearchWarehouseV300 # -- Rate Limiting ----------------------------------- # Define how many calls per hour a consumer can make # In case isn't defined default value is "false" -# use_consumer_limits=false +use_consumer_limits=true # In case isn't defined default value is 60 # user_consumer_limit_anonymous_access=100 # For the Rate Limiting feature we use Redis cache instance diff --git a/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala b/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala index 525b20a743..259216f2bc 100644 --- a/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala +++ b/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala @@ -46,6 +46,7 @@ import code.api.util.ApiRole.CanCreateEntitlementAtAnyBank import code.api.util.ErrorMessages.MandatoryPropertyIsNotSet import code.api.util._ import code.api.util.migration.Migration +import code.api.util.CommonsEmailWrapper import code.api.util.migration.Migration.DbFunction import code.apicollection.ApiCollection import code.apicollectionendpoint.ApiCollectionEndpoint @@ -93,12 +94,13 @@ import code.metadata.tags.MappedTag import code.metadata.transactionimages.MappedTransactionImage import code.metadata.wheretags.MappedWhereTag import code.methodrouting.MethodRouting -import code.metrics.{MappedConnectorMetric, MappedMetric, MetricArchive} +import code.metrics.{MappedConnectorMetric, MappedMetric, MetricArchive, PrometheusMetrics} import code.migration.MigrationScriptLog import code.model._ import code.model.dataAccess._ import code.model.dataAccess.internalMapping.AccountIdMapping import code.obp.grpc.HelloWorldServer +import code.payments.MappedPayment import code.productAttributeattribute.MappedProductAttribute import code.productcollection.MappedProductCollection import code.productcollectionitem.MappedProductCollectionItem @@ -136,10 +138,12 @@ import code.webuiprops.WebUiProps import com.openbankproject.commons.model.ErrorMessage import com.openbankproject.commons.util.Functions.Implicits._ import com.openbankproject.commons.util.{ApiVersion, Functions} +import io.prometheus.client.hotspot.DefaultExports import net.liftweb.common._ import net.liftweb.db.{DB, DBLogEntry} import net.liftweb.http.LiftRules.DispatchPF import net.liftweb.http._ +import net.liftweb.http.provider.servlet.HTTPRequestServlet import net.liftweb.json.Extraction import net.liftweb.mapper.{DefaultConnectionIdentifier => _, _} import net.liftweb.sitemap.Loc._ @@ -470,6 +474,8 @@ class Boot extends MdcLoggable { enableVersionIfAllowed(ApiVersion.`dynamic-endpoint`) enableVersionIfAllowed(ApiVersion.`dynamic-entity`) + PrometheusMetrics.init() + def enableOpenIdConnectApis = { // OpenIdConnect endpoint and validator if (code.api.Constant.openidConnectEnabled) { @@ -601,6 +607,9 @@ class Boot extends MdcLoggable { 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-bg-payment-request") / "confirm-bg-payment-request" >> AuthUser.loginFirst,//OAuth consent page, + Menu.i("confirm-bg-payment-request-sca") / "confirm-bg-payment-request-sca" >> AuthUser.loginFirst,//OAuth consent page, + Menu.i("confirm-bg-payment-request-redirect-uri") / "confirm-bg-payment-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++ awakePage @@ -681,11 +690,82 @@ class Boot extends MdcLoggable { ("X-Frame-Options", "DENY") :: Nil ) - + LiftRules.supplementalHeaders.default.set( + ("ASPSP-SCA-Approach", "REDIRECT") :: + Nil + ) + // Make a transaction span the whole HTTP request S.addAround(DB.buildLoanWrapper) logger.info("Note: We added S.addAround(DB.buildLoanWrapper) so each HTTP request uses ONE database transaction.") + S.addAround(new LoanWrapper { + override def apply[T](f: => T): T = { + val maybeReq = S.request + + val isApiCall = maybeReq.exists { r => + val headLower = r.path.partPath.headOption.map(_.toLowerCase) + + val berlinPrefix = "berlin-group" + val berlinPrefixSmall = "bg" + + val isApiPrefix = + headLower.exists { h => + h == berlinPrefix || + h == berlinPrefixSmall + } + + val isMetrics = headLower.contains("metrics") + isApiPrefix && !isMetrics + } + + if (!isApiCall) { + f + } else { + val req = maybeReq.openOrThrowException("Request is expected here") + + var url = req.uri + url = url.replaceAll("/[0-9a-fA-F-]{20,36}", "/uid") + url = url.replaceAll("/[A-Z]{2}[0-9A-Z]{10,32}", "/iban") + url = url.replaceAll("/[0-9]{5,33}", "/number") + url = url.replaceAll("\\?.*", "?query") + + val tppCertificate = req.request.headers.find(_.name == "TPP-Signature-Certificate") + val tpp = tppCertificate.flatMap { _ => + try { + val certificate = BerlinGroupSigning.getCertificateFromTppSignatureCertificate(req.request.headers.toList) + val subjectDN = certificate.getSubjectDN.getName + BerlinGroupSigning.cnPattern.findFirstMatchIn(subjectDN).map(_.group(1).trim) + } catch { + case _: Exception => None + } + }.getOrElse("default") + + if (tpp == "default") { + f + } else { + val start = System.nanoTime() + try { + val res = f + res match { + case lr: LiftResponse => + val code = lr.toResponse.code + PrometheusMetrics.recordApiRequest(req.request.method, url, code) + PrometheusMetrics.recordApiActiveTpp(tpp) + case _ => + } + res + } finally { + val elapsedSeconds = (System.nanoTime() - start) / 1e9 + PrometheusMetrics.recordApiLatency(elapsedSeconds) + PrometheusMetrics.recordApiLatencyByEndpoint(elapsedSeconds, url) + } + } + } + } + }) + + try { val useMessageQueue = APIUtil.getPropsAsBoolValue("messageQueue.createBankAccounts", false) if(useMessageQueue) @@ -1066,6 +1146,7 @@ object ToSchemify { MappedCustomerIdMapping, MappedProductAttribute, MappedConsent, + MappedPayment, ConsentRequest, MigrationScriptLog, MethodRouting, diff --git a/obp-api/src/main/scala/code/Payment/MappedPayment.scala b/obp-api/src/main/scala/code/Payment/MappedPayment.scala new file mode 100644 index 0000000000..33b9079506 --- /dev/null +++ b/obp-api/src/main/scala/code/Payment/MappedPayment.scala @@ -0,0 +1,40 @@ +package code.payments + +import code.util.MappedUUID +import com.openbankproject.commons.model.enums.TransactionRequestTypes +import net.liftweb.mapper.{MappedString, _} + +class MappedPayment extends LongKeyedMapper[MappedPayment] with IdPK with CreatedUpdated { + def getSingleton = MappedPayment + + object mPaymentId extends MappedUUID(this) + object mEndToEndIdentification extends MappedString(this, 50) + object mDebtorAccountIban extends MappedString(this, 50) { override def defaultValue = null } + object mInstructedAmountCurrency extends MappedString(this, 10) + object mInstructedAmountAmount extends MappedString(this, 20) + object mCreditorAccountMsisdn extends MappedString(this, 50) + object mCreditorAccountIban extends MappedString(this, 50) + object mCreditorName extends MappedString(this, 100) + object mCreditorId extends MappedString(this, 50) + object mPurposeCode extends MappedString(this, 10) + object mRemittanceInformationUnstructured extends MappedString(this, 200) + object mCreditorCtryOfRes extends MappedString(this, 5) + object mInstructionPriority extends MappedString(this, 10) + object mPurposeType extends MappedString(this, 50) + object mTppRedirectUri extends MappedString(this, 250) + object mTppNokRedirectUri extends MappedString(this, 250) + + object mStatus extends MappedString(this, 10) { override def defaultValue = "RCVD" } // Default status "RCVD" + + object mType extends MappedString(this, 50) { override def defaultValue = TransactionRequestTypes.INSTANT_CREDIT_TRANSFERS_MD.toString() } // Default type "INSTANT_CREDIT_TRANSFERS_MD" + + def status: String = mStatus.get + def transactionType: String = mType.get +} + +object MappedPayment extends MappedPayment with LongKeyedMetaMapper[MappedPayment] { + override def dbIndexes = UniqueIndex(mPaymentId) :: super.dbIndexes +} + + + diff --git a/obp-api/src/main/scala/code/Payment/PaymentProvider.scala b/obp-api/src/main/scala/code/Payment/PaymentProvider.scala new file mode 100644 index 0000000000..a46ebed953 --- /dev/null +++ b/obp-api/src/main/scala/code/Payment/PaymentProvider.scala @@ -0,0 +1,158 @@ +package code.payments + +import code.api.berlin.group.v1_3.model.TransactionStatus +import net.liftweb.http.S +import code.api.util.OBPQueryParam +import com.openbankproject.commons.model.enums.TransactionRequestTypes +import net.liftweb.common.{Box, Empty, Failure, Full} +import net.liftweb.mapper._ + +trait PaymentProvider { + protected val redirectUriValue: String = "confirm-bg-payment-request" + def approvePaymentRequestProcess(paymentId: String, debtorIban: String, purposeType: String): Unit + def cancelPaymentRequestProcess(paymentId: String): Unit + def getPaymentById(paymentId: String): Box[MappedPayment] + def getPaymentByIdAndType(paymentId: String, paymentType: String): Box[MappedPayment] + def getPaymentByEndToEndIdentification(endToEndIdentification: String): Box[MappedPayment] + def getPayments(queryParams: List[OBPQueryParam] = Nil): List[MappedPayment] + def createPayment( + endToEndIdentification: String, + debtorAccountIban: Option[String], + instructedAmountCurrency: String, + instructedAmountAmount: String, + creditorAccountMsisdn: String, + purposeCode: String, + remittanceInformationUnstructured: String, + status: TransactionStatus = TransactionStatus.RCVD, + paymentType: TransactionRequestTypes = TransactionRequestTypes.INSTANT_CREDIT_TRANSFERS_MD + ): Box[MappedPayment] + def updatePayment( + paymentId: String, + status: Option[TransactionStatus] = None, + paymentType: Option[TransactionRequestTypes] = None, + debtorAccountIban: Option[String], + purposeType: Option[String] = None + ): Box[MappedPayment] +} + +object MappedPaymentProvider extends PaymentProvider { + + override def getPaymentById(paymentId: String): Box[MappedPayment] = + MappedPayment.find(By(MappedPayment.mPaymentId, paymentId)) + + override def getPaymentByIdAndType(paymentId: String, paymentType: String): Box[MappedPayment] = + MappedPayment.find( + By(MappedPayment.mPaymentId, paymentId), + By(MappedPayment.mType, paymentType) + ) + + override def getPaymentByEndToEndIdentification(endToEndIdentification: String): Box[MappedPayment] = + MappedPayment.find(By(MappedPayment.mEndToEndIdentification, endToEndIdentification)) + + override def getPayments(queryParams: List[OBPQueryParam] = Nil): List[MappedPayment] = { + MappedPayment.findAll() + } + + override def createPayment( + endToEndIdentification: String, + debtorAccountIban: Option[String], + instructedAmountCurrency: String, + instructedAmountAmount: String, + creditorAccountMsisdn: String, + purposeCode: String, + remittanceInformationUnstructured: String, + status: TransactionStatus = TransactionStatus.RCVD, + paymentType: TransactionRequestTypes = TransactionRequestTypes.INSTANT_CREDIT_TRANSFERS_MD + ): Box[MappedPayment] = { + try { + Full( + MappedPayment.create + .mEndToEndIdentification(endToEndIdentification) + .mDebtorAccountIban(debtorAccountIban.orNull) + .mInstructedAmountCurrency(instructedAmountCurrency) + .mInstructedAmountAmount(instructedAmountAmount) + .mCreditorAccountMsisdn(creditorAccountMsisdn) + .mPurposeCode(purposeCode) + .mRemittanceInformationUnstructured(remittanceInformationUnstructured) + .mStatus(status.code) + .mType(paymentType.toString()) + .saveMe() + ) + } catch { + case e: Exception => Failure(e.getMessage) + } + } + + override def updatePayment( + paymentId: String, + status: Option[TransactionStatus] = None, + paymentType: Option[TransactionRequestTypes] = None, + debtorAccountIban: Option[String] = None, + purposeType: Option[String] = None + ): Box[MappedPayment] = { + getPaymentById(paymentId) match { + case Full(payment) => + try { + status.foreach(s => payment.mStatus(s.code)) + debtorAccountIban.foreach(iban => payment.mDebtorAccountIban(iban)) + paymentType.foreach(t => payment.mType(t.toString())) + purposeType.foreach(p => payment.mPurposeType(p.toString())) + Full(payment.saveMe()) + } catch { + case e: Exception => Failure(e.getMessage) + } + case Empty => Empty + case f: Failure => f + } + } + + def approvePaymentRequestProcess(paymentId: String, debtorIban: String, purposeType: String): Unit = { + MappedPaymentProvider.getPaymentById(paymentId) match { + case Full(payment) => + MappedPaymentProvider.updatePayment(paymentId, Some(TransactionStatus.ACCP), debtorAccountIban = Some(debtorIban), purposeType = Some(purposeType)) match { + case Full(updatedPayment) => + S.redirectTo(s"$redirectUriValue?PAYMENT_ID=${paymentId}") + case _ => + S.error("Failed to update payment status") + } + case _ => + S.error("Payment not found") + } + } + + def cancelPaymentRequestProcess(paymentId: String): Unit = { + MappedPaymentProvider.getPaymentById(paymentId) match { + case Full(payment) => + MappedPaymentProvider.updatePayment(paymentId, Some(TransactionStatus.CANC)) match { + case Full(updatedPayment) => + S.redirectTo(s"$redirectUriValue?PAYMENT_ID=${paymentId}") + case _ => + S.error("Failed to update payment status") + } + case _ => + S.error("Payment not found") + } + } + + object PurposeType extends Enumeration { + type PurposeType = Value + + val Donation = Value("Donation/Free help") + val RefundOfErroneousPayment = Value("Refund of erroneous payment") + val LoanOrFinancialHelp = Value("Loan/Financial help") + val LoanOrFinancialHelpReimbursement = Value("Loan/financial help reimbursement") + val TransferInPersonalAccount = Value("Transfer in one's own account") + val PersonalTransferFamilyExpenses = Value("Personal transfer-family expenses") + val PaymentsToBudget = Value("Payments to the budget") + + def description(purpose: PurposeType): String = purpose match { + case Donation => "Donation/Free help (Дарение/Безвозмездная помощь)" + case RefundOfErroneousPayment => "Refund of erroneous payment (Возврат неверно зачисленного платежа)" + case LoanOrFinancialHelp => "Loan / Financial help (Ссуда / Финансовая помощь)" + case LoanOrFinancialHelpReimbursement => "Loan / financial help reimbursement (Возврат займа / финансовой помощи)" + case TransferInPersonalAccount => "Transfer in one's own account (Перевод на свой счет в другом банке)" + case PersonalTransferFamilyExpenses => "Personal transfer – family expenses (Семейные расходы)" + case PaymentsToBudget => "Payments to the budget (Платежи в бюджет)" + } + } +} \ No newline at end of file diff --git a/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/MessageDocsSwaggerDefinitions.scala b/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/MessageDocsSwaggerDefinitions.scala index 2decaab967..859ebd2048 100644 --- a/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/MessageDocsSwaggerDefinitions.scala +++ b/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/MessageDocsSwaggerDefinitions.scala @@ -113,6 +113,8 @@ object MessageDocsSwaggerDefinitions correlationIdExample.value, Some(sessionIdExample.value), Some(consumerIdExample.value), + Some(consumerNameExample.value), + Some(consumerNameExample.value), generalContext = Some(List(BasicGeneralContext(keyExample.value,valueExample.value))), Some(outboundAdapterAuthInfo), Some(outboundAdapterConsenterInfo) diff --git a/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala b/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala index c7f56c3c6e..e503eb611a 100644 --- a/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala +++ b/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala @@ -2707,6 +2707,7 @@ object SwaggerDefinitionsJSON { consumer_id = consumerIdExample.value, consumer_key = consumerKeyExample.value, app_name = appNameExample.value, + app_alias = appNameExample.value, app_type = appTypeExample.value, description = descriptionExample.value, developer_email = emailExample.value, @@ -2717,6 +2718,7 @@ object SwaggerDefinitionsJSON { created_by_user = resourceUserJSON, enabled = true, created = DateWithDayExampleObject, + updated = DateWithDayExampleObject, logo_url = Some(logoURLExample.value) ) lazy val consumerJsonOnlyForPostResponseV510: ConsumerJsonOnlyForPostResponseV510 = ConsumerJsonOnlyForPostResponseV510( 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 d3684b2681..4c3592abf6 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 @@ -30,6 +30,7 @@ import net.liftweb.http.provider.HTTPParam import net.liftweb.http.rest.RestHelper import net.liftweb.json import net.liftweb.json._ +import net.liftweb.http.S import scala.collection.mutable.ArrayBuffer import scala.concurrent.Future @@ -339,48 +340,46 @@ of the PSU at this ASPSP. ApiTag("Account Information Service (AIS)") :: apiTagBerlinGroupM :: Nil ) - lazy val getAccountList : OBPEndpoint = { - case "accounts" :: Nil JsonGet _ => { - cc => - for { - (Full(u), callContext) <- authenticatedAccess(cc) - withBalanceParam <- NewStyle.function.tryons(s"$InvalidUrlParameters withBalance parameter can only take two values: TRUE or FALSE!", 400, callContext) { - - val withBalance = APIUtil.getHttpRequestUrlParam(cc.url, "withBalance") - - if(withBalance.isEmpty)Some(false) else Some(withBalance.toBoolean) - } - _ <- passesPsd2Aisp(callContext) - (availablePrivateAccounts, callContext) <- NewStyle.function.getAccountListOfBerlinGroup(u, callContext) - (canReadBalancesAccounts, callContext) <- NewStyle.function.getAccountCanReadBalancesOfBerlinGroup(u, callContext) - (canReadTransactionsAccounts, callContext) <- NewStyle.function.getAccountCanReadTransactionsOfBerlinGroup(u, callContext) - (accounts, callContext) <- NewStyle.function.getBankAccounts(availablePrivateAccounts, callContext) - bankAccountsFiltered = accounts.filter(bankAccount => - bankAccount.attributes.toList.flatten.find(attribute => - attribute.name.equals("CashAccountTypeCode")&& + lazy val getAccountList : OBPEndpoint = { + case "accounts" :: Nil JsonGet _ => { + cc => + for { + (Full(u), callContext) <- authenticatedAccess(cc) + withBalanceParam <- NewStyle.function.tryons(s"$InvalidUrlParameters withBalance parameter can only take two values: TRUE or FALSE!", 400, callContext) { + val withBalance = APIUtil.getHttpRequestUrlParam(cc.url, "withBalance") + if(withBalance.isEmpty)Some(false) else Some(withBalance.toBoolean) + } + _ <- passesPsd2Aisp(callContext) + (availablePrivateAccounts, callContext) <- NewStyle.function.getAccountListOfBerlinGroup(u, callContext) + (canReadBalancesAccounts, callContext) <- NewStyle.function.getAccountCanReadBalancesOfBerlinGroup(u, callContext) + (canReadTransactionsAccounts, callContext) <- NewStyle.function.getAccountCanReadTransactionsOfBerlinGroup(u, callContext) + (accounts, callContext) <- NewStyle.function.getBankAccounts(availablePrivateAccounts, callContext) + bankAccountsFiltered = accounts.filter(bankAccount => + bankAccount.attributes.toList.flatten.find(attribute => + attribute.name.equals("CashAccountTypeCode")&& attribute.`type`.equals("STRING")&& attribute.value.equalsIgnoreCase("card") - ).isEmpty) - - (balances, callContext) <- code.api.util.newstyle.BankAccountBalanceNewStyle.getBankAccountsBalances( - bankAccountsFiltered.map(_.accountId), - callContext - ) - - } yield { - (JSONFactory_BERLIN_GROUP_1_3.createAccountListJson( - bankAccountsFiltered, - canReadBalancesAccounts, - canReadTransactionsAccounts, - u, - withBalanceParam, - balances - ), callContext) - } - } - } + ).isEmpty) + + (balances, callContext) <- code.api.util.newstyle.BankAccountBalanceNewStyle.getBankAccountsBalances( + bankAccountsFiltered.map(_.accountId), + callContext + ) + + } yield { + (JSONFactory_BERLIN_GROUP_1_3.createAccountListJson( + bankAccountsFiltered, + canReadBalancesAccounts, + canReadTransactionsAccounts, + u, + withBalanceParam, + balances + ), callContext) + } + } + } - resourceDocs += ResourceDoc( + resourceDocs += ResourceDoc( getBalances, apiVersion, nameOf(getBalances), @@ -745,7 +744,7 @@ where the consent was directly managed between ASPSP and PSU e.g. in a re-direct (_, callContext) <- applicationAccess(cc) _ <- passesPsd2Aisp(callContext) consent <- Future(Consents.consentProvider.vend.getConsentByConsentId(consentId)) map { - unboxFullOrFail(_, callContext, s"$ConsentNotFound ($consentId)") + unboxFullOrFail(_, callContext, s"$ConsentNotFound ($consentId)", 403) } consumerIdFromConsent = consent.mConsumerId.get consumerIdFromCurrentCall = callContext.map(_.consumer.map(_.consumerId.get).getOrElse("None")).getOrElse("None") 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 646873c1f5..a4f76c6f98 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 @@ -33,11 +33,12 @@ object BgSpecValidation { try { val date = LocalDate.parse(dateStr, DateFormat) val today = LocalDate.now() + val maxValidDaysCurrent = today.plusDays(180) if (date.isBefore(today)) { Left(s"$InvalidDateFormat The `validUntil` date ($dateStr) cannot be in the past!") - } else if (date.isAfter(MaxValidDays)) { - Left(s"$InvalidDateFormat The `validUntil` date ($dateStr) exceeds the maximum allowed period of 180 days (until $MaxValidDays).") + } else if (date.isAfter(maxValidDaysCurrent)) { + Left(s"$InvalidDateFormat The `validUntil` date ($dateStr) exceeds the maximum allowed period of 180 days (until $maxValidDaysCurrent). today: ($today)") } else { Right(date) // Valid date (inclusive of 180 days) } @@ -54,5 +55,4 @@ object BgSpecValidation { localDate.format(DateTimeFormatter.ISO_LOCAL_DATE) } } - } 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 78a7e747ab..18eb4146fd 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 @@ -2,7 +2,8 @@ package code.api.berlin.group.v1_3 import code.api.Constant.bgRemoveSignOfAmounts import code.api.berlin.group.ConstantsBG -import code.api.berlin.group.v1_3.model.TransactionStatus.mapTransactionStatus +import code.api.berlin.group.v1_3.JSONFactory_BERLIN_GROUP_1_3.LinkHrefJson +import code.api.berlin.group.v1_3.model.TransactionStatus.{mapTransactionStatus, mapTransactionStatusMdV1} import code.api.berlin.group.v1_3.model._ import code.api.util.APIUtil._ import code.api.util.ErrorMessages.MissingPropsValueAtThisInstance @@ -12,7 +13,7 @@ import code.model.ModeratedTransaction import code.util.Helper.MdcLoggable import com.openbankproject.commons.ExecutionContext.Implicits.global import com.openbankproject.commons.model._ -import com.openbankproject.commons.model.enums.{AccountRoutingScheme, TransactionRequestStatus} +import com.openbankproject.commons.model.enums.{AccountRoutingScheme, TransactionRequestStatus, TransactionRequestTypes} import net.liftweb.common.Box.tryo import net.liftweb.common.{Box, Full} import net.liftweb.json.{JValue, parse} @@ -286,7 +287,13 @@ object JSONFactory_BERLIN_GROUP_1_3 extends CustomJsonFormats with MdcLoggable{ self: LinkHrefJson, status: LinkHrefJson, scaStatus: LinkHrefJson - ) + ) + case class InitiatePaymentMdV1ResponseLinks( + scaRedirect: LinkHrefJson, + self: LinkHrefJson, + status: LinkHrefJson, + scaStatus: Option[LinkHrefJson] + ) case class CancelPaymentResponseLinks( self: LinkHrefJson, status: LinkHrefJson, @@ -297,6 +304,11 @@ object JSONFactory_BERLIN_GROUP_1_3 extends CustomJsonFormats with MdcLoggable{ paymentId: String, _links: InitiatePaymentResponseLinks ) + case class InitiatePaymentMdV1ResponseJson( + transactionStatus: String, + paymentId: String, + _links: InitiatePaymentMdV1ResponseLinks + ) case class CancelPaymentResponseJson( transactionStatus: String, _links: CancelPaymentResponseLinks @@ -728,7 +740,7 @@ object JSONFactory_BERLIN_GROUP_1_3 extends CustomJsonFormats with MdcLoggable{ ) } - def createTransactionRequestJson(transactionRequest : TransactionRequestBGV1) : InitiatePaymentResponseJson = { + def createTransactionRequestJson(transactionRequest : TransactionRequestBGV1, transactionRequestType: TransactionRequestTypes) : InitiatePaymentMdV1ResponseJson = { // - 'ACCC': 'AcceptedSettlementCompleted' - // Settlement on the creditor's account has been completed. // - 'ACCP': 'AcceptedCustomerProfile' - @@ -772,17 +784,32 @@ object JSONFactory_BERLIN_GROUP_1_3 extends CustomJsonFormats with MdcLoggable{ scaRedirectUrlPattern.replace("PLACEHOLDER", paymentId) else s"$scaRedirectUrlPattern/${paymentId}" - InitiatePaymentResponseJson( - transactionStatus = mapTransactionStatus(transactionRequest.status), + val self = + if(transactionRequestType == TransactionRequestTypes.INSTANT_CREDIT_TRANSFERS_MD) + LinkHrefJson(s"/${ConstantsBG.berlinGroupVersion1.apiShortVersion}/payments/instant-credit-transfers-md/$paymentId") + else if (transactionRequestType == TransactionRequestTypes.DOMESTIC_CREDIT_TRANSFERS_MD) + LinkHrefJson(s"/${ConstantsBG.berlinGroupVersion1.apiShortVersion}/payments/domestic-credit-transfers-md/$paymentId") + else + LinkHrefJson(s"/${ConstantsBG.berlinGroupVersion1.apiShortVersion}/payments/sepa-credit-transfers/$paymentId") + val scaStatus = + if(transactionRequestType == TransactionRequestTypes.INSTANT_CREDIT_TRANSFERS_MD + || transactionRequestType == TransactionRequestTypes.DOMESTIC_CREDIT_TRANSFERS_MD) + None + else + Some(LinkHrefJson(s"/${ConstantsBG.berlinGroupVersion1.apiShortVersion}/payments/$paymentId/authorisations/$paymentId")) + + InitiatePaymentMdV1ResponseJson( + transactionStatus = mapTransactionStatusMdV1(transactionRequest.status), paymentId = paymentId, - _links = InitiatePaymentResponseLinks( - scaRedirect = LinkHrefJson(s"$scaRedirectUrl/$paymentId"), - self = LinkHrefJson(s"/${ConstantsBG.berlinGroupVersion1.apiShortVersion}/payments/sepa-credit-transfers/$paymentId"), - status = LinkHrefJson(s"/${ConstantsBG.berlinGroupVersion1.apiShortVersion}/payments/$paymentId/status"), - scaStatus = LinkHrefJson(s"/${ConstantsBG.berlinGroupVersion1.apiShortVersion}/payments/$paymentId/authorisations/${paymentId}") + _links = InitiatePaymentMdV1ResponseLinks( + scaRedirect = LinkHrefJson(s"$scaRedirectUrl"), + self = self, + status = LinkHrefJson(s"${self.href}/status"), + scaStatus = scaStatus ) ) } + def createCancellationTransactionRequestJson(transactionRequest : TransactionRequest) : CancelPaymentResponseJson = { val paymentId = transactionRequest.id.value CancelPaymentResponseJson( 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 0d39f7502c..7cab0ac737 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 @@ -2,10 +2,10 @@ package code.api.builder.PaymentInitiationServicePISApi import code.api.berlin.group.ConstantsBG 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.model.TransactionStatus.mapTransactionStatus +import code.api.berlin.group.v1_3.model.TransactionStatus.{mapTransactionStatus, mapTransactionStatusMdV1} 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.APIUtil.{applicationAccess, _} import code.api.util.ApiTag._ import code.api.util.ErrorMessages._ import code.api.util.NewStyle.HttpCode @@ -25,15 +25,17 @@ import net.liftweb.http.js.JE.JsRaw import net.liftweb.http.rest.RestHelper import net.liftweb.json import net.liftweb.json._ +import code.payments.{MappedPayment, MappedPaymentProvider} +import com.openbankproject.adapter.akka.commons.actor.toOption import scala.collection.mutable.ArrayBuffer import scala.concurrent.Future object APIMethods_PaymentInitiationServicePISApi extends RestHelper { - val apiVersion = ConstantsBG.berlinGroupVersion1 - val resourceDocs = ArrayBuffer[ResourceDoc]() - val apiRelations = ArrayBuffer[ApiRelation]() - protected implicit def JvalueToSuper(what: JValue): JvalueCaseClass = JvalueCaseClass(what) + val apiVersion = ConstantsBG.berlinGroupVersion1 + val resourceDocs = ArrayBuffer[ResourceDoc]() + val apiRelations = ArrayBuffer[ApiRelation]() + protected implicit def JvalueToSuper(what: JValue): JvalueCaseClass = JvalueCaseClass(what) def checkPaymentServerTypeError(paymentService: String) = { val ccc = "" @@ -45,14 +47,16 @@ object APIMethods_PaymentInitiationServicePISApi extends RestHelper { PaymentServiceTypes.withName(paymentService.replaceAll("-","_")) }.isDefined - val endpoints = - cancelPayment :: + val endpoints = + cancelPayment :: getPaymentCancellationScaStatus :: - getPaymentInformation :: +/* getPaymentInformation ::*/ + getPaymentInformationMD :: + getPaymentInformationStatusMD :: getPaymentInitiationAuthorisation :: getPaymentInitiationCancellationAuthorisationInformation :: getPaymentInitiationScaStatus :: - getPaymentInitiationStatus :: +/* getPaymentInitiationStatus :: */ initiatePayments :: initiateBulkPayments :: initiatePeriodicPayments :: @@ -72,144 +76,144 @@ object APIMethods_PaymentInitiationServicePISApi extends RestHelper { updatePaymentPsuDataAuthorisationConfirmation :: Nil - - resourceDocs += ResourceDoc( - cancelPayment, - apiVersion, - nameOf(cancelPayment), - "DELETE", - "/PAYMENT_SERVICE/PAYMENT_PRODUCT/PAYMENTID", - "Payment Cancellation Request", - s"""${mockedDataText(false)} -This method initiates the cancellation of a payment. Depending on the payment-service, the payment-product -and the ASPSP's implementation, this TPP call might be sufficient to cancel a payment. If an authorisation -of the payment cancellation is mandated by the ASPSP, a corresponding hyperlink will be contained in the -response message. Cancels the addressed payment with resource identification paymentId if applicable to the -payment-service, payment-product and received in product related timelines (e.g. before end of business day -for scheduled payments of the last business day before the scheduled execution day). The response to this -DELETE command will tell the TPP whether the * access method was rejected * access method was successful, + + resourceDocs += ResourceDoc( + cancelPayment, + apiVersion, + nameOf(cancelPayment), + "DELETE", + "/PAYMENT_SERVICE/PAYMENT_PRODUCT/PAYMENTID", + "Payment Cancellation Request", + s"""${mockedDataText(false)} +This method initiates the cancellation of a payment. Depending on the payment-service, the payment-product +and the ASPSP's implementation, this TPP call might be sufficient to cancel a payment. If an authorisation +of the payment cancellation is mandated by the ASPSP, a corresponding hyperlink will be contained in the +response message. Cancels the addressed payment with resource identification paymentId if applicable to the +payment-service, payment-product and received in product related timelines (e.g. before end of business day +for scheduled payments of the last business day before the scheduled execution day). The response to this +DELETE command will tell the TPP whether the * access method was rejected * access method was successful, or * access method is generally applicable, but further authorisation processes are needed. """, - EmptyBody, - CancelPaymentResponseJson( - "ACTC", - _links = CancelPaymentResponseLinks( - self = LinkHrefJson(s"/v1.3/payments/sepa-credit-transfers/1234-wertiq-983"), - status = LinkHrefJson(s"/v1.3/payments/sepa-credit-transfers/1234-wertiq-983/status"), - startAuthorisation = LinkHrefJson(s"/v1.3/payments/sepa-credit-transfers/cancellation-authorisations/1234-wertiq-983/status") - ) - ), - List(UserNotLoggedIn, UnknownError), - ApiTag("Payment Initiation Service (PIS)") :: Nil - ) - - lazy val cancelPayment : OBPEndpoint = { - case paymentService :: paymentProduct :: paymentId :: Nil JsonDelete _ => { - cc => - for { - (Full(u), callContext) <- authenticatedAccess(cc) - _ <- passesPsd2Pisp(callContext) - _ <- NewStyle.function.tryons(checkPaymentServerTypeError(paymentService),404, callContext) { - PaymentServiceTypes.withName(paymentService.replaceAll("-","_")) - } - transactionRequestTypes <- NewStyle.function.tryons(checkPaymentProductError(paymentProduct),404, callContext) { - TransactionRequestTypes.withName(paymentProduct.replaceAll("-","_").toUpperCase) - } - (transactionRequest, callContext) <- NewStyle.function.getTransactionRequestImpl(TransactionRequestId(paymentId), callContext) - - transactionRequestBody <- NewStyle.function.tryons(s"${UnknownError} No data for Payment Body ",400, callContext) { - transactionRequest.body.to_sepa_credit_transfers.get - } - fromAccountIban = transactionRequestBody.debtorAccount.iban - toAccountIban = transactionRequestBody.creditorAccount.iban - (_, callContext) <- NewStyle.function.getBankAccountByIban(fromAccountIban, callContext) - (ibanChecker, callContext) <- NewStyle.function.validateAndCheckIbanNumber(toAccountIban, callContext) - _ <- Helper.booleanToFuture(invalidIban, cc=callContext) { ibanChecker.isValid == true } - (_, callContext) <- NewStyle.function.getToBankAccountByIban(toAccountIban, callContext) - (canBeCancelled, _, startSca) <- transactionRequestTypes match { - case TransactionRequestTypes.SEPA_CREDIT_TRANSFERS => { - transactionRequest.status.toUpperCase() match { - 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 => - NewStyle.function.saveTransactionRequestStatusImpl(transactionRequest.id, CANCELLATION_PENDING.toString, callContext) - (true, x._2, Some(startSca)) - case CancelPayment(true, Some(startSca)) if startSca == false => - NewStyle.function.saveTransactionRequestStatusImpl(transactionRequest.id, CANCELLED.toString, callContext) - (true, x._2, Some(startSca)) - case CancelPayment(false, _) => - (false, x._2, Some(false)) - } - } - case "INITIATED" => - NewStyle.function.saveTransactionRequestStatusImpl(transactionRequest.id, CANCELLED.toString, callContext) - Future(true, callContext, Some(false)) - case "CANCELLED" => - Future(true, callContext, Some(false)) - } - } - } - _ <- Helper.booleanToFuture(failMsg= TransactionRequestCannotBeCancelled, cc=callContext) { canBeCancelled == true } - (updatedTransactionRequest, callContext) <- NewStyle.function.getTransactionRequestImpl(TransactionRequestId(paymentId), callContext) - } yield { - startSca.getOrElse(false) match { - case true => (createCancellationTransactionRequestJson(updatedTransactionRequest), HttpCode.`202`(callContext)) - case false => (JsRaw(""), HttpCode.`204`(callContext)) - } - } - } - } - - resourceDocs += ResourceDoc( - getPaymentCancellationScaStatus, - apiVersion, - nameOf(getPaymentCancellationScaStatus), - "GET", - "/PAYMENT_SERVICE/PAYMENT_PRODUCT/PAYMENTID/cancellation-authorisations/CANCELLATIONID", - "Read the SCA status of the payment cancellation's authorisation.", - s"""${mockedDataText(false)} + EmptyBody, + CancelPaymentResponseJson( + "ACTC", + _links = CancelPaymentResponseLinks( + self = LinkHrefJson(s"/v1.3/payments/sepa-credit-transfers/1234-wertiq-983"), + status = LinkHrefJson(s"/v1.3/payments/sepa-credit-transfers/1234-wertiq-983/status"), + startAuthorisation = LinkHrefJson(s"/v1.3/payments/sepa-credit-transfers/cancellation-authorisations/1234-wertiq-983/status") + ) + ), + List(UserNotLoggedIn, UnknownError), + ApiTag("Payment Initiation Service (PIS)") :: Nil + ) + + lazy val cancelPayment : OBPEndpoint = { + case paymentService :: paymentProduct :: paymentId :: Nil JsonDelete _ => { + cc => + for { + (Full(u), callContext) <- authenticatedAccess(cc) + _ <- passesPsd2Pisp(callContext) + _ <- NewStyle.function.tryons(checkPaymentServerTypeError(paymentService),404, callContext) { + PaymentServiceTypes.withName(paymentService.replaceAll("-","_")) + } + transactionRequestTypes <- NewStyle.function.tryons(checkPaymentProductError(paymentProduct),404, callContext) { + TransactionRequestTypes.withName(paymentProduct.replaceAll("-","_").toUpperCase) + } + (transactionRequest, callContext) <- NewStyle.function.getTransactionRequestImpl(TransactionRequestId(paymentId), callContext) + + transactionRequestBody <- NewStyle.function.tryons(s"${UnknownError} No data for Payment Body ",400, callContext) { + transactionRequest.body.to_sepa_credit_transfers.get + } + fromAccountIban = transactionRequestBody.debtorAccount.iban + toAccountIban = transactionRequestBody.creditorAccount.iban + (_, callContext) <- NewStyle.function.getBankAccountByIban(fromAccountIban, callContext) + (ibanChecker, callContext) <- NewStyle.function.validateAndCheckIbanNumber(toAccountIban, callContext) + _ <- Helper.booleanToFuture(invalidIban, cc=callContext) { ibanChecker.isValid == true } + (_, callContext) <- NewStyle.function.getToBankAccountByIban(toAccountIban, callContext) + (canBeCancelled, _, startSca) <- transactionRequestTypes match { + case TransactionRequestTypes.SEPA_CREDIT_TRANSFERS => { + transactionRequest.status.toUpperCase() match { + 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 => + NewStyle.function.saveTransactionRequestStatusImpl(transactionRequest.id, CANCELLATION_PENDING.toString, callContext) + (true, x._2, Some(startSca)) + case CancelPayment(true, Some(startSca)) if startSca == false => + NewStyle.function.saveTransactionRequestStatusImpl(transactionRequest.id, CANCELLED.toString, callContext) + (true, x._2, Some(startSca)) + case CancelPayment(false, _) => + (false, x._2, Some(false)) + } + } + case "INITIATED" => + NewStyle.function.saveTransactionRequestStatusImpl(transactionRequest.id, CANCELLED.toString, callContext) + Future(true, callContext, Some(false)) + case "CANCELLED" => + Future(true, callContext, Some(false)) + } + } + } + _ <- Helper.booleanToFuture(failMsg= TransactionRequestCannotBeCancelled, cc=callContext) { canBeCancelled == true } + (updatedTransactionRequest, callContext) <- NewStyle.function.getTransactionRequestImpl(TransactionRequestId(paymentId), callContext) + } yield { + startSca.getOrElse(false) match { + case true => (createCancellationTransactionRequestJson(updatedTransactionRequest), HttpCode.`202`(callContext)) + case false => (JsRaw(""), HttpCode.`204`(callContext)) + } + } + } + } + + resourceDocs += ResourceDoc( + getPaymentCancellationScaStatus, + apiVersion, + nameOf(getPaymentCancellationScaStatus), + "GET", + "/PAYMENT_SERVICE/PAYMENT_PRODUCT/PAYMENTID/cancellation-authorisations/CANCELLATIONID", + "Read the SCA status of the payment cancellation's authorisation.", + s"""${mockedDataText(false)} This method returns the SCA status of a payment initiation's authorisation sub-resource. """, - EmptyBody, - json.parse("""{ + EmptyBody, + json.parse("""{ "scaStatus" : "psuAuthenticated" }"""), - List(UserNotLoggedIn, UnknownError), - ApiTag("Payment Initiation Service (PIS)") :: apiTagBerlinGroupM :: Nil - ) - - lazy val getPaymentCancellationScaStatus : OBPEndpoint = { - case paymentService :: paymentProduct :: paymentId:: "cancellation-authorisations" :: cancellationId :: Nil JsonGet _ => { - cc => - for { - (_, callContext) <- authenticatedAccess(cc) - _ <- passesPsd2Pisp(callContext) - _ <- NewStyle.function.tryons(checkPaymentServerTypeError(paymentService),404, callContext) { - PaymentServiceTypes.withName(paymentService.replaceAll("-","_")) - } - _ <- NewStyle.function.tryons(checkPaymentProductError(paymentProduct),404, callContext) { - TransactionRequestTypes.withName(paymentProduct.replaceAll("-","_").toUpperCase) - } - (_, callContext) <- NewStyle.function.getTransactionRequestImpl(TransactionRequestId(paymentId), callContext) - (challenge, callContext) <- NewStyle.function.getChallenge(cancellationId, callContext) - } yield { - (JSONFactory_BERLIN_GROUP_1_3.ScaStatusJsonV13(challenge.scaStatus.map(_.toString).getOrElse("None")), HttpCode.`200`(callContext)) - } - } - } - - resourceDocs += ResourceDoc( - getPaymentInformation, - apiVersion, - nameOf(getPaymentInformation), - "GET", - "/PAYMENT_SERVICE/PAYMENT_PRODUCT/PAYMENTID", - "Get Payment Information", - s"""${mockedDataText(false)} + List(UserNotLoggedIn, UnknownError), + ApiTag("Payment Initiation Service (PIS)") :: apiTagBerlinGroupM :: Nil + ) + + lazy val getPaymentCancellationScaStatus : OBPEndpoint = { + case paymentService :: paymentProduct :: paymentId:: "cancellation-authorisations" :: cancellationId :: Nil JsonGet _ => { + cc => + for { + (_, callContext) <- authenticatedAccess(cc) + _ <- passesPsd2Pisp(callContext) + _ <- NewStyle.function.tryons(checkPaymentServerTypeError(paymentService),404, callContext) { + PaymentServiceTypes.withName(paymentService.replaceAll("-","_")) + } + _ <- NewStyle.function.tryons(checkPaymentProductError(paymentProduct),404, callContext) { + TransactionRequestTypes.withName(paymentProduct.replaceAll("-","_").toUpperCase) + } + (_, callContext) <- NewStyle.function.getTransactionRequestImpl(TransactionRequestId(paymentId), callContext) + (challenge, callContext) <- NewStyle.function.getChallenge(cancellationId, callContext) + } yield { + (JSONFactory_BERLIN_GROUP_1_3.ScaStatusJsonV13(challenge.scaStatus.map(_.toString).getOrElse("None")), HttpCode.`200`(callContext)) + } + } + } + + resourceDocs += ResourceDoc( + getPaymentInformation, + apiVersion, + nameOf(getPaymentInformation), + "GET", + "/PAYMENT_SERVICE/PAYMENT_PRODUCT/PAYMENTID", + "Get Payment Information", + s"""${mockedDataText(false)} Returns the content of a payment object""", - EmptyBody, - json.parse("""{ + EmptyBody, + json.parse("""{ "debtorAccount":{ "iban":"GR12 1234 5123 4511 3981 4475 477" }, @@ -222,48 +226,147 @@ Returns the content of a payment object""", }, "creditorName":"70charname" }"""), - List(UserNotLoggedIn, UnknownError), - ApiTag("Payment Initiation Service (PIS)") :: apiTagBerlinGroupM ::Nil - ) - - lazy val getPaymentInformation : OBPEndpoint = { - case paymentService :: paymentProduct :: paymentId :: Nil JsonGet _ if checkPaymentServiceType(paymentService) => { - cc => - for { - (Full(u), callContext) <- authenticatedAccess(cc) - _ <- passesPsd2Pisp(callContext) - _ <- NewStyle.function.tryons(checkPaymentServerTypeError(paymentService),404, callContext) { - PaymentServiceTypes.withName(paymentService.replaceAll("-","_")) - } - transactionRequestTypes <- NewStyle.function.tryons(checkPaymentProductError(paymentProduct),404, callContext) { - TransactionRequestTypes.withName(paymentProduct.replaceAll("-","_").toUpperCase) - } - (transactionRequest, callContext) <- NewStyle.function.getTransactionRequestImpl(TransactionRequestId(paymentId), callContext) - - transactionRequestBody <- NewStyle.function.tryons(s"${UnknownError} No data for Payment Body ",400, callContext) { - transactionRequest.body.to_sepa_credit_transfers.get - } - - } yield { - (transactionRequestBody, callContext) - } - } - } - - resourceDocs += ResourceDoc( - getPaymentInitiationAuthorisation, - apiVersion, - nameOf(getPaymentInitiationAuthorisation), - "GET", - "/PAYMENT_SERVICE/PAYMENT_PRODUCT/PAYMENTID/authorisations", - "Get Payment Initiation Authorisation Sub-Resources Request", - s"""${mockedDataText(false)} + List(UserNotLoggedIn, UnknownError), + ApiTag("Payment Initiation Service (PIS)") :: apiTagBerlinGroupM ::Nil + ) + + lazy val getPaymentInformation : OBPEndpoint = { + case paymentService :: paymentProduct :: paymentId :: Nil JsonGet _ if checkPaymentServiceType(paymentService) => { + cc => + for { + (Full(u), callContext) <- authenticatedAccess(cc) + _ <- passesPsd2Pisp(callContext) + _ <- NewStyle.function.tryons(checkPaymentServerTypeError(paymentService),404, callContext) { + PaymentServiceTypes.withName(paymentService.replaceAll("-","_")) + } + transactionRequestTypes <- NewStyle.function.tryons(checkPaymentProductError(paymentProduct),404, callContext) { + TransactionRequestTypes.withName(paymentProduct.replaceAll("-","_").toUpperCase) + } + (transactionRequest, callContext) <- NewStyle.function.getTransactionRequestImpl(TransactionRequestId(paymentId), callContext) + + transactionRequestBody <- NewStyle.function.tryons(s"${UnknownError} No data for Payment Body ",400, callContext) { + transactionRequest.body.to_sepa_credit_transfers.get + } + + } yield { + (transactionRequestBody, callContext) + } + } + } + + resourceDocs += ResourceDoc( + getPaymentInformationMD, + apiVersion, + nameOf(getPaymentInformationMD), + "GET", + "/payments/PAYMENT_PRODUCT/PAYMENTID", + "Get Payment Information", + s"""${mockedDataText(false)} + Returns the content of a payment object""", + EmptyBody, + json.parse("""{ + "paymentId" : "MD123456789" , + "debtorAccount" : { + "iban" : "MD12AA000001100032130935" + }, + "instructedAmount" : { + "currency" : "MDL" , + "amount" : "1000.00" + }, + "creditorAccount" : { + "msisdn" : "37399000000" + }, + "remittanceInformationUnstructured" : "Plata P2P" , + "transactionStatus" : "RCVD" + }"""), + List(UserNotLoggedIn, UnknownError), + ApiTag("Payment Initiation Service (PIS)") :: apiTagBerlinGroupM :: Nil + ) + + lazy val getPaymentInformationMD: OBPEndpoint = { + case "payments" :: paymentProduct :: paymentId :: Nil JsonGet _ => { + cc => + getPaymentInformationImplementationMD(paymentProduct, paymentId, false, cc) + } + } + resourceDocs += ResourceDoc( + getPaymentInformationStatusMD, + apiVersion, + nameOf(getPaymentInformationStatusMD), + "GET", + "/payments/PAYMENT_PRODUCT/PAYMENTID/status", + "Get Payment Satus Information", + s"""${mockedDataText(false)} + Returns the content of a payment object""", + EmptyBody, + json.parse("""{ + "transactionStatus" : "RCVD" + }"""), + List(UserNotLoggedIn, UnknownError), + ApiTag("Payment Initiation Service (PIS)") :: apiTagBerlinGroupM :: Nil + ) + + lazy val getPaymentInformationStatusMD: OBPEndpoint = { + case "payments" :: paymentProduct :: paymentId :: "status" :: Nil JsonGet _ => { + cc => + getPaymentInformationImplementationMD(paymentProduct, paymentId, true, cc) + } + } + + + def getPaymentInformationImplementationMD(paymentProduct: String, paymentId: String, getStatus: Boolean, cc: CallContext) = { + for { + (u, callContext) <- applicationAccess(cc); _ <- passesPsd2Pisp(callContext) + transactionRequestType <- NewStyle.function.tryons(checkPaymentProductError(paymentProduct), 404, callContext) { + TransactionRequestTypes.withName(paymentProduct.replaceAll("-", "_").toUpperCase) + } + (paymentResponse, callContext) <- transactionRequestType match { + case TransactionRequestTypes.INSTANT_CREDIT_TRANSFERS_MD => + NewStyle.function.getInstantPaymentInformationMdBGV1( + paymentId = paymentId, + callContext = callContext + ) + case TransactionRequestTypes.DOMESTIC_CREDIT_TRANSFERS_MD => + NewStyle.function.getDomesticPaymentInformationMdBGV1( + paymentId = paymentId, + callContext = callContext + ) + case _ => + Future.failed(new RuntimeException("Unsupported transaction type")) + } + + paymentResponseBody = if (getStatus) { + paymentResponse match { + case instantPayment: InstantPaymentInformation => + InstantPaymentStatus(mapTransactionStatusMdV1(instantPayment.transactionStatus)) + case domesticPayment: DomesticPaymentInformationResponse => + InstantPaymentStatus(mapTransactionStatusMdV1(domesticPayment.transactionStatus)) + case _ => + InstantPaymentStatus("Unsupported payment response type") + } + } else { + paymentResponse + } + } yield { + (paymentResponseBody, callContext) + } + } + + + resourceDocs += ResourceDoc( + getPaymentInitiationAuthorisation, + apiVersion, + nameOf(getPaymentInitiationAuthorisation), + "GET", + "/PAYMENT_SERVICE/PAYMENT_PRODUCT/PAYMENTID/authorisations", + "Get Payment Initiation Authorisation Sub-Resources Request", + s"""${mockedDataText(false)} Read a list of all authorisation subresources IDs which have been created. This function returns an array of hyperlinks to all generated authorisation sub-resources. """, - EmptyBody, - json.parse("""[ + EmptyBody, + json.parse("""[ { "scaStatus": "received", "authorisationId": "940948c7-1c86-4d88-977e-e739bf2c1492", @@ -281,176 +384,176 @@ This function returns an array of hyperlinks to all generated authorisation sub- } } ]"""), - List(UserNotLoggedIn, UnknownError), - ApiTag("Payment Initiation Service (PIS)") :: apiTagBerlinGroupM :: Nil - ) - - lazy val getPaymentInitiationAuthorisation : OBPEndpoint = { - case paymentService :: paymentProduct :: paymentId :: "authorisations" :: Nil JsonGet _ => { - cc => - for { - (_, callContext) <- authenticatedAccess(cc) - _ <- passesPsd2Pisp(callContext) - _ <- NewStyle.function.tryons(checkPaymentServerTypeError(paymentService),404, callContext) { - PaymentServiceTypes.withName(paymentService.replaceAll("-","_")) - } - _ <- NewStyle.function.tryons(checkPaymentProductError(paymentProduct),404, callContext) { - TransactionRequestTypes.withName(paymentProduct.replaceAll("-","_").toUpperCase) - } - (_, callContext) <- NewStyle.function.getTransactionRequestImpl(TransactionRequestId(paymentId), callContext) - (challenges, callContext) <- NewStyle.function.getChallengesByTransactionRequestId(paymentId, callContext) - } yield { - (JSONFactory_BERLIN_GROUP_1_3.createStartPaymentAuthorisationsJson(challenges), callContext) - } - } - } - - resourceDocs += ResourceDoc( - getPaymentInitiationCancellationAuthorisationInformation, - apiVersion, - nameOf(getPaymentInitiationCancellationAuthorisationInformation), - "GET", - "/PAYMENT_SERVICE/PAYMENT_PRODUCT/PAYMENTID/cancellation-authorisations", - "Get Cancellation Authorisation Sub-Resources Request", - s"""${mockedDataText(false)} + List(UserNotLoggedIn, UnknownError), + ApiTag("Payment Initiation Service (PIS)") :: apiTagBerlinGroupM :: Nil + ) + + lazy val getPaymentInitiationAuthorisation : OBPEndpoint = { + case paymentService :: paymentProduct :: paymentId :: "authorisations" :: Nil JsonGet _ => { + cc => + for { + (_, callContext) <- authenticatedAccess(cc) + _ <- passesPsd2Pisp(callContext) + _ <- NewStyle.function.tryons(checkPaymentServerTypeError(paymentService),404, callContext) { + PaymentServiceTypes.withName(paymentService.replaceAll("-","_")) + } + _ <- NewStyle.function.tryons(checkPaymentProductError(paymentProduct),404, callContext) { + TransactionRequestTypes.withName(paymentProduct.replaceAll("-","_").toUpperCase) + } + (_, callContext) <- NewStyle.function.getTransactionRequestImpl(TransactionRequestId(paymentId), callContext) + (challenges, callContext) <- NewStyle.function.getChallengesByTransactionRequestId(paymentId, callContext) + } yield { + (JSONFactory_BERLIN_GROUP_1_3.createStartPaymentAuthorisationsJson(challenges), callContext) + } + } + } + + resourceDocs += ResourceDoc( + getPaymentInitiationCancellationAuthorisationInformation, + apiVersion, + nameOf(getPaymentInitiationCancellationAuthorisationInformation), + "GET", + "/PAYMENT_SERVICE/PAYMENT_PRODUCT/PAYMENTID/cancellation-authorisations", + "Get Cancellation Authorisation Sub-Resources Request", + s"""${mockedDataText(false)} Retrieve a list of all created cancellation authorisation sub-resources. """, - EmptyBody, - json.parse("""{ + EmptyBody, + json.parse("""{ "cancellationIds" : ["faa3657e-13f0-4feb-a6c3-34bf21a9ae8e]" }"""), - List(UserNotLoggedIn, UnknownError), - ApiTag("Payment Initiation Service (PIS)") :: apiTagBerlinGroupM :: Nil - ) - - lazy val getPaymentInitiationCancellationAuthorisationInformation : OBPEndpoint = { - case paymentService :: paymentProduct :: paymentId :: "cancellation-authorisations" :: Nil JsonGet _ => { - cc => - for { - (_, callContext) <- authenticatedAccess(cc) - _ <- passesPsd2Pisp(callContext) - _ <- NewStyle.function.tryons(checkPaymentServerTypeError(paymentService),404, callContext) { - PaymentServiceTypes.withName(paymentService.replaceAll("-","_")) - } - _ <- NewStyle.function.tryons(checkPaymentProductError(paymentProduct),404, callContext) { - TransactionRequestTypes.withName(paymentProduct.replaceAll("-","_").toUpperCase) - } - (challenges, callContext) <- NewStyle.function.getChallengesByTransactionRequestId(paymentId, callContext) - } yield { - (JSONFactory_BERLIN_GROUP_1_3.CancellationJsonV13(challenges.map(_.challengeId)), callContext) - } - } - } - - resourceDocs += ResourceDoc( - getPaymentInitiationScaStatus, - apiVersion, - nameOf(getPaymentInitiationScaStatus), - "GET", - "/PAYMENT_SERVICE/PAYMENT_PRODUCT/PAYMENT_ID/authorisations/AUTHORISATION_ID", - "Read the SCA Status of the payment authorisation", - s"""${mockedDataText(false)} + List(UserNotLoggedIn, UnknownError), + ApiTag("Payment Initiation Service (PIS)") :: apiTagBerlinGroupM :: Nil + ) + + lazy val getPaymentInitiationCancellationAuthorisationInformation : OBPEndpoint = { + case paymentService :: paymentProduct :: paymentId :: "cancellation-authorisations" :: Nil JsonGet _ => { + cc => + for { + (_, callContext) <- authenticatedAccess(cc) + _ <- passesPsd2Pisp(callContext) + _ <- NewStyle.function.tryons(checkPaymentServerTypeError(paymentService),404, callContext) { + PaymentServiceTypes.withName(paymentService.replaceAll("-","_")) + } + _ <- NewStyle.function.tryons(checkPaymentProductError(paymentProduct),404, callContext) { + TransactionRequestTypes.withName(paymentProduct.replaceAll("-","_").toUpperCase) + } + (challenges, callContext) <- NewStyle.function.getChallengesByTransactionRequestId(paymentId, callContext) + } yield { + (JSONFactory_BERLIN_GROUP_1_3.CancellationJsonV13(challenges.map(_.challengeId)), callContext) + } + } + } + + resourceDocs += ResourceDoc( + getPaymentInitiationScaStatus, + apiVersion, + nameOf(getPaymentInitiationScaStatus), + "GET", + "/PAYMENT_SERVICE/PAYMENT_PRODUCT/PAYMENT_ID/authorisations/AUTHORISATION_ID", + "Read the SCA Status of the payment authorisation", + s"""${mockedDataText(false)} This method returns the SCA status of a payment initiation's authorisation sub-resource. """, - EmptyBody, - json.parse("""{ + EmptyBody, + json.parse("""{ "scaStatus" : "psuAuthenticated" }"""), - List(UserNotLoggedIn, UnknownError), - ApiTag("Payment Initiation Service (PIS)") :: apiTagBerlinGroupM :: Nil - ) - - lazy val getPaymentInitiationScaStatus : OBPEndpoint = { - case paymentService :: paymentProduct :: paymentId:: "authorisations" :: authorisationid :: Nil JsonGet _ => { - cc => - for { - (Full(u), callContext) <- authenticatedAccess(cc) - _ <- passesPsd2Pisp(callContext) - _ <- NewStyle.function.tryons(checkPaymentServerTypeError(paymentService),404, callContext) { - PaymentServiceTypes.withName(paymentService.replaceAll("-","_")) - } - _ <- NewStyle.function.tryons(checkPaymentProductError(paymentProduct),404, callContext) { - TransactionRequestTypes.withName(paymentProduct.replaceAll("-","_").toUpperCase) - } - (_, callContext) <- NewStyle.function.getTransactionRequestImpl(TransactionRequestId(paymentId), callContext) - (challenge, callContext) <- NewStyle.function.getChallenge(authorisationid, callContext) - - } yield { - (json.parse( - s"""{"scaStatus" : "${challenge.scaStatus.getOrElse("None")}"}"""), callContext) - } - } - } - - resourceDocs += ResourceDoc( - getPaymentInitiationStatus, - apiVersion, - nameOf(getPaymentInitiationStatus), - "GET", - "/PAYMENT_SERVICE/PAYMENT_PRODUCT/PAYMENT_ID/status", - "Payment initiation status request", - s"""${mockedDataText(false)} + List(UserNotLoggedIn, UnknownError), + ApiTag("Payment Initiation Service (PIS)") :: apiTagBerlinGroupM :: Nil + ) + + lazy val getPaymentInitiationScaStatus : OBPEndpoint = { + case paymentService :: paymentProduct :: paymentId:: "authorisations" :: authorisationid :: Nil JsonGet _ => { + cc => + for { + (Full(u), callContext) <- authenticatedAccess(cc) + _ <- passesPsd2Pisp(callContext) + _ <- NewStyle.function.tryons(checkPaymentServerTypeError(paymentService),404, callContext) { + PaymentServiceTypes.withName(paymentService.replaceAll("-","_")) + } + _ <- NewStyle.function.tryons(checkPaymentProductError(paymentProduct),404, callContext) { + TransactionRequestTypes.withName(paymentProduct.replaceAll("-","_").toUpperCase) + } + (_, callContext) <- NewStyle.function.getTransactionRequestImpl(TransactionRequestId(paymentId), callContext) + (challenge, callContext) <- NewStyle.function.getChallenge(authorisationid, callContext) + + } yield { + (json.parse( + s"""{"scaStatus" : "${challenge.scaStatus.getOrElse("None")}"}"""), callContext) + } + } + } + + resourceDocs += ResourceDoc( + getPaymentInitiationStatus, + apiVersion, + nameOf(getPaymentInitiationStatus), + "GET", + "/PAYMENT_SERVICE/PAYMENT_PRODUCT/PAYMENT_ID/status", + "Payment initiation status request", + s"""${mockedDataText(false)} Check the transaction status of a payment initiation.""", - EmptyBody, - json.parse(s"""{ + EmptyBody, + json.parse(s"""{ "transactionStatus": "${TransactionStatus.ACCP.code}" }"""), - List(UserNotLoggedIn, UnknownError), - ApiTag("Payment Initiation Service (PIS)") :: apiTagBerlinGroupM :: Nil - ) - - lazy val getPaymentInitiationStatus : OBPEndpoint = { - case paymentService :: paymentProduct :: paymentId:: "status" :: Nil JsonGet _ => { - cc => - for { - (Full(u), callContext) <- authenticatedAccess(cc) - _ <- passesPsd2Pisp(callContext) - _ <- NewStyle.function.tryons(checkPaymentServerTypeError(paymentService),404, callContext) { - PaymentServiceTypes.withName(paymentService.replaceAll("-","_")) - } - _ <- NewStyle.function.tryons(checkPaymentProductError(paymentProduct),404, callContext) { - TransactionRequestTypes.withName(paymentProduct.replaceAll("-","_").toUpperCase) - } - (transactionRequest, callContext) <- NewStyle.function.getTransactionRequestImpl(TransactionRequestId(paymentId), callContext) - - transactionRequestStatus = mapTransactionStatus(transactionRequest.status) - - transactionRequestAmount <- NewStyle.function.tryons(s"${InvalidNumber} transaction request amount cannot convert to a Decimal",400, callContext) { - BigDecimal(transactionRequest.body.to_sepa_credit_transfers.get.instructedAmount.amount) - } - transactionRequestCurrency <- NewStyle.function.tryons(s"${InvalidCurrency} can not get currency from this paymentId(${paymentId})",400, callContext) { - transactionRequest.body.to_sepa_credit_transfers.get.instructedAmount.currency - } - - - transactionRequestFromAccount = transactionRequest.from - (fromAccount, callContext) <- NewStyle.function.checkBankAccountExists(BankId(transactionRequestFromAccount.bank_id), AccountId(transactionRequestFromAccount.account_id), callContext) - fromAccountBalance = fromAccount.balance - fromAccountCurrency = fromAccount.currency - fundsAvalible = fromAccountBalance >= transactionRequestAmount - - - //From change from requestAccount Currency to currentBankAccount Currency - rate = fx.exchangeRate(transactionRequestCurrency, fromAccountCurrency, None, callContext) - _ <- Helper.booleanToFuture(s"$InvalidCurrency The requested currency conversion (${transactionRequestCurrency} to ${fromAccountCurrency}) is not supported.", cc=callContext) { - rate.isDefined - } - - requestChangedCurrencyAmount = fx.convert(transactionRequestAmount, rate) - - fundsAvailable = (fromAccountBalance >= requestChangedCurrencyAmount) - - transactionRequestStatusChekedFunds = if(fundsAvailable) transactionRequestStatus else TransactionStatus.RCVD.code - - } yield { - (json.parse(s"""{ + List(UserNotLoggedIn, UnknownError), + ApiTag("Payment Initiation Service (PIS)") :: apiTagBerlinGroupM :: Nil + ) + + lazy val getPaymentInitiationStatus : OBPEndpoint = { + case paymentService :: paymentProduct :: paymentId:: "status" :: Nil JsonGet _ => { + cc => + for { + (Full(u), callContext) <- authenticatedAccess(cc) + _ <- passesPsd2Pisp(callContext) + _ <- NewStyle.function.tryons(checkPaymentServerTypeError(paymentService),404, callContext) { + PaymentServiceTypes.withName(paymentService.replaceAll("-","_")) + } + _ <- NewStyle.function.tryons(checkPaymentProductError(paymentProduct),404, callContext) { + TransactionRequestTypes.withName(paymentProduct.replaceAll("-","_").toUpperCase) + } + (transactionRequest, callContext) <- NewStyle.function.getTransactionRequestImpl(TransactionRequestId(paymentId), callContext) + + transactionRequestStatus = mapTransactionStatus(transactionRequest.status) + + transactionRequestAmount <- NewStyle.function.tryons(s"${InvalidNumber} transaction request amount cannot convert to a Decimal",400, callContext) { + BigDecimal(transactionRequest.body.to_sepa_credit_transfers.get.instructedAmount.amount) + } + transactionRequestCurrency <- NewStyle.function.tryons(s"${InvalidCurrency} can not get currency from this paymentId(${paymentId})",400, callContext) { + transactionRequest.body.to_sepa_credit_transfers.get.instructedAmount.currency + } + + + transactionRequestFromAccount = transactionRequest.from + (fromAccount, callContext) <- NewStyle.function.checkBankAccountExists(BankId(transactionRequestFromAccount.bank_id), AccountId(transactionRequestFromAccount.account_id), callContext) + fromAccountBalance = fromAccount.balance + fromAccountCurrency = fromAccount.currency + fundsAvalible = fromAccountBalance >= transactionRequestAmount + + + //From change from requestAccount Currency to currentBankAccount Currency + rate = fx.exchangeRate(transactionRequestCurrency, fromAccountCurrency, None, callContext) + _ <- Helper.booleanToFuture(s"$InvalidCurrency The requested currency conversion (${transactionRequestCurrency} to ${fromAccountCurrency}) is not supported.", cc=callContext) { + rate.isDefined + } + + requestChangedCurrencyAmount = fx.convert(transactionRequestAmount, rate) + + fundsAvailable = (fromAccountBalance >= requestChangedCurrencyAmount) + + transactionRequestStatusChekedFunds = if(fundsAvailable) transactionRequestStatus else TransactionStatus.RCVD.code + + } yield { + (json.parse(s"""{ "transactionStatus": "$transactionRequestStatusChekedFunds" "fundsAvailable": $fundsAvailable }""" - ), callContext) - } - } - } + ), callContext) + } + } + } val additionalInstructions : String = @@ -465,7 +568,7 @@ Check the transaction status of a payment initiation.""", def generalPaymentSummary (isMockedData :Boolean) = - s"""${mockedDataText(isMockedData)} + s"""${mockedDataText(isMockedData)} This method is used to initiate a payment at the ASPSP. ## Variants of Payment Initiation Requests @@ -514,119 +617,186 @@ Check the transaction status of a payment initiation.""", $additionalInstructions """ - def initiatePaymentImplementation(paymentService: String, paymentProduct: String, json: liftweb.json.JValue, cc: CallContext) = { - for { - (u, callContext) <- applicationAccess(cc) - _ <- passesPsd2Pisp(callContext) - - paymentServiceType <- NewStyle.function.tryons(checkPaymentServerTypeError(paymentService), 404, callContext) { - PaymentServiceTypes.withName(paymentService.replaceAll("-", "_")) - } - - //Berlin Group PaymentProduct is OBP transaction request type - transactionRequestType <- NewStyle.function.tryons(checkPaymentProductError(paymentProduct), 404, callContext) { - TransactionRequestTypes.withName(paymentProduct.replaceAll("-", "_").toUpperCase) - } - - sepaCreditTransfersBerlinGroupV13 <- if(paymentServiceType.equals(PaymentServiceTypes.payments)){ - NewStyle.function.tryons(s"$InvalidJsonFormat The Json body should be the $SepaCreditTransfersBerlinGroupV13 ", 400, callContext) { - json.extract[SepaCreditTransfersBerlinGroupV13] - } - } else if(paymentServiceType.equals(PaymentServiceTypes.periodic_payments)){ - NewStyle.function.tryons(s"$InvalidJsonFormat The Json body should be the $PeriodicSepaCreditTransfersBerlinGroupV13 ", 400, callContext) { - json.extract[PeriodicSepaCreditTransfersBerlinGroupV13] - } - }else{ - Future{throw new RuntimeException(checkPaymentServerTypeError(paymentServiceType.toString))} - } - isValidAmountNumber <- NewStyle.function.tryons(s"$InvalidNumber Current input is ${sepaCreditTransfersBerlinGroupV13.instructedAmount.amount} ", 400, callContext) { - BigDecimal(sepaCreditTransfersBerlinGroupV13.instructedAmount.amount) - } - - _ <- Helper.booleanToFuture(s"${NotPositiveAmount} Current input is: '${isValidAmountNumber}'", cc = callContext) { - isValidAmountNumber > BigDecimal("0") - } - - // Prevent default value for transaction request type (at least). - _ <- Helper.booleanToFuture(s"${InvalidISOCurrencyCode} Current input is: '${sepaCreditTransfersBerlinGroupV13.instructedAmount.currency}'", cc = callContext) { - isValidCurrencyISOCode(sepaCreditTransfersBerlinGroupV13.instructedAmount.currency) - } - - _ <- NewStyle.function.isEnabledTransactionRequests(callContext) - - - (createdTransactionRequest, callContext) <- transactionRequestType match { - case TransactionRequestTypes.SEPA_CREDIT_TRANSFERS => { - for { - (createdTransactionRequest, callContext) <- NewStyle.function.createTransactionRequestBGV1( - initiator = u, - paymentServiceType, - transactionRequestType, - transactionRequestBody = sepaCreditTransfersBerlinGroupV13, - callContext - ) - } yield (createdTransactionRequest, callContext) - } - } - } yield { - (JSONFactory_BERLIN_GROUP_1_3.createTransactionRequestJson(createdTransactionRequest), HttpCode.`201`(callContext)) - } - } - - resourceDocs += ResourceDoc( - initiatePayments, - apiVersion, - nameOf(initiatePayments), - "POST", - "/payments/PAYMENT_PRODUCT", - "Payment initiation request(payments)", - generalPaymentSummary(false), - json.parse(s"""{ + resourceDocs += ResourceDoc( + initiatePayments, + apiVersion, + nameOf(initiatePayments), + "POST", + "/payments/PAYMENT_PRODUCT", + "Payment initiation request(payments)", + generalPaymentSummary(false), + json.parse(s"""{ "debtorAccount": { - "iban": "DE123456987480123" + "iban": "MD12AG00000002233445566", + "msisdn" : "373690178437" }, "instructedAmount": { "currency": "EUR", "amount": "100" }, "creditorAccount": { - "iban": "UK12 1234 5123 4517 2948 6166 077" + "iban": "MD12AG00000002233445567" }, - "creditorName": "70charname" + "creditorName": "Comerciant X", + "creditorId": "2002002002002", + "instructionPriority": "NORM", + "creditorCtryOfRes": "MD", + "remittanceInformationUnstructured": "Plata facturii", + "endToEndIdentification": "d14c3e75-8a2f-4e93-b3ca-ec4fd7128133" }"""), - json.parse(s"""{ + json.parse(s"""{ "transactionStatus": "${TransactionStatus.RCVD.code}", "paymentId": "1234-wertiq-983", "_links": { "scaRedirect": {"href": "$getServerUrl/otp?flow=payment&paymentService=payments&paymentProduct=sepa_credit_transfers&paymentId=b0472c21-6cea-4ee0-b036-3e253adb3b0b"}, - "self": {"href": "/v1.3/payments/sepa-credit-transfers/1234-wertiq-983"}, + "self": {"href": "/v1.3/payments/PAYMENT_PRODUCT/1234-wertiq-983"}, "status": {"href": "/v1.3/payments/1234-wertiq-983/status"}, "scaStatus": {"href": "/v1.3/payments/1234-wertiq-983/authorisations/123auth456"} } }"""), - List(UserNotLoggedIn, UnknownError), - ApiTag("Payment Initiation Service (PIS)") :: apiTagBerlinGroupM :: Nil - ) - - lazy val initiatePayments : OBPEndpoint = { - case "payments" :: paymentProduct :: Nil JsonPost json -> _ => { - cc => - initiatePaymentImplementation("payments", paymentProduct, json, cc) - } + List(UserNotLoggedIn, UnknownError), + ApiTag("Payment Initiation Service (PIS)") :: apiTagBerlinGroupM :: Nil + ) + + lazy val initiatePayments : OBPEndpoint = { + case "payments" :: paymentProduct :: Nil JsonPost json -> _ => { + cc => + initiatePaymentImplementation("payments", paymentProduct, json, cc) } + } + private def assertF(msg: String, code: Int = 400, cc: Option[CallContext])(cond: => Boolean): Future[Unit] = + Helper.booleanToFuture(msg, code, cc)(cond).map(_ => ()) + + def initiatePaymentImplementation(paymentService: String, paymentProduct: String, json: liftweb.json.JValue, cc: CallContext) = { + for { + (u, callContext) <- applicationAccess(cc); _ <- passesPsd2Pisp(callContext) + + //val psuGeoLocation = headers.find(_.name == RequestHeader.`PSU-Geo-Location`) + paymentServiceType <- NewStyle.function.tryons(checkPaymentServerTypeError(paymentService), 404, callContext){ PaymentServiceTypes.withName(paymentService.replaceAll("-", "_")) } + transactionRequestType <- NewStyle.function.tryons(checkPaymentProductError(paymentProduct), 404, callContext){ TransactionRequestTypes.withName(paymentProduct.replaceAll("-", "_").toUpperCase) } + transferRequest <- { + if (paymentServiceType == PaymentServiceTypes.payments) transactionRequestType match { + case TransactionRequestTypes.SEPA_CREDIT_TRANSFERS => + NewStyle.function.tryons(s"$InvalidJsonFormat The Json body should be the ${classOf[SepaCreditTransfersBerlinGroupV13].getSimpleName}", 400, callContext){ json.extract[SepaCreditTransfersBerlinGroupV13] } + case TransactionRequestTypes.INSTANT_CREDIT_TRANSFERS_MD => + NewStyle.function.tryons(s"$InvalidJsonFormat The Json body should be the ${classOf[InstantCreditTransfersMdV1].getSimpleName}", 400, callContext){ json.extract[InstantCreditTransfersMdV1] } + case TransactionRequestTypes.DOMESTIC_CREDIT_TRANSFERS_MD => + NewStyle.function.tryons(s"$InvalidJsonFormat The Json body should be the ${classOf[DomesticCreditTransfersMdV1].getSimpleName}", 400, callContext){ json.extract[DomesticCreditTransfersMdV1] } + } + else if (paymentServiceType == PaymentServiceTypes.periodic_payments) + NewStyle.function.tryons(s"$InvalidJsonFormat The Json body should be the ${classOf[PeriodicSepaCreditTransfersBerlinGroupV13].getSimpleName}", 400, callContext){ json.extract[PeriodicSepaCreditTransfersBerlinGroupV13] } + else Future(throw new RuntimeException(checkPaymentServerTypeError(paymentServiceType.toString))) + } + _ <- transferRequest match { + case m: InstantCreditTransfersMdV1 => + for { + _ <- validateInstantOnly(m, callContext) + } yield () + case m: DomesticCreditTransfersMdV1 => + for { + _ <- validateDomesticOnly(m, callContext) + } yield () + case _ => + Future.unit + } + + isValidAmountNumber <- transferRequest match { + case s: SepaCreditTransfersBerlinGroupV13 => NewStyle.function.tryons(s"$InvalidNumber Current input is ${s.instructedAmount.amount} ", 400, callContext){ BigDecimal(s.instructedAmount.amount) } + case m: InstantCreditTransfersMdV1 => NewStyle.function.tryons(s"$InvalidNumber Current input is ${m.instructedAmount.amount} ", 400, callContext){ BigDecimal(m.instructedAmount.amount) } + case m: DomesticCreditTransfersMdV1 => NewStyle.function.tryons(s"$InvalidNumber Current input is ${m.instructedAmount.amount} ", 400, callContext){ BigDecimal(m.instructedAmount.amount) } + case _ => NewStyle.function.tryons(s"$InvalidNumber", 400, callContext){ BigDecimal(0) } + } + _ <- Helper.booleanToFuture(s"$NotPositiveAmount Current input is: '$isValidAmountNumber'", cc = callContext)(isValidAmountNumber > BigDecimal("0")).map(_ => ()) + _ <- transferRequest match { + case s: SepaCreditTransfersBerlinGroupV13 => Helper.booleanToFuture(s"$InvalidISOCurrencyCode Current input is: '${s.instructedAmount.currency}'", cc = callContext)(isValidCurrencyISOCode(s.instructedAmount.currency)).map(_ => ()) + case m: InstantCreditTransfersMdV1 => Helper.booleanToFuture(s"$InvalidISOCurrencyCode Current input is: '${m.instructedAmount.currency}'", cc = callContext)(isValidCurrencyISOCode(m.instructedAmount.currency)).map(_ => ()) + case m: DomesticCreditTransfersMdV1 => Helper.booleanToFuture(s"$InvalidISOCurrencyCode Current input is: '${m.instructedAmount.currency}'", cc = callContext)(isValidCurrencyISOCode(m.instructedAmount.currency)).map(_ => ()) + case _ => Future.unit + } + _ <- NewStyle.function.isEnabledTransactionRequests(callContext) + (createdTransactionRequest, callContext) <- transactionRequestType match { + case TransactionRequestTypes.SEPA_CREDIT_TRANSFERS => NewStyle.function.createTransactionRequestBGV1(u, paymentServiceType, transactionRequestType, transferRequest.asInstanceOf[SepaCreditTransfersBerlinGroupV13], callContext) + case TransactionRequestTypes.INSTANT_CREDIT_TRANSFERS_MD => NewStyle.function.createInstantTransactionRequestMdBGV1(u, paymentServiceType, transactionRequestType, transferRequest.asInstanceOf[InstantCreditTransfersMdV1], callContext) + case TransactionRequestTypes.DOMESTIC_CREDIT_TRANSFERS_MD => NewStyle.function.createDomesticTransactionRequestMdBGV1(u, paymentServiceType, transactionRequestType, transferRequest.asInstanceOf[DomesticCreditTransfersMdV1], callContext) + } + } yield (JSONFactory_BERLIN_GROUP_1_3.createTransactionRequestJson(createdTransactionRequest, transactionRequestType), HttpCode.`201`(callContext)) + } - resourceDocs += ResourceDoc( - initiatePeriodicPayments, - apiVersion, - nameOf(initiatePeriodicPayments), - "POST", - "/periodic-payments/PAYMENT_PRODUCT", - "Payment initiation request(periodic-payments)", - generalPaymentSummary(false), - json.parse(s"""{ + + private def validateInstantOnly(m: InstantCreditTransfersMdV1, cc: Option[CallContext]): Future[Unit] = { + for { + _ <- assertF(s"$MandatoryError Msisdn is required", 400, cc)( + m.creditorAccount != null && + Option(m.creditorAccount.msisdn).exists(_.nonEmpty)) + _ <- assertF(s"$ValidationError Msisdn. Telephone number format is not correct", 400, cc)( + Option(m.creditorAccount.msisdn).exists(_.matches("^373\\d{8}$"))) + _ <- assertF(s"$ValidationError RemittanceInformationUnstructured. Must be ≤ 200 characters", 400, cc)( + m.remittanceInformationUnstructured.forall(_.length <= 200)) + _ <- assertF(s"$ValidationError Iban. Invalid format", 400, cc)( + m.debtorAccount.forall { a => + val iban = Option(a.iban).getOrElse("") + iban.isEmpty || iban.matches("^[A-Za-z0-9]{1,24}$") + }) + _ <- assertF(s"$MandatoryError PurposeCode is required", 400, cc)( + m.purposeCode.isDefined) + _ <- assertF(s"$ValidationError PurposeCode must be exactly 201", 400, cc)( + m.purposeCode.contains("201")) + _ <- assertF(s"$MandatoryError EndToEndIdentification is required", 400, cc)( + m.endToEndIdentification.isDefined) + existingPayment = MappedPaymentProvider.getPaymentByEndToEndIdentification(m.endToEndIdentification.getOrElse("")) + _ <- assertF(s"$ValidationError EndToEndIdentification must be unique", 400, cc)( + existingPayment.isEmpty) + } yield () + } + + private def validateDomesticOnly(m: DomesticCreditTransfersMdV1, cc: Option[CallContext]): Future[Unit] = { + for { + _ <- assertF(s"$MandatoryError Iban is required", 400, cc)( + m.creditorAccount != null && + Option(m.creditorAccount.iban).exists(_.nonEmpty)) + _ <- assertF(s"$ValidationError Creditor.Iban. Invalid format", 400, cc)( + Option(m.creditorAccount.iban).exists(_.matches("^[A-Za-z0-9]{1,24}$"))) + _ <- assertF(s"$ValidationError RemittanceInformationUnstructured. Must be ≤ 200 characters", 400, cc)( + m.remittanceInformationUnstructured.forall(_.length <= 200)) + _ <- assertF(s"$ValidationError Iban. Invalid format", 400, cc)( + m.debtorAccount.forall { a => + val iban = Option(a.iban).getOrElse("") + iban.isEmpty || iban.matches("^[A-Za-z0-9]{1,24}$") + }) + _ <- assertF(s"$MandatoryError CreditorName is required", 400, cc)( + m.creditorName.isDefined) + _ <- assertF(s"$MandatoryError CreditorId is required", 400, cc)( + m.creditorId.isDefined) + _ <- assertF(s"$MandatoryError CreditorCtryOfRes is required", 400, cc)( + m.creditorCtryOfRes.isDefined) + _ <- assertF(s"$MandatoryError Creditor country code is required", 400, cc)( + m.creditorCtryOfRes != null && m.creditorCtryOfRes.nonEmpty) + _ <- assertF(s"$ValidationError Invalid creditor country code format", 400, cc)( + m.creditorCtryOfRes.matches("^[A-Z]{2}$")) // ISO 3166-1 country code (2 uppercase letters) + _ <- assertF(s"$ValidationError InstructionPriority must be either 'NORM' or 'URGT'", 400, cc)( + Option(m.instructionPriority).map(_.trim.toUpperCase(java.util.Locale.ROOT)).exists(p => p == "NORM" || p == "URGT")) + _ <- assertF(s"$MandatoryError EndToEndIdentification is required", 400, cc)( + m.endToEndIdentification.isDefined + ) + existingPayment = MappedPaymentProvider.getPaymentByEndToEndIdentification(m.endToEndIdentification.getOrElse("")) + _ <- assertF(s"$ValidationError EndToEndIdentification must be unique", 400, cc)( + existingPayment.isEmpty + ) + } yield () + } + + resourceDocs += ResourceDoc( + initiatePeriodicPayments, + apiVersion, + nameOf(initiatePeriodicPayments), + "POST", + "/periodic-payments/PAYMENT_PRODUCT", + "Payment initiation request(periodic-payments)", + generalPaymentSummary(false), + json.parse(s"""{ "instructedAmount": { "currency": "EUR", "amount": "123" @@ -644,7 +814,7 @@ Check the transaction status of a payment initiation.""", "frequency": "Monthly", "dayOfExecution": "01" }"""), - json.parse(s"""{ + json.parse(s"""{ "transactionStatus": "${TransactionStatus.RCVD.code}", "paymentId": "1234-wertiq-983", "_links": @@ -655,26 +825,26 @@ Check the transaction status of a payment initiation.""", "scaStatus": {"href": "/v1.3/periodic-payments/1234-wertiq-983/authorisations/123auth456"} } }"""), - List(UserNotLoggedIn, UnknownError), - ApiTag("Payment Initiation Service (PIS)") :: apiTagBerlinGroupM :: Nil - ) - - lazy val initiatePeriodicPayments : OBPEndpoint = { - case "periodic-payments" :: paymentProduct :: Nil JsonPost json -> _ => { - cc => - initiatePaymentImplementation("periodic-payments", paymentProduct, json, cc) - } + List(UserNotLoggedIn, UnknownError), + ApiTag("Payment Initiation Service (PIS)") :: apiTagBerlinGroupM :: Nil + ) + + lazy val initiatePeriodicPayments : OBPEndpoint = { + case "periodic-payments" :: paymentProduct :: Nil JsonPost json -> _ => { + cc => + initiatePaymentImplementation("periodic-payments", paymentProduct, json, cc) } + } - resourceDocs += ResourceDoc( - initiateBulkPayments, - apiVersion, - nameOf(initiateBulkPayments), - "POST", - "/bulk-payments/PAYMENT_PRODUCT", - "Payment initiation request(bulk-payments)", - generalPaymentSummary(true), - json.parse(s"""{ + resourceDocs += ResourceDoc( + initiateBulkPayments, + apiVersion, + nameOf(initiateBulkPayments), + "POST", + "/bulk-payments/PAYMENT_PRODUCT", + "Payment initiation request(bulk-payments)", + generalPaymentSummary(true), + json.parse(s"""{ "batchBookingPreferred": "true", "debtorAccount": { "iban": "DE40100100103307118608" @@ -706,7 +876,7 @@ Check the transaction status of a payment initiation.""", } ] }"""), - json.parse(s"""{ + json.parse(s"""{ "transactionStatus": "${TransactionStatus.RCVD.code}", "paymentId": "1234-wertiq-983", "_links": @@ -717,68 +887,68 @@ Check the transaction status of a payment initiation.""", "scaStatus": {"href": "/v1.3/bulk-payments/1234-wertiq-983/authorisations/123auth456"} } }"""), - List(UserNotLoggedIn, UnknownError), - ApiTag("Payment Initiation Service (PIS)") :: apiTagBerlinGroupM :: Nil - ) - - lazy val initiateBulkPayments : OBPEndpoint = { - case "bulk-payments" :: paymentProduct :: Nil JsonPost json -> _ => { - cc => - initiatePaymentImplementation("bulk-payments", paymentProduct, json, cc) - } + List(UserNotLoggedIn, UnknownError), + ApiTag("Payment Initiation Service (PIS)") :: apiTagBerlinGroupM :: Nil + ) + + lazy val initiateBulkPayments : OBPEndpoint = { + case "bulk-payments" :: paymentProduct :: Nil JsonPost json -> _ => { + cc => + initiatePaymentImplementation("bulk-payments", paymentProduct, json, cc) } + } - def generalStartPaymentAuthorisationSummary(isMockedDate: Boolean) = s"""${mockedDataText(isMockedDate)} -Create an authorisation sub-resource and start the authorisation process. -The message might in addition transmit authentication and authorisation related data. + def generalStartPaymentAuthorisationSummary(isMockedDate: Boolean) = s"""${mockedDataText(isMockedDate)} +Create an authorisation sub-resource and start the authorisation process. +The message might in addition transmit authentication and authorisation related data. -This method is iterated n times for a n times SCA authorisation in a -corporate context, each creating an own authorisation sub-endpoint for +This method is iterated n times for a n times SCA authorisation in a +corporate context, each creating an own authorisation sub-endpoint for the corresponding PSU authorising the transaction. -The ASPSP might make the usage of this access method unnecessary in case -of only one SCA process needed, since the related authorisation resource -might be automatically created by the ASPSP after the submission of the +The ASPSP might make the usage of this access method unnecessary in case +of only one SCA process needed, since the related authorisation resource +might be automatically created by the ASPSP after the submission of the payment data with the first POST payments/{payment-product} call. -The start authorisation process is a process which is needed for creating a new authorisation -or cancellation sub-resource. +The start authorisation process is a process which is needed for creating a new authorisation +or cancellation sub-resource. This applies in the following scenarios: - * The ASPSP has indicated with an 'startAuthorisation' hyperlink in the preceeding Payment - Initiation Response that an explicit start of the authorisation process is needed by the TPP. - The 'startAuthorisation' hyperlink can transport more information about data which needs to be + * The ASPSP has indicated with an 'startAuthorisation' hyperlink in the preceeding Payment + Initiation Response that an explicit start of the authorisation process is needed by the TPP. + The 'startAuthorisation' hyperlink can transport more information about data which needs to be uploaded by using the extended forms. - * 'startAuthorisationWithPsuIdentfication', + * 'startAuthorisationWithPsuIdentfication', * 'startAuthorisationWithPsuAuthentication' #TODO - * 'startAuthorisationWithAuthentciationMethodSelection' + * 'startAuthorisationWithAuthentciationMethodSelection' * The related payment initiation cannot yet be executed since a multilevel SCA is mandated. - * The ASPSP has indicated with an 'startAuthorisation' hyperlink in the preceeding - Payment Cancellation Response that an explicit start of the authorisation process is needed by the TPP. - The 'startAuthorisation' hyperlink can transport more information about data which needs to be uploaded + * The ASPSP has indicated with an 'startAuthorisation' hyperlink in the preceeding + Payment Cancellation Response that an explicit start of the authorisation process is needed by the TPP. + The 'startAuthorisation' hyperlink can transport more information about data which needs to be uploaded by using the extended forms as indicated above. - * The related payment cancellation request cannot be applied yet since a multilevel SCA is mandate for + * The related payment cancellation request cannot be applied yet since a multilevel SCA is mandate for executing the cancellation. * The signing basket needs to be authorised yet. """ - resourceDocs += ResourceDoc( - startPaymentAuthorisationUpdatePsuAuthentication, - apiVersion, - nameOf(startPaymentAuthorisationUpdatePsuAuthentication), - "POST", - "/PAYMENT_SERVICE/PAYMENT_PRODUCT/PAYMENT_ID/authorisations", - "Start the authorisation process for a payment initiation (updatePsuAuthentication)", - generalStartPaymentAuthorisationSummary(true), - json.parse( - """{ - | "scaStatus": "finalised", - | "_links":{ - | "status": {"href":"/v1/payments/sepa-credit-transfers/qwer3456tzui7890/status"} - | } - | }""".stripMargin), - json.parse("""{ + resourceDocs += ResourceDoc( + startPaymentAuthorisationUpdatePsuAuthentication, + apiVersion, + nameOf(startPaymentAuthorisationUpdatePsuAuthentication), + "POST", + "/PAYMENT_SERVICE/PAYMENT_PRODUCT/PAYMENT_ID/authorisations", + "Start the authorisation process for a payment initiation (updatePsuAuthentication)", + generalStartPaymentAuthorisationSummary(true), + json.parse( + """{ + | "scaStatus": "finalised", + | "_links":{ + | "status": {"href":"/v1/payments/sepa-credit-transfers/qwer3456tzui7890/status"} + | } + | }""".stripMargin), + json.parse("""{ "challengeData": { "scaStatus": "received", "authorisationId": "88695566-6642-46d5-9985-0d824624f507", @@ -788,13 +958,13 @@ This applies in the following scenarios: } } }"""), - List(UserNotLoggedIn, UnknownError), - ApiTag("Payment Initiation Service (PIS)") :: apiTagBerlinGroupM :: Nil - ) + List(UserNotLoggedIn, UnknownError), + ApiTag("Payment Initiation Service (PIS)") :: apiTagBerlinGroupM :: Nil + ) - lazy val startPaymentAuthorisationUpdatePsuAuthentication : OBPEndpoint = { - case paymentService :: paymentProduct :: paymentId :: "authorisations" :: Nil JsonPost json -> _ if checkUpdatePsuAuthentication(json) => { + lazy val startPaymentAuthorisationUpdatePsuAuthentication : OBPEndpoint = { + case paymentService :: paymentProduct :: paymentId :: "authorisations" :: Nil JsonPost json -> _ if checkUpdatePsuAuthentication(json) => { cc => for { (_, callContext) <- authenticatedAccess(cc) @@ -810,19 +980,19 @@ This applies in the following scenarios: } }"""), HttpCode.`201`(callContext)) } - } } + } - resourceDocs += ResourceDoc( - startPaymentAuthorisationSelectPsuAuthenticationMethod, - apiVersion, - nameOf(startPaymentAuthorisationSelectPsuAuthenticationMethod), - "POST", - "/PAYMENT_SERVICE/PAYMENT_PRODUCT/PAYMENT_ID/authorisations", - "Start the authorisation process for a payment initiation (selectPsuAuthenticationMethod)", - generalStartPaymentAuthorisationSummary(true), - json.parse("""{"authenticationMethodId":""}"""), - json.parse("""{ + resourceDocs += ResourceDoc( + startPaymentAuthorisationSelectPsuAuthenticationMethod, + apiVersion, + nameOf(startPaymentAuthorisationSelectPsuAuthenticationMethod), + "POST", + "/PAYMENT_SERVICE/PAYMENT_PRODUCT/PAYMENT_ID/authorisations", + "Start the authorisation process for a payment initiation (selectPsuAuthenticationMethod)", + generalStartPaymentAuthorisationSummary(true), + json.parse("""{"authenticationMethodId":""}"""), + json.parse("""{ "challengeData": { "scaStatus": "received", "authorisationId": "88695566-6642-46d5-9985-0d824624f507", @@ -832,12 +1002,12 @@ This applies in the following scenarios: } } }"""), - List(UserNotLoggedIn, UnknownError), - ApiTag("Payment Initiation Service (PIS)") :: apiTagBerlinGroupM :: Nil - ) + List(UserNotLoggedIn, UnknownError), + ApiTag("Payment Initiation Service (PIS)") :: apiTagBerlinGroupM :: Nil + ) - lazy val startPaymentAuthorisationSelectPsuAuthenticationMethod : OBPEndpoint = { - case paymentService :: paymentProduct :: paymentId :: "authorisations" :: Nil JsonPost json -> _ if checkSelectPsuAuthenticationMethod(json) => { + lazy val startPaymentAuthorisationSelectPsuAuthenticationMethod : OBPEndpoint = { + case paymentService :: paymentProduct :: paymentId :: "authorisations" :: Nil JsonPost json -> _ if checkSelectPsuAuthenticationMethod(json) => { cc => for { (Full(u), callContext) <- authenticatedAccess(cc) @@ -855,18 +1025,18 @@ This applies in the following scenarios: }"""), HttpCode.`201`(callContext)) } } - } + } - resourceDocs += ResourceDoc( - startPaymentAuthorisationTransactionAuthorisation, - apiVersion, - nameOf(startPaymentAuthorisationTransactionAuthorisation), - "POST", - "/PAYMENT_SERVICE/PAYMENT_PRODUCT/PAYMENT_ID/authorisations", - "Start the authorisation process for a payment initiation (transactionAuthorisation)", - generalStartPaymentAuthorisationSummary(false), - json.parse("""{"scaAuthenticationData":"123"}"""), - json.parse("""{ + resourceDocs += ResourceDoc( + startPaymentAuthorisationTransactionAuthorisation, + apiVersion, + nameOf(startPaymentAuthorisationTransactionAuthorisation), + "POST", + "/PAYMENT_SERVICE/PAYMENT_PRODUCT/PAYMENT_ID/authorisations", + "Start the authorisation process for a payment initiation (transactionAuthorisation)", + generalStartPaymentAuthorisationSummary(false), + json.parse("""{"scaAuthenticationData":"123"}"""), + json.parse("""{ "challengeData": { "scaStatus": "received", "authorisationId": "88695566-6642-46d5-9985-0d824624f507", @@ -876,61 +1046,61 @@ This applies in the following scenarios: } } }"""), - List(UserNotLoggedIn, UnknownError), - ApiTag("Payment Initiation Service (PIS)") :: apiTagBerlinGroupM :: Nil - ) + List(UserNotLoggedIn, UnknownError), + ApiTag("Payment Initiation Service (PIS)") :: apiTagBerlinGroupM :: Nil + ) - lazy val startPaymentAuthorisationTransactionAuthorisation : OBPEndpoint = { - case paymentService :: paymentProduct :: paymentId :: "authorisations" :: Nil JsonPost json -> _ if checkTransactionAuthorisation(json) => { - cc => - for { - (Full(u), callContext) <- authenticatedAccess(cc) - _ <- passesPsd2Pisp(callContext) - _ <- NewStyle.function.tryons(checkPaymentServerTypeError(paymentService), 404, callContext) { - PaymentServiceTypes.withName(paymentService.replaceAll("-", "_")) - } - _ <- NewStyle.function.tryons(checkPaymentProductError(paymentProduct), 404, callContext) { - TransactionRequestTypes.withName(paymentProduct.replaceAll("-", "_").toUpperCase) - } - (_, callContext) <- NewStyle.function.getTransactionRequestImpl(TransactionRequestId(paymentId), callContext) - - (challenges, callContext) <- NewStyle.function.createChallengesC2( - List(u.userId), - ChallengeType.BERLIN_GROUP_PAYMENT_CHALLENGE, - Some(paymentId), - getScaMethodAtInstance(SEPA_CREDIT_TRANSFERS.toString).toOption, - Some(StrongCustomerAuthenticationStatus.received), - None, - None, - callContext - ) - //NOTE: in OBP it support multiple challenges, but in Berlin Group it has only one challenge. The following guard is to make sure it returns the 1st challenge properly. - challenge <- NewStyle.function.tryons(InvalidConnectorResponseForCreateChallenge, 400, callContext) { - challenges.head - } - } yield { - (JSONFactory_BERLIN_GROUP_1_3.createStartPaymentAuthorisationJson(challenge), HttpCode.`201`(callContext)) + lazy val startPaymentAuthorisationTransactionAuthorisation : OBPEndpoint = { + case paymentService :: paymentProduct :: paymentId :: "authorisations" :: Nil JsonPost json -> _ if checkTransactionAuthorisation(json) => { + cc => + for { + (Full(u), callContext) <- authenticatedAccess(cc) + _ <- passesPsd2Pisp(callContext) + _ <- NewStyle.function.tryons(checkPaymentServerTypeError(paymentService), 404, callContext) { + PaymentServiceTypes.withName(paymentService.replaceAll("-", "_")) } - } + _ <- NewStyle.function.tryons(checkPaymentProductError(paymentProduct), 404, callContext) { + TransactionRequestTypes.withName(paymentProduct.replaceAll("-", "_").toUpperCase) + } + (_, callContext) <- NewStyle.function.getTransactionRequestImpl(TransactionRequestId(paymentId), callContext) + + (challenges, callContext) <- NewStyle.function.createChallengesC2( + List(u.userId), + ChallengeType.BERLIN_GROUP_PAYMENT_CHALLENGE, + Some(paymentId), + getScaMethodAtInstance(SEPA_CREDIT_TRANSFERS.toString).toOption, + Some(StrongCustomerAuthenticationStatus.received), + None, + None, + callContext + ) + //NOTE: in OBP it support multiple challenges, but in Berlin Group it has only one challenge. The following guard is to make sure it returns the 1st challenge properly. + challenge <- NewStyle.function.tryons(InvalidConnectorResponseForCreateChallenge, 400, callContext) { + challenges.head + } + } yield { + (JSONFactory_BERLIN_GROUP_1_3.createStartPaymentAuthorisationJson(challenge), HttpCode.`201`(callContext)) + } } + } - def generalStartPaymentInitiationCancellationAuthorisationSummary (isMockedDate:Boolean) = - s"""${mockedDataText(isMockedDate)} -Creates an authorisation sub-resource and start the authorisation process of the cancellation of the addressed payment. + def generalStartPaymentInitiationCancellationAuthorisationSummary (isMockedDate:Boolean) = + s"""${mockedDataText(isMockedDate)} +Creates an authorisation sub-resource and start the authorisation process of the cancellation of the addressed payment. The message might in addition transmit authentication and authorisation related data. -This method is iterated n times for a n times SCA authorisation in a -corporate context, each creating an own authorisation sub-endpoint for +This method is iterated n times for a n times SCA authorisation in a +corporate context, each creating an own authorisation sub-endpoint for the corresponding PSU authorising the cancellation-authorisation. -The ASPSP might make the usage of this access method unnecessary in case -of only one SCA process needed, since the related authorisation resource -might be automatically created by the ASPSP after the submission of the +The ASPSP might make the usage of this access method unnecessary in case +of only one SCA process needed, since the related authorisation resource +might be automatically created by the ASPSP after the submission of the payment data with the first POST payments/{payment-product} call. -The start authorisation process is a process which is needed for creating a new authorisation -or cancellation sub-resource. +The start authorisation process is a process which is needed for creating a new authorisation +or cancellation sub-resource. This applies in the following scenarios: @@ -951,16 +1121,16 @@ This applies in the following scenarios: * The signing basket needs to be authorised yet. """ - resourceDocs += ResourceDoc( - startPaymentInitiationCancellationAuthorisationTransactionAuthorisation, - apiVersion, - nameOf(startPaymentInitiationCancellationAuthorisationTransactionAuthorisation), - "POST", - "/PAYMENT_SERVICE/PAYMENT_PRODUCT/PAYMENT_ID/cancellation-authorisations", - "Start the authorisation process for the cancellation of the addressed payment (transactionAuthorisation)", - generalStartPaymentInitiationCancellationAuthorisationSummary(false), - json.parse("""{"scaAuthenticationData":""}"""), - json.parse("""{ + resourceDocs += ResourceDoc( + startPaymentInitiationCancellationAuthorisationTransactionAuthorisation, + apiVersion, + nameOf(startPaymentInitiationCancellationAuthorisationTransactionAuthorisation), + "POST", + "/PAYMENT_SERVICE/PAYMENT_PRODUCT/PAYMENT_ID/cancellation-authorisations", + "Start the authorisation process for the cancellation of the addressed payment (transactionAuthorisation)", + generalStartPaymentInitiationCancellationAuthorisationSummary(false), + json.parse("""{"scaAuthenticationData":""}"""), + json.parse("""{ "scaStatus": "received", "authorisationId": "123auth456", "psuMessage": "Please use your BankApp for transaction Authorisation.", @@ -970,65 +1140,65 @@ This applies in the following scenarios: } } }"""), - List(UserNotLoggedIn, UnknownError), - ApiTag("Payment Initiation Service (PIS)") :: apiTagBerlinGroupM :: Nil - ) - - lazy val startPaymentInitiationCancellationAuthorisationTransactionAuthorisation : OBPEndpoint = { - case paymentService :: paymentProduct :: paymentId:: "cancellation-authorisations" :: Nil JsonPost json -> _ if checkTransactionAuthorisation(json)=> { - cc => - for { - (Full(u), callContext) <- authenticatedAccess(cc) - _ <- passesPsd2Pisp(callContext) - _ <- NewStyle.function.tryons(checkPaymentServerTypeError(paymentService),404, callContext) { - PaymentServiceTypes.withName(paymentService.replaceAll("-","_")) - } - _ <- NewStyle.function.tryons(checkPaymentProductError(paymentProduct),404, callContext) { - TransactionRequestTypes.withName(paymentProduct.replaceAll("-","_").toUpperCase) - } - (transactionRequest, callContext) <- NewStyle.function.getTransactionRequestImpl(TransactionRequestId(paymentId), callContext) - _ <- Helper.booleanToFuture(failMsg= CannotStartTheAuthorisationProcessForTheCancellation, cc=callContext) { - transactionRequest.status == TransactionRequestStatus.CANCELLATION_PENDING.toString - } - (challenges, callContext) <- NewStyle.function.createChallengesC2( - List(u.userId), - ChallengeType.BERLIN_GROUP_PAYMENT_CHALLENGE, - Some(paymentId), - getScaMethodAtInstance(SEPA_CREDIT_TRANSFERS.toString).toOption, - Some(StrongCustomerAuthenticationStatus.received), - None, - None, - callContext - ) - //NOTE: in OBP it support multiple challenges, but in Berlin Group it has only one challenge. The following guard is to make sure it return the 1st challenge properly. - challenge <- NewStyle.function.tryons(InvalidConnectorResponseForCreateChallenge,400, callContext) { - challenges.head - } - } yield { - (JSONFactory_BERLIN_GROUP_1_3.createStartPaymentInitiationCancellationAuthorisation( - challenge, - paymentService, - paymentProduct, - paymentId - ), HttpCode.`201`(callContext)) - } - } - } - - resourceDocs += ResourceDoc( - startPaymentInitiationCancellationAuthorisationUpdatePsuAuthentication, - apiVersion, - nameOf(startPaymentInitiationCancellationAuthorisationUpdatePsuAuthentication), - "POST", - "/PAYMENT_SERVICE/PAYMENT_PRODUCT/PAYMENT_ID/cancellation-authorisations", - "Start the authorisation process for the cancellation of the addressed payment (updatePsuAuthentication)", - generalStartPaymentInitiationCancellationAuthorisationSummary(true), - json.parse("""{ + List(UserNotLoggedIn, UnknownError), + ApiTag("Payment Initiation Service (PIS)") :: apiTagBerlinGroupM :: Nil + ) + + lazy val startPaymentInitiationCancellationAuthorisationTransactionAuthorisation : OBPEndpoint = { + case paymentService :: paymentProduct :: paymentId:: "cancellation-authorisations" :: Nil JsonPost json -> _ if checkTransactionAuthorisation(json)=> { + cc => + for { + (Full(u), callContext) <- authenticatedAccess(cc) + _ <- passesPsd2Pisp(callContext) + _ <- NewStyle.function.tryons(checkPaymentServerTypeError(paymentService),404, callContext) { + PaymentServiceTypes.withName(paymentService.replaceAll("-","_")) + } + _ <- NewStyle.function.tryons(checkPaymentProductError(paymentProduct),404, callContext) { + TransactionRequestTypes.withName(paymentProduct.replaceAll("-","_").toUpperCase) + } + (transactionRequest, callContext) <- NewStyle.function.getTransactionRequestImpl(TransactionRequestId(paymentId), callContext) + _ <- Helper.booleanToFuture(failMsg= CannotStartTheAuthorisationProcessForTheCancellation, cc=callContext) { + transactionRequest.status == TransactionRequestStatus.CANCELLATION_PENDING.toString + } + (challenges, callContext) <- NewStyle.function.createChallengesC2( + List(u.userId), + ChallengeType.BERLIN_GROUP_PAYMENT_CHALLENGE, + Some(paymentId), + getScaMethodAtInstance(SEPA_CREDIT_TRANSFERS.toString).toOption, + Some(StrongCustomerAuthenticationStatus.received), + None, + None, + callContext + ) + //NOTE: in OBP it support multiple challenges, but in Berlin Group it has only one challenge. The following guard is to make sure it return the 1st challenge properly. + challenge <- NewStyle.function.tryons(InvalidConnectorResponseForCreateChallenge,400, callContext) { + challenges.head + } + } yield { + (JSONFactory_BERLIN_GROUP_1_3.createStartPaymentInitiationCancellationAuthorisation( + challenge, + paymentService, + paymentProduct, + paymentId + ), HttpCode.`201`(callContext)) + } + } + } + + resourceDocs += ResourceDoc( + startPaymentInitiationCancellationAuthorisationUpdatePsuAuthentication, + apiVersion, + nameOf(startPaymentInitiationCancellationAuthorisationUpdatePsuAuthentication), + "POST", + "/PAYMENT_SERVICE/PAYMENT_PRODUCT/PAYMENT_ID/cancellation-authorisations", + "Start the authorisation process for the cancellation of the addressed payment (updatePsuAuthentication)", + generalStartPaymentInitiationCancellationAuthorisationSummary(true), + json.parse("""{ "psuData": { "password": "start12" } }"""), - json.parse("""{ + json.parse("""{ "scaStatus": "received", "authorisationId": "123auth456", "psuMessage": "Please use your BankApp for transaction Authorisation.", @@ -1038,18 +1208,18 @@ This applies in the following scenarios: } } }"""), - List(UserNotLoggedIn, UnknownError), - ApiTag("Payment Initiation Service (PIS)") :: apiTagBerlinGroupM :: Nil - ) - - lazy val startPaymentInitiationCancellationAuthorisationUpdatePsuAuthentication : OBPEndpoint = { - case paymentService :: paymentProduct :: paymentId:: "cancellation-authorisations" :: Nil JsonPost json -> _ if checkUpdatePsuAuthentication(json)=> { - cc => - for { - (Full(u), callContext) <- authenticatedAccess(cc) - } yield { - (liftweb.json.parse( - """{ + List(UserNotLoggedIn, UnknownError), + ApiTag("Payment Initiation Service (PIS)") :: apiTagBerlinGroupM :: Nil + ) + + lazy val startPaymentInitiationCancellationAuthorisationUpdatePsuAuthentication : OBPEndpoint = { + case paymentService :: paymentProduct :: paymentId:: "cancellation-authorisations" :: Nil JsonPost json -> _ if checkUpdatePsuAuthentication(json)=> { + cc => + for { + (Full(u), callContext) <- authenticatedAccess(cc) + } yield { + (liftweb.json.parse( + """{ "scaStatus": "received", "authorisationId": "123auth456", "psuMessage": "Please use your BankApp for transaction Authorisation.", @@ -1059,20 +1229,20 @@ This applies in the following scenarios: } } }"""), HttpCode.`201`(callContext)) - } - } - } - - resourceDocs += ResourceDoc( - startPaymentInitiationCancellationAuthorisationSelectPsuAuthenticationMethod, - apiVersion, - nameOf(startPaymentInitiationCancellationAuthorisationSelectPsuAuthenticationMethod), - "POST", - "/PAYMENT_SERVICE/PAYMENT_PRODUCT/PAYMENT_ID/cancellation-authorisations", - "Start the authorisation process for the cancellation of the addressed payment (selectPsuAuthenticationMethod)", - generalStartPaymentInitiationCancellationAuthorisationSummary(true), - json.parse("""{"authenticationMethodId":""}"""), - json.parse("""{ + } + } + } + + resourceDocs += ResourceDoc( + startPaymentInitiationCancellationAuthorisationSelectPsuAuthenticationMethod, + apiVersion, + nameOf(startPaymentInitiationCancellationAuthorisationSelectPsuAuthenticationMethod), + "POST", + "/PAYMENT_SERVICE/PAYMENT_PRODUCT/PAYMENT_ID/cancellation-authorisations", + "Start the authorisation process for the cancellation of the addressed payment (selectPsuAuthenticationMethod)", + generalStartPaymentInitiationCancellationAuthorisationSummary(true), + json.parse("""{"authenticationMethodId":""}"""), + json.parse("""{ "scaStatus": "received", "authorisationId": "123auth456", "psuMessage": "Please use your BankApp for transaction Authorisation.", @@ -1082,18 +1252,18 @@ This applies in the following scenarios: } } }"""), - List(UserNotLoggedIn, UnknownError), - ApiTag("Payment Initiation Service (PIS)") :: apiTagBerlinGroupM :: Nil - ) - - lazy val startPaymentInitiationCancellationAuthorisationSelectPsuAuthenticationMethod : OBPEndpoint = { - case paymentService :: paymentProduct :: paymentId:: "cancellation-authorisations" :: Nil JsonPost json -> _ if checkSelectPsuAuthenticationMethod(json)=> { - cc => - for { - (Full(u), callContext) <- authenticatedAccess(cc) - } yield { - (liftweb.json.parse( - """{ + List(UserNotLoggedIn, UnknownError), + ApiTag("Payment Initiation Service (PIS)") :: apiTagBerlinGroupM :: Nil + ) + + lazy val startPaymentInitiationCancellationAuthorisationSelectPsuAuthenticationMethod : OBPEndpoint = { + case paymentService :: paymentProduct :: paymentId:: "cancellation-authorisations" :: Nil JsonPost json -> _ if checkSelectPsuAuthenticationMethod(json)=> { + cc => + for { + (Full(u), callContext) <- authenticatedAccess(cc) + } yield { + (liftweb.json.parse( + """{ "scaStatus": "received", "authorisationId": "123auth456", "psuMessage": "Please use your BankApp for transaction Authorisation.", @@ -1103,21 +1273,21 @@ This applies in the following scenarios: } } }"""), HttpCode.`201`(callContext)) - } - } - } + } + } + } - def generalUpdatePaymentCancellationPsuDataSummary (isMockedData: Boolean)= - s"""${mockedDataText(isMockedData)} -This method updates PSU data on the cancellation authorisation resource if needed. + def generalUpdatePaymentCancellationPsuDataSummary (isMockedData: Boolean)= + s"""${mockedDataText(isMockedData)} +This method updates PSU data on the cancellation authorisation resource if needed. It may authorise a cancellation of the payment within the Embedded SCA Approach where needed. -Independently from the SCA Approach it supports e.g. the selection of +Independently from the SCA Approach it supports e.g. the selection of the authentication method and a non-SCA PSU authentication. -This methods updates PSU data on the cancellation authorisation resource if needed. +This methods updates PSU data on the cancellation authorisation resource if needed. -There are several possible Update PSU Data requests in the context of a cancellation authorisation within the payment initiation services needed, +There are several possible Update PSU Data requests in the context of a cancellation authorisation within the payment initiation services needed, which depends on the SCA approach: * Redirect SCA Approach: @@ -1127,13 +1297,13 @@ A specific Update PSU Data Request is applicable for A specific Update PSU Data Request is only applicable for * adding the PSU Identification, if not provided yet in the Payment Initiation Request or the Account Information Consent Request, or if no OAuth2 access token is used, or * the selection of authentication methods. -* Embedded SCA Approach: +* Embedded SCA Approach: The Update PSU Data Request might be used * to add credentials as a first factor authentication data of the PSU and * to select the authentication method and * transaction authorisation. -The SCA Approach might depend on the chosen SCA method. +The SCA Approach might depend on the chosen SCA method. For that reason, the following possible Update PSU Data request can apply to all SCA approaches: * Select an SCA method in case of several SCA methods are available for the customer. @@ -1151,132 +1321,132 @@ There are the following request types on this access path: Maybe in a later version the access path will change. """ - resourceDocs += ResourceDoc( - updatePaymentCancellationPsuDataTransactionAuthorisation, - apiVersion, - nameOf(updatePaymentCancellationPsuDataTransactionAuthorisation), - "PUT", - "/PAYMENT_SERVICE/PAYMENT_PRODUCT/PAYMENT_ID/cancellation-authorisations/AUTHORISATION_ID", - "Update PSU Data for payment initiation cancellation (transactionAuthorisation)", - generalUpdatePaymentCancellationPsuDataSummary(false), - json.parse("""{"scaAuthenticationData":"123"}"""), - json.parse("""{ + resourceDocs += ResourceDoc( + updatePaymentCancellationPsuDataTransactionAuthorisation, + apiVersion, + nameOf(updatePaymentCancellationPsuDataTransactionAuthorisation), + "PUT", + "/PAYMENT_SERVICE/PAYMENT_PRODUCT/PAYMENT_ID/cancellation-authorisations/AUTHORISATION_ID", + "Update PSU Data for payment initiation cancellation (transactionAuthorisation)", + generalUpdatePaymentCancellationPsuDataSummary(false), + json.parse("""{"scaAuthenticationData":"123"}"""), + json.parse("""{ "scaStatus":"finalised", "psuMessage":"Please check your SMS at a mobile device.", "_links":{ "scaStatus":"/v1.3/payments/sepa-credit-transfers/PAYMENT_ID/4f4a8b7f-9968-4183-92ab-ca512b396bfc" } }"""), - List(UserNotLoggedIn, UnknownError), - ApiTag("Payment Initiation Service (PIS)") :: apiTagBerlinGroupM :: Nil - ) - - lazy val updatePaymentCancellationPsuDataTransactionAuthorisation : OBPEndpoint = { - case paymentService :: paymentProduct :: paymentId:: "cancellation-authorisations" :: authorisationId :: Nil JsonPut json -> _ if checkTransactionAuthorisation(json) => { - cc => - for { - (_, callContext) <- authenticatedAccess(cc) - _ <- passesPsd2Pisp(callContext) - failMsg = s"$InvalidJsonFormat The Json body should be the $UpdatePaymentPsuDataJson " - transactionAuthorisation <- NewStyle.function.tryons(failMsg, 400, callContext) { - json.extract[TransactionAuthorisation] - } - - _ <- NewStyle.function.tryons(checkPaymentServerTypeError(paymentService),404, callContext) { - PaymentServiceTypes.withName(paymentService.replaceAll("-","_")) - } - _ <- NewStyle.function.tryons(checkPaymentProductError(paymentProduct),404, callContext) { - TransactionRequestTypes.withName(paymentProduct.replaceAll("-","_").toUpperCase) - } - //Map obp transaction request id with BerlinGroup PaymentId - transactionRequestId = TransactionRequestId(paymentId) - (existingTransactionRequest, callContext) <- NewStyle.function.getTransactionRequestImpl(transactionRequestId, callContext) - _ <- Helper.booleanToFuture(failMsg= CannotUpdatePSUDataCancellation, cc=callContext) { - existingTransactionRequest.status == TransactionRequestStatus.INITIATED.toString || - existingTransactionRequest.status == TransactionRequestStatus.CANCELLATION_PENDING.toString || - existingTransactionRequest.status == TransactionRequestStatus.COMPLETED.toString - } - (_, callContext) <- NewStyle.function.getTransactionRequestImpl(TransactionRequestId(paymentId), callContext) - (challenge, callContext) <- NewStyle.function.validateChallengeAnswerC4( - ChallengeType.BERLIN_GROUP_PAYMENT_CHALLENGE, - Some(paymentId), - None, - authorisationId, - transactionAuthorisation.scaAuthenticationData, - SuppliedAnswerType.PLAIN_TEXT_VALUE, - callContext - ) - - (fromAccount, callContext) <- NewStyle.function.checkBankAccountExists( - BankId(existingTransactionRequest.from.bank_id), - AccountId(existingTransactionRequest.from.account_id), - callContext - ) - _ <- challenge.scaStatus match { - case Some(status) if status == StrongCustomerAuthenticationStatus.finalised => // finalised - NewStyle.function.saveTransactionRequestStatusImpl(existingTransactionRequest.id, CANCELLED.toString, callContext) - case Some(status) if status == StrongCustomerAuthenticationStatus.failed => // failed - NewStyle.function.saveTransactionRequestStatusImpl(existingTransactionRequest.id, REJECTED.toString, callContext) - case _ => // all other cases - Future(Full(true)) - } - } yield { - (JSONFactory_BERLIN_GROUP_1_3.createStartPaymentCancellationAuthorisationJson( - challenge, - paymentService, - paymentProduct, - paymentId - ), callContext) - } - } - } - - resourceDocs += ResourceDoc( - updatePaymentCancellationPsuDataUpdatePsuAuthentication, - apiVersion, - nameOf(updatePaymentCancellationPsuDataUpdatePsuAuthentication), - "PUT", - "/PAYMENT_SERVICE/PAYMENT_PRODUCT/PAYMENT_ID/cancellation-authorisations/AUTHORISATION_ID", - "Update PSU Data for payment initiation cancellation (updatePsuAuthentication)", - generalUpdatePaymentCancellationPsuDataSummary(true), - json.parse("""{ "psuData":{"password":"start12" }}"""), - json.parse("""{ + List(UserNotLoggedIn, UnknownError), + ApiTag("Payment Initiation Service (PIS)") :: apiTagBerlinGroupM :: Nil + ) + + lazy val updatePaymentCancellationPsuDataTransactionAuthorisation : OBPEndpoint = { + case paymentService :: paymentProduct :: paymentId:: "cancellation-authorisations" :: authorisationId :: Nil JsonPut json -> _ if checkTransactionAuthorisation(json) => { + cc => + for { + (_, callContext) <- authenticatedAccess(cc) + _ <- passesPsd2Pisp(callContext) + failMsg = s"$InvalidJsonFormat The Json body should be the $UpdatePaymentPsuDataJson " + transactionAuthorisation <- NewStyle.function.tryons(failMsg, 400, callContext) { + json.extract[TransactionAuthorisation] + } + + _ <- NewStyle.function.tryons(checkPaymentServerTypeError(paymentService),404, callContext) { + PaymentServiceTypes.withName(paymentService.replaceAll("-","_")) + } + _ <- NewStyle.function.tryons(checkPaymentProductError(paymentProduct),404, callContext) { + TransactionRequestTypes.withName(paymentProduct.replaceAll("-","_").toUpperCase) + } + //Map obp transaction request id with BerlinGroup PaymentId + transactionRequestId = TransactionRequestId(paymentId) + (existingTransactionRequest, callContext) <- NewStyle.function.getTransactionRequestImpl(transactionRequestId, callContext) + _ <- Helper.booleanToFuture(failMsg= CannotUpdatePSUDataCancellation, cc=callContext) { + existingTransactionRequest.status == TransactionRequestStatus.INITIATED.toString || + existingTransactionRequest.status == TransactionRequestStatus.CANCELLATION_PENDING.toString || + existingTransactionRequest.status == TransactionRequestStatus.COMPLETED.toString + } + (_, callContext) <- NewStyle.function.getTransactionRequestImpl(TransactionRequestId(paymentId), callContext) + (challenge, callContext) <- NewStyle.function.validateChallengeAnswerC4( + ChallengeType.BERLIN_GROUP_PAYMENT_CHALLENGE, + Some(paymentId), + None, + authorisationId, + transactionAuthorisation.scaAuthenticationData, + SuppliedAnswerType.PLAIN_TEXT_VALUE, + callContext + ) + + (fromAccount, callContext) <- NewStyle.function.checkBankAccountExists( + BankId(existingTransactionRequest.from.bank_id), + AccountId(existingTransactionRequest.from.account_id), + callContext + ) + _ <- challenge.scaStatus match { + case Some(status) if status == StrongCustomerAuthenticationStatus.finalised => // finalised + NewStyle.function.saveTransactionRequestStatusImpl(existingTransactionRequest.id, CANCELLED.toString, callContext) + case Some(status) if status == StrongCustomerAuthenticationStatus.failed => // failed + NewStyle.function.saveTransactionRequestStatusImpl(existingTransactionRequest.id, REJECTED.toString, callContext) + case _ => // all other cases + Future(Full(true)) + } + } yield { + (JSONFactory_BERLIN_GROUP_1_3.createStartPaymentCancellationAuthorisationJson( + challenge, + paymentService, + paymentProduct, + paymentId + ), callContext) + } + } + } + + resourceDocs += ResourceDoc( + updatePaymentCancellationPsuDataUpdatePsuAuthentication, + apiVersion, + nameOf(updatePaymentCancellationPsuDataUpdatePsuAuthentication), + "PUT", + "/PAYMENT_SERVICE/PAYMENT_PRODUCT/PAYMENT_ID/cancellation-authorisations/AUTHORISATION_ID", + "Update PSU Data for payment initiation cancellation (updatePsuAuthentication)", + generalUpdatePaymentCancellationPsuDataSummary(true), + json.parse("""{ "psuData":{"password":"start12" }}"""), + json.parse("""{ "scaStatus": "psuAuthenticated", "_links": { "authoriseTransaction": {"href": "/psd2/v1.3/payments/1234-wertiq-983/authorisations/123auth456"} } }"""), - List(UserNotLoggedIn, UnknownError), - ApiTag("Payment Initiation Service (PIS)") :: apiTagBerlinGroupM :: Nil - ) - - lazy val updatePaymentCancellationPsuDataUpdatePsuAuthentication : OBPEndpoint = { - case paymentService :: paymentProduct :: paymentId:: "cancellation-authorisations" :: authorisationId :: Nil JsonPut json -> _ if checkUpdatePsuAuthentication(json)=> { - cc => - for { - (_, callContext) <- authenticatedAccess(cc) - } yield { - (net.liftweb.json.parse( - """{ + List(UserNotLoggedIn, UnknownError), + ApiTag("Payment Initiation Service (PIS)") :: apiTagBerlinGroupM :: Nil + ) + + lazy val updatePaymentCancellationPsuDataUpdatePsuAuthentication : OBPEndpoint = { + case paymentService :: paymentProduct :: paymentId:: "cancellation-authorisations" :: authorisationId :: Nil JsonPut json -> _ if checkUpdatePsuAuthentication(json)=> { + cc => + for { + (_, callContext) <- authenticatedAccess(cc) + } yield { + (net.liftweb.json.parse( + """{ "scaStatus": "psuAuthenticated", "_links": { "authoriseTransaction": {"href": "/psd2/v1.3/payments/1234-wertiq-983/authorisations/123auth456"} } }"""), callContext) - } - } - } - - resourceDocs += ResourceDoc( - updatePaymentCancellationPsuDataSelectPsuAuthenticationMethod, - apiVersion, - nameOf(updatePaymentCancellationPsuDataSelectPsuAuthenticationMethod), - "PUT", - "/PAYMENT_SERVICE/PAYMENT_PRODUCT/PAYMENT_ID/cancellation-authorisations/AUTHORISATION_ID", - "Update PSU Data for payment initiation cancellation (selectPsuAuthenticationMethod)", - generalUpdatePaymentCancellationPsuDataSummary(true), - json.parse("""{"authenticationMethodId":""}"""), - json.parse("""{ + } + } + } + + resourceDocs += ResourceDoc( + updatePaymentCancellationPsuDataSelectPsuAuthenticationMethod, + apiVersion, + nameOf(updatePaymentCancellationPsuDataSelectPsuAuthenticationMethod), + "PUT", + "/PAYMENT_SERVICE/PAYMENT_PRODUCT/PAYMENT_ID/cancellation-authorisations/AUTHORISATION_ID", + "Update PSU Data for payment initiation cancellation (selectPsuAuthenticationMethod)", + generalUpdatePaymentCancellationPsuDataSummary(true), + json.parse("""{"authenticationMethodId":""}"""), + json.parse("""{ "scaStatus": "scaMethodSelected", "chosenScaMethod": { "authenticationType": "SMS_OTP", @@ -1288,18 +1458,18 @@ There are the following request types on this access path: "authoriseTransaction": {"href": "/psd2/v1.3/payments/1234-wertiq-983/authorisations/123auth456"} } }"""), - List(UserNotLoggedIn, UnknownError), - ApiTag("Payment Initiation Service (PIS)") :: apiTagBerlinGroupM :: Nil - ) - - lazy val updatePaymentCancellationPsuDataSelectPsuAuthenticationMethod : OBPEndpoint = { - case paymentService :: paymentProduct :: paymentId:: "cancellation-authorisations" :: authorisationId :: Nil JsonPut json -> _ if checkSelectPsuAuthenticationMethod(json)=> { - cc => - for { - (_, callContext) <- authenticatedAccess(cc) - } yield { - (net.liftweb.json.parse( - """{ + List(UserNotLoggedIn, UnknownError), + ApiTag("Payment Initiation Service (PIS)") :: apiTagBerlinGroupM :: Nil + ) + + lazy val updatePaymentCancellationPsuDataSelectPsuAuthenticationMethod : OBPEndpoint = { + case paymentService :: paymentProduct :: paymentId:: "cancellation-authorisations" :: authorisationId :: Nil JsonPut json -> _ if checkSelectPsuAuthenticationMethod(json)=> { + cc => + for { + (_, callContext) <- authenticatedAccess(cc) + } yield { + (net.liftweb.json.parse( + """{ "scaStatus": "scaMethodSelected", "chosenScaMethod": { "authenticationType": "SMS_OTP", @@ -1311,56 +1481,56 @@ There are the following request types on this access path: "authoriseTransaction": {"href": "/psd2/v1.3/payments/1234-wertiq-983/authorisations/123auth456"} } }"""), callContext) - } - } - } - - resourceDocs += ResourceDoc( - updatePaymentCancellationPsuDataAuthorisationConfirmation, - apiVersion, - nameOf(updatePaymentCancellationPsuDataAuthorisationConfirmation), - "PUT", - "/PAYMENT_SERVICE/PAYMENT_PRODUCT/PAYMENT_ID/cancellation-authorisations/AUTHORISATION_ID", - "Update PSU Data for payment initiation cancellation (authorisationConfirmation)", - generalUpdatePaymentCancellationPsuDataSummary(true), - json.parse("""{"confirmationCode":"confirmationCode"}"""), - json.parse("""{ + } + } + } + + resourceDocs += ResourceDoc( + updatePaymentCancellationPsuDataAuthorisationConfirmation, + apiVersion, + nameOf(updatePaymentCancellationPsuDataAuthorisationConfirmation), + "PUT", + "/PAYMENT_SERVICE/PAYMENT_PRODUCT/PAYMENT_ID/cancellation-authorisations/AUTHORISATION_ID", + "Update PSU Data for payment initiation cancellation (authorisationConfirmation)", + generalUpdatePaymentCancellationPsuDataSummary(true), + json.parse("""{"confirmationCode":"confirmationCode"}"""), + json.parse("""{ "scaStatus": "finalised", "_links":{ "status": {"href":"/v1.3/payments/sepa-credit-transfers/qwer3456tzui7890/status"} } }"""), - List(UserNotLoggedIn, UnknownError), - ApiTag("Payment Initiation Service (PIS)") :: apiTagBerlinGroupM :: Nil - ) - - lazy val updatePaymentCancellationPsuDataAuthorisationConfirmation : OBPEndpoint = { - case paymentService :: paymentProduct :: paymentId:: "cancellation-authorisations" :: authorisationId :: Nil JsonPut json -> _ if checkAuthorisationConfirmation(json)=> { - cc => - for { - (_, callContext) <- authenticatedAccess(cc) - } yield { - (net.liftweb.json.parse( - """{ + List(UserNotLoggedIn, UnknownError), + ApiTag("Payment Initiation Service (PIS)") :: apiTagBerlinGroupM :: Nil + ) + + lazy val updatePaymentCancellationPsuDataAuthorisationConfirmation : OBPEndpoint = { + case paymentService :: paymentProduct :: paymentId:: "cancellation-authorisations" :: authorisationId :: Nil JsonPut json -> _ if checkAuthorisationConfirmation(json)=> { + cc => + for { + (_, callContext) <- authenticatedAccess(cc) + } yield { + (net.liftweb.json.parse( + """{ "scaStatus": "finalised", "_links":{ "status": {"href":"/v1.3/payments/sepa-credit-transfers/qwer3456tzui7890/status"} } }"""), callContext) - } - } - } + } + } + } - def generalUpdatePaymentPsuDataSumarry(isMockedData: Boolean) = - s"""${mockedDataText(isMockedData)} -This methods updates PSU data on the authorisation resource if needed. + def generalUpdatePaymentPsuDataSumarry(isMockedData: Boolean) = + s"""${mockedDataText(isMockedData)} +This methods updates PSU data on the authorisation resource if needed. It may authorise a payment within the Embedded SCA Approach where needed. -Independently from the SCA Approach it supports e.g. the selection of +Independently from the SCA Approach it supports e.g. the selection of the authentication method and a non-SCA PSU authentication. -There are several possible Update PSU Data requests in the context of payment initiation services needed, +There are several possible Update PSU Data requests in the context of payment initiation services needed, which depends on the SCA approach: * Redirect SCA Approach: @@ -1370,13 +1540,13 @@ A specific Update PSU Data Request is applicable for A specific Update PSU Data Request is only applicable for * adding the PSU Identification, if not provided yet in the Payment Initiation Request or the Account Information Consent Request, or if no OAuth2 access token is used, or * the selection of authentication methods. -* Embedded SCA Approach: +* Embedded SCA Approach: The Update PSU Data Request might be used * to add credentials as a first factor authentication data of the PSU and * to select the authentication method and * transaction authorisation. -The SCA Approach might depend on the chosen SCA method. +The SCA Approach might depend on the chosen SCA method. For that reason, the following possible Update PSU Data request can apply to all SCA approaches: * Select an SCA method in case of several SCA methods are available for the customer. @@ -1398,130 +1568,130 @@ There are the following request types on this access path: """ - resourceDocs += ResourceDoc( - updatePaymentPsuDataTransactionAuthorisation, - apiVersion, - nameOf(updatePaymentPsuDataTransactionAuthorisation), - "PUT", - "/PAYMENT_SERVICE/PAYMENT_PRODUCT/PAYMENT_ID/authorisations/AUTHORISATION_ID", - "Update PSU data for payment initiation (transactionAuthorisation)", - generalUpdatePaymentPsuDataSumarry(false), - json.parse("""{"scaAuthenticationData":"123"}"""), - json.parse("""{ + resourceDocs += ResourceDoc( + updatePaymentPsuDataTransactionAuthorisation, + apiVersion, + nameOf(updatePaymentPsuDataTransactionAuthorisation), + "PUT", + "/PAYMENT_SERVICE/PAYMENT_PRODUCT/PAYMENT_ID/authorisations/AUTHORISATION_ID", + "Update PSU data for payment initiation (transactionAuthorisation)", + generalUpdatePaymentPsuDataSumarry(false), + json.parse("""{"scaAuthenticationData":"123"}"""), + json.parse("""{ "scaStatus": "finalised", "psuMessage": "Please check your SMS at a mobile device.", "_links": { "scaStatus": {"href":"/v1.3/payments/sepa-credit-transfers/88695566-6642-46d5-9985-0d824624f507"} } }"""), - List(UserNotLoggedIn, UnknownError), - ApiTag("Payment Initiation Service (PIS)") :: apiTagBerlinGroupM :: Nil - ) - - lazy val updatePaymentPsuDataTransactionAuthorisation : OBPEndpoint = { - case paymentService :: paymentProduct :: paymentId:: "authorisations" :: authorisationId :: Nil JsonPut json -> _ if checkTransactionAuthorisation(json) => { - cc => - for { - (Full(u), callContext) <- authenticatedAccess(cc) - _ <- passesPsd2Pisp(callContext) - failMsg = s"$InvalidJsonFormat The Json body should be the $TransactionAuthorisation " - transactionAuthorisationJson <- NewStyle.function.tryons(failMsg, 400, callContext) { - json.extract[TransactionAuthorisation] - } - - _ <- NewStyle.function.tryons(checkPaymentServerTypeError(paymentService),404, callContext) { - PaymentServiceTypes.withName(paymentService.replaceAll("-","_")) - } - _ <- NewStyle.function.tryons(checkPaymentProductError(paymentProduct),404, callContext) { - TransactionRequestTypes.withName(paymentProduct.replaceAll("-","_").toUpperCase) - } - //Map obp transaction request id with BerlinGroup PaymentId - transactionRequestId = TransactionRequestId(paymentId) - (existingTransactionRequest, callContext) <- NewStyle.function.getTransactionRequestImpl(transactionRequestId, callContext) - _ <- Helper.booleanToFuture(failMsg= CannotUpdatePSUData, cc=callContext) { - existingTransactionRequest.status == TransactionStatus.RCVD.code - } - (_, callContext) <- NewStyle.function.getChallenge(authorisationId, callContext) - (challenge, callContext) <- NewStyle.function.validateChallengeAnswerC4( - ChallengeType.BERLIN_GROUP_PAYMENT_CHALLENGE, - Some(paymentId), - None, - authorisationId, - transactionAuthorisationJson.scaAuthenticationData, - SuppliedAnswerType.PLAIN_TEXT_VALUE, - callContext - ) - - (fromAccount, callContext) <- NewStyle.function.checkBankAccountExists( - BankId(existingTransactionRequest.from.bank_id), - AccountId(existingTransactionRequest.from.account_id), - callContext - ) - _ <- challenge.scaStatus match { - case Some(status) if status == StrongCustomerAuthenticationStatus.finalised => // finalised - NewStyle.function.createTransactionAfterChallengeV210(fromAccount, existingTransactionRequest, callContext) map { - response => - NewStyle.function.saveTransactionRequestStatusImpl(existingTransactionRequest.id, COMPLETED.toString, callContext) - } - case Some(status) if status == StrongCustomerAuthenticationStatus.failed => // failed - NewStyle.function.saveTransactionRequestStatusImpl(existingTransactionRequest.id, REJECTED.toString, callContext) - case _ => // started and all other cases - Future(Full(true)) - } - } yield { - (JSONFactory_BERLIN_GROUP_1_3.createUpdatePaymentPsuDataTransactionAuthorisationJson(challenge), callContext) - } - } - } - - resourceDocs += ResourceDoc( - updatePaymentPsuDataUpdatePsuAuthentication, - apiVersion, - nameOf(updatePaymentPsuDataUpdatePsuAuthentication), - "PUT", - "/PAYMENT_SERVICE/PAYMENT_PRODUCT/PAYMENT_ID/authorisations/AUTHORISATION_ID", - "Update PSU data for payment initiation (updatePsuAuthentication)", - generalUpdatePaymentPsuDataSumarry(true), - json.parse("""{"psuData": {"password": "start12"}}""".stripMargin), - json.parse("""{ + List(UserNotLoggedIn, UnknownError), + ApiTag("Payment Initiation Service (PIS)") :: apiTagBerlinGroupM :: Nil + ) + + lazy val updatePaymentPsuDataTransactionAuthorisation : OBPEndpoint = { + case paymentService :: paymentProduct :: paymentId:: "authorisations" :: authorisationId :: Nil JsonPut json -> _ if checkTransactionAuthorisation(json) => { + cc => + for { + (Full(u), callContext) <- authenticatedAccess(cc) + _ <- passesPsd2Pisp(callContext) + failMsg = s"$InvalidJsonFormat The Json body should be the $TransactionAuthorisation " + transactionAuthorisationJson <- NewStyle.function.tryons(failMsg, 400, callContext) { + json.extract[TransactionAuthorisation] + } + + _ <- NewStyle.function.tryons(checkPaymentServerTypeError(paymentService),404, callContext) { + PaymentServiceTypes.withName(paymentService.replaceAll("-","_")) + } + _ <- NewStyle.function.tryons(checkPaymentProductError(paymentProduct),404, callContext) { + TransactionRequestTypes.withName(paymentProduct.replaceAll("-","_").toUpperCase) + } + //Map obp transaction request id with BerlinGroup PaymentId + transactionRequestId = TransactionRequestId(paymentId) + (existingTransactionRequest, callContext) <- NewStyle.function.getTransactionRequestImpl(transactionRequestId, callContext) + _ <- Helper.booleanToFuture(failMsg= CannotUpdatePSUData, cc=callContext) { + existingTransactionRequest.status == TransactionStatus.RCVD.code + } + (_, callContext) <- NewStyle.function.getChallenge(authorisationId, callContext) + (challenge, callContext) <- NewStyle.function.validateChallengeAnswerC4( + ChallengeType.BERLIN_GROUP_PAYMENT_CHALLENGE, + Some(paymentId), + None, + authorisationId, + transactionAuthorisationJson.scaAuthenticationData, + SuppliedAnswerType.PLAIN_TEXT_VALUE, + callContext + ) + + (fromAccount, callContext) <- NewStyle.function.checkBankAccountExists( + BankId(existingTransactionRequest.from.bank_id), + AccountId(existingTransactionRequest.from.account_id), + callContext + ) + _ <- challenge.scaStatus match { + case Some(status) if status == StrongCustomerAuthenticationStatus.finalised => // finalised + NewStyle.function.createTransactionAfterChallengeV210(fromAccount, existingTransactionRequest, callContext) map { + response => + NewStyle.function.saveTransactionRequestStatusImpl(existingTransactionRequest.id, COMPLETED.toString, callContext) + } + case Some(status) if status == StrongCustomerAuthenticationStatus.failed => // failed + NewStyle.function.saveTransactionRequestStatusImpl(existingTransactionRequest.id, REJECTED.toString, callContext) + case _ => // started and all other cases + Future(Full(true)) + } + } yield { + (JSONFactory_BERLIN_GROUP_1_3.createUpdatePaymentPsuDataTransactionAuthorisationJson(challenge), callContext) + } + } + } + + resourceDocs += ResourceDoc( + updatePaymentPsuDataUpdatePsuAuthentication, + apiVersion, + nameOf(updatePaymentPsuDataUpdatePsuAuthentication), + "PUT", + "/PAYMENT_SERVICE/PAYMENT_PRODUCT/PAYMENT_ID/authorisations/AUTHORISATION_ID", + "Update PSU data for payment initiation (updatePsuAuthentication)", + generalUpdatePaymentPsuDataSumarry(true), + json.parse("""{"psuData": {"password": "start12"}}""".stripMargin), + json.parse("""{ "scaStatus": "finalised", "_links": { "scaStatus": {"href":"/v1.3/payments/sepa-credit-transfers/88695566-6642-46d5-9985-0d824624f507"} } }"""), - List(UserNotLoggedIn, UnknownError), - ApiTag("Payment Initiation Service (PIS)") :: apiTagBerlinGroupM :: Nil - ) - - lazy val updatePaymentPsuDataUpdatePsuAuthentication : OBPEndpoint = { - case paymentService :: paymentProduct :: paymentId:: "authorisations" :: authorisationid :: Nil JsonPut json -> _ if checkUpdatePsuAuthentication(json) => { - cc => - for { - (Full(u), callContext) <- authenticatedAccess(cc) - - } yield { - (liftweb.json.parse( - """{ + List(UserNotLoggedIn, UnknownError), + ApiTag("Payment Initiation Service (PIS)") :: apiTagBerlinGroupM :: Nil + ) + + lazy val updatePaymentPsuDataUpdatePsuAuthentication : OBPEndpoint = { + case paymentService :: paymentProduct :: paymentId:: "authorisations" :: authorisationid :: Nil JsonPut json -> _ if checkUpdatePsuAuthentication(json) => { + cc => + for { + (Full(u), callContext) <- authenticatedAccess(cc) + + } yield { + (liftweb.json.parse( + """{ "scaStatus": "finalised", "_links": { "scaStatus": {"href":"/v1.3/payments/sepa-credit-transfers/88695566-6642-46d5-9985-0d824624f507"} } }"""), callContext) - } - } - } - - resourceDocs += ResourceDoc( - updatePaymentPsuDataSelectPsuAuthenticationMethod, - apiVersion, - nameOf(updatePaymentPsuDataSelectPsuAuthenticationMethod), - "PUT", - "/PAYMENT_SERVICE/PAYMENT_PRODUCT/PAYMENT_ID/authorisations/AUTHORISATION_ID", - "Update PSU data for payment initiation (selectPsuAuthenticationMethod)", - generalUpdatePaymentPsuDataSumarry(true), - json.parse("""{"authenticationMethodId":""}"""), - json.parse( - """{ + } + } + } + + resourceDocs += ResourceDoc( + updatePaymentPsuDataSelectPsuAuthenticationMethod, + apiVersion, + nameOf(updatePaymentPsuDataSelectPsuAuthenticationMethod), + "PUT", + "/PAYMENT_SERVICE/PAYMENT_PRODUCT/PAYMENT_ID/authorisations/AUTHORISATION_ID", + "Update PSU data for payment initiation (selectPsuAuthenticationMethod)", + generalUpdatePaymentPsuDataSumarry(true), + json.parse("""{"authenticationMethodId":""}"""), + json.parse( + """{ "scaStatus": "scaMethodSelected", "chosenScaMethod": { "authenticationType": "SMS_OTP", @@ -1533,19 +1703,19 @@ There are the following request types on this access path: "authoriseTransaction": {"href": "/psd2/v1.3/payments/1234-wertiq-983/authorisations/123auth456"} } }"""), - List(UserNotLoggedIn, UnknownError), - ApiTag("Payment Initiation Service (PIS)") :: apiTagBerlinGroupM :: Nil - ) - - lazy val updatePaymentPsuDataSelectPsuAuthenticationMethod : OBPEndpoint = { - case paymentService :: paymentProduct :: paymentId:: "authorisations" :: authorisationid :: Nil JsonPut json -> _ if checkSelectPsuAuthenticationMethod(json) => { - cc => - for { - (Full(u), callContext) <- authenticatedAccess(cc) - - } yield { - (liftweb.json.parse( - """{ + List(UserNotLoggedIn, UnknownError), + ApiTag("Payment Initiation Service (PIS)") :: apiTagBerlinGroupM :: Nil + ) + + lazy val updatePaymentPsuDataSelectPsuAuthenticationMethod : OBPEndpoint = { + case paymentService :: paymentProduct :: paymentId:: "authorisations" :: authorisationid :: Nil JsonPut json -> _ if checkSelectPsuAuthenticationMethod(json) => { + cc => + for { + (Full(u), callContext) <- authenticatedAccess(cc) + + } yield { + (liftweb.json.parse( + """{ "scaStatus": "scaMethodSelected", "chosenScaMethod": { "authenticationType": "SMS_OTP", @@ -1557,49 +1727,46 @@ There are the following request types on this access path: "authoriseTransaction": {"href": "/psd2/v1.3/payments/1234-wertiq-983/authorisations/123auth456"} } }"""), callContext) - } - } - } - - resourceDocs += ResourceDoc( - updatePaymentPsuDataAuthorisationConfirmation, - apiVersion, - nameOf(updatePaymentPsuDataAuthorisationConfirmation), - "PUT", - "/PAYMENT_SERVICE/PAYMENT_PRODUCT/PAYMENT_ID/authorisations/AUTHORISATION_ID", - "Update PSU data for payment initiation (authorisationConfirmation)", - generalUpdatePaymentPsuDataSumarry(true), - json.parse("""{"confirmationCode":"confirmationCode"}"""), - json.parse( - """{ + } + } + } + + resourceDocs += ResourceDoc( + updatePaymentPsuDataAuthorisationConfirmation, + apiVersion, + nameOf(updatePaymentPsuDataAuthorisationConfirmation), + "PUT", + "/PAYMENT_SERVICE/PAYMENT_PRODUCT/PAYMENT_ID/authorisations/AUTHORISATION_ID", + "Update PSU data for payment initiation (authorisationConfirmation)", + generalUpdatePaymentPsuDataSumarry(true), + json.parse("""{"confirmationCode":"confirmationCode"}"""), + json.parse( + """{ "scaStatus": "finalised", "_links":{ "status": {"href":"/v1.3/payments/sepa-credit-transfers/qwer3456tzui7890/status"} } }"""), - List(UserNotLoggedIn, UnknownError), - ApiTag("Payment Initiation Service (PIS)") :: apiTagBerlinGroupM :: Nil - ) - - lazy val updatePaymentPsuDataAuthorisationConfirmation : OBPEndpoint = { - case paymentService :: paymentProduct :: paymentId:: "authorisations" :: authorisationid :: Nil JsonPut json -> _ if checkAuthorisationConfirmation(json) => { - cc => - for { - (Full(u), callContext) <- authenticatedAccess(cc) - - } yield { - (liftweb.json.parse( - """{ + List(UserNotLoggedIn, UnknownError), + ApiTag("Payment Initiation Service (PIS)") :: apiTagBerlinGroupM :: Nil + ) + + lazy val updatePaymentPsuDataAuthorisationConfirmation : OBPEndpoint = { + case paymentService :: paymentProduct :: paymentId:: "authorisations" :: authorisationid :: Nil JsonPut json -> _ if checkAuthorisationConfirmation(json) => { + cc => + for { + (Full(u), callContext) <- authenticatedAccess(cc) + + } yield { + (liftweb.json.parse( + """{ "scaStatus": "finalised", "_links":{ "status": {"href":"/v1.3/payments/sepa-credit-transfers/qwer3456tzui7890/status"} } }"""), callContext) - } - } - } - -} - - + } + } + } +} \ No newline at end of file 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 eb69ccc555..fc0157623e 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 @@ -102,7 +102,15 @@ object TransactionStatus extends ApiModel { case other => other } } - + def mapTransactionStatusMdV1(status: String): String = { + status match { + case "COMPLETED" => TransactionStatus.ACCC.code + case "INITIATED" => TransactionStatus.RCVD.code + case "CANCELED" => TransactionStatus.CANC.code + case "REJECTED" => TransactionStatus.RJCT.code + case other => other + } + } } 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 c352fa46bf..68f76dabbe 100644 --- a/obp-api/src/main/scala/code/api/util/APIUtil.scala +++ b/obp-api/src/main/scala/code/api/util/APIUtil.scala @@ -3040,31 +3040,31 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{ // OBP-API supports two methods for identifying/validating API consumers (applications): // 1. CONSUMER_CERTIFICATE - Uses mTLS certificates or certificate headers (more secure, PSD2 compliant) // 2. CONSUMER_KEY_VALUE - Uses traditional API keys in request headers (simpler for dev/test) - + // Step 1: Always attempt to identify consumer via certificate/mTLS // This looks for TPP-Signature-Certificate or PSD2-CERT headers, or mTLS client certificates val consumerByCertificate = Consent.getCurrentConsumerViaTppSignatureCertOrMtls(callContext = cc) logger.debug(s"getUserAndSessionContextFuture says consumerByCertificate is: $consumerByCertificate") - + // Step 2: Check which validation method is configured for consent requests // Default is CONSUMER_CERTIFICATE (certificate-based validation) // Alternative is CONSUMER_KEY_VALUE (consumer key-based validation) val method = APIUtil.getPropsValue(nameOfProperty = "consumer_validation_method_for_consent", defaultValue = "CONSUMER_CERTIFICATE") - + // Step 3: Conditionally attempt to identify consumer via consumer key (only if method allows it) val consumerByConsumerKey = getConsumerKey(reqHeaders) match { case Some(consumerKey) if method == "CONSUMER_KEY_VALUE" => // Consumer key found AND system is configured to use consumer key validation // Look up the consumer by their API key Consumers.consumers.vend.getConsumerByConsumerKey(consumerKey) - + case Some(_) => // Consumer key found BUT system is configured for certificate validation // Ignore the consumer key and return Empty (will rely on certificate validation instead) // This prevents MatchError when consumer key is present but method != "CONSUMER_KEY_VALUE" logger.warn(s"Consumer key provided in request but OBP is configured for certificate validation (method=$method). Ignoring consumer key and using certificate validation instead.") Empty - + case None => // No consumer key found in request headers // This is normal for certificate-based validation or anonymous requests @@ -3241,12 +3241,8 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{ } map { // Inject logged in user into CallContext data x => (x._1, x._2.map(_.copy(user = x._1))) } - } - - - /** * This Function is used to terminate a Future used in for-comprehension with specific message and code in case that value of Box is not Full. * For example: @@ -3414,21 +3410,25 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{ if (x.msg != null) true else { logger.info("Failure: " + obj); false } } - // Remove duplicated content, because in the process of box, the FailBox will be wrapped may multiple times, and message is same. getPropsAsBoolValue("display_internal_errors", false) match { - case true => // Show all error in a chain + case true => obj.rootExceptionCause match { - case Full(cause) => obj.messageChain.split(" <- ").distinct.mkString(" <- ") + " <- " + cause - case _ => obj.messageChain.split(" <- ").distinct.mkString(" <- ") + case Full(cause) => + val chain = obj.messageChain.split(" <- ").distinct.filter(_.nonEmpty) + if (chain.isEmpty) cause.getMessage + else chain.mkString(" <- ") + " <- " + cause.getMessage + case _ => + obj.messageChain.split(" <- ").distinct.filter(_.nonEmpty).mkString(" <- ") } - case false => // Do not display internal errors + case false => val obpFailures = obj.failureChain.filter(x => messageIsNotNull(x, obj) && x.msg.startsWith("OBP-")) obpFailures match { case Nil => ErrorMessages.AnUnspecifiedOrInternalErrorOccurred - case _ => obpFailures.map(_.msg).distinct.mkString(" <- ") + case _ => + val msgs = obpFailures.map(_.msg).distinct.filter(_.nonEmpty) + msgs.mkString(" <- ") } } - } /** diff --git a/obp-api/src/main/scala/code/api/util/ApiSession.scala b/obp-api/src/main/scala/code/api/util/ApiSession.scala index 59f4af5606..4dfe4f4160 100644 --- a/obp-api/src/main/scala/code/api/util/ApiSession.scala +++ b/obp-api/src/main/scala/code/api/util/ApiSession.scala @@ -64,6 +64,8 @@ case class CallContext( username <- tryo(Some(user.name)) currentResourceUserId <- tryo(Some(user.userId)) consumerId = this.consumer.map(_.consumerId.get).openOr("") // if none, just return "" + consumerName = this.consumer.map(_.name.get).openOr("") + consumerAlias = this.consumer.map(_.alias.get).openOr("") permission <- Views.views.vend.getPermissionForUser(user) views <- tryo(permission.views) linkedCustomers <- tryo(CustomerX.customerProvider.vend.getCustomersByUserId(user.userId)) @@ -92,6 +94,8 @@ case class CallContext( correlationId = this.correlationId, sessionId = this.sessionId, consumerId = Some(consumerId), + consumerName = Some(consumerName), + consumerAlias = Some(consumerAlias), generalContext = Some(generalContextFromPassThroughHeaders), outboundAdapterAuthInfo = Some(OutboundAdapterAuthInfo( userId = currentResourceUserId, @@ -99,7 +103,7 @@ case class CallContext( linkedCustomers = likedCustomersBasic, userAuthContext = basicUserAuthContexts, if (authViews.isEmpty) None else Some(authViews))), - outboundAdapterConsenterInfo = + outboundAdapterConsenterInfo = if (this.consenter.isDefined){ Some(OutboundAdapterAuthInfo( username = this.consenter.toOption.map(_.name)))//TODO, here we may added more field to the consenter, at the moment only username is useful @@ -109,7 +113,11 @@ case class CallContext( ) }}.openOr(OutboundAdapterCallContext( //For anonymousAccess endpoints, there are no user info this.correlationId, - this.sessionId)) + this.sessionId, + consumerId = Some(this.consumer.map(_.consumerId.get).openOr("")), + consumerName = Some(this.consumer.map(_.name.get).openOr("")), + consumerAlias = Some(this.consumer.map(_.alias.get).openOr("")))) + def toLight: CallContextLight = { CallContextLight( 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 df62ba7f7c..3eb6c9e9b9 100644 --- a/obp-api/src/main/scala/code/api/util/BerlinGroupCheck.scala +++ b/obp-api/src/main/scala/code/api/util/BerlinGroupCheck.scala @@ -29,6 +29,10 @@ object BerlinGroupCheck extends MdcLoggable { .split(",") .map(_.trim.toLowerCase) .toList.filterNot(_.isEmpty) + private val berlinGroupMandatoryHeaderPyment = APIUtil.getPropsValue("berlin_group_mandatory_header_payment", defaultValue = "TPP-Redirect-URI,PSU-Geo-Location") + .split(",") + .map(_.trim.toLowerCase) + .toList.filterNot(_.isEmpty) def hasUnwantedConsentIdHeaderForBGEndpoint(path: String, reqHeaders: List[HTTPParam]): Boolean = { val headerMap: Map[String, HTTPParam] = reqHeaders.map(h => h.name.toLowerCase -> h).toMap @@ -59,10 +63,58 @@ object BerlinGroupCheck extends MdcLoggable { val missingHeaders: List[String] = { if (url.contains(ConstantsBG.berlinGroupVersion1.urlPrefix) && url.endsWith("/consents")) (berlinGroupMandatoryHeaders ++ berlinGroupMandatoryHeaderConsent).filterNot(headerMap.contains) + else if (url.contains(ConstantsBG.berlinGroupVersion1.urlPrefix) + && (url.endsWith("payments/instant-credit-transfers-md") || url.endsWith("payments/domestic-credit-transfers-md"))) + (berlinGroupMandatoryHeaders ++ berlinGroupMandatoryHeaderPyment).filterNot(headerMap.contains) else berlinGroupMandatoryHeaders.filterNot(headerMap.contains) } + object GeoLocationUtil { + + private val pattern = + """^GEO\s*:\s*([-+]?\d{1,2}(?:\.\d+)?);([-+]?\d{1,3}(?:\.\d+)?)$""".r + + def isValidPsuGeoLocation(value: String): Boolean = { + import scala.util.Try + + value match { + case pattern(latStr, lonStr) => + val latOpt = Try(latStr.toDouble).toOption + val lonOpt = Try(lonStr.toDouble).toOption + + (for { + lat <- latOpt + lon <- lonOpt + } yield lat >= -90 && lat <= 90 && lon >= -180 && lon <= 180 + ).getOrElse(false) + + case _ => + false + } + } + } + + val resultWithWrongPsuGeoLocationHeaderCheck: Option[(Box[User], Option[CallContext])] = { + val geoLocationHeaderName = "psu-geo-location" + val geoLocation: Option[String] = + headerMap.get(geoLocationHeaderName).flatMap(_.values.headOption) + + if (geoLocation.isDefined && !GeoLocationUtil.isValidPsuGeoLocation(geoLocation.get)) { + val message = ErrorMessages.NotValidPsuGeoLocation + Some( + ( + fullBoxOrException( + Empty ~> APIFailureNewStyle(message, 400, forwardResult._2.map(_.toLight)) + ), + forwardResult._2 + ) + ) + } else { + None + } + } + val resultWithWrongDateHeaderCheck: Option[(Box[User], Option[CallContext])] = { val date: Option[String] = headerMap.get(RequestHeader.Date.toLowerCase).flatMap(_.values.headOption) if (date.isDefined && !DateTimeUtil.isValidRfc7231Date(date.get)) { @@ -188,6 +240,7 @@ object BerlinGroupCheck extends MdcLoggable { // Chain validation steps resultWithMissingHeaderCheck .orElse(resultWithWrongDateHeaderCheck) + .orElse(resultWithWrongPsuGeoLocationHeaderCheck) .orElse(resultWithInvalidRequestIdCheck) .orElse(resultWithRequestIdUsedTwiceCheck) .orElse(resultWithInvalidSignatureHeaderCheck) 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 7e286003bc..db57ae4875 100644 --- a/obp-api/src/main/scala/code/api/util/BerlinGroupError.scala +++ b/obp-api/src/main/scala/code/api/util/BerlinGroupError.scala @@ -48,6 +48,9 @@ object BerlinGroupError { code match { // If this error occurs it implies that its error handling MUST be refined in OBP code case "400" if message.contains("OBP-50005") => "INTERNAL_ERROR" + case "400" if message.contains("InternalServerError") => "INTERNAL_ERROR" + case "404" if message.contains("InternalServerError") => "INTERNAL_ERROR" + case "500" if message.contains("InternalServerError") => "INTERNAL_ERROR" case "401" if message.contains("OBP-20001") => "PSU_CREDENTIALS_INVALID" case "401" if message.contains("OBP-20201") => "PSU_CREDENTIALS_INVALID" @@ -83,6 +86,7 @@ object BerlinGroupError { case "400" if message.contains("OBP-35018") => "CONSENT_UNKNOWN" case "400" if message.contains("OBP-35001") => "CONSENT_UNKNOWN" + case "401" if message.contains("OBP-35001") => "CONSENT_UNKNOWN" case "403" if message.contains("OBP-35001") => "CONSENT_UNKNOWN" case "400" if message.contains("OBP-50200") => "RESOURCE_UNKNOWN" @@ -103,12 +107,18 @@ object BerlinGroupError { case "400" if message.contains("OBP-20255") => "FORMAT_ERROR" case "400" if message.contains("OBP-20256") => "FORMAT_ERROR" case "400" if message.contains("OBP-20257") => "FORMAT_ERROR" + case "400" if message.contains("OBP-20258") => "FORMAT_ERROR" case "400" if message.contains("OBP-20251") => "FORMAT_ERROR" case "400" if message.contains("OBP-20088") => "FORMAT_ERROR" case "400" if message.contains("OBP-20089") => "FORMAT_ERROR" case "400" if message.contains("OBP-20090") => "FORMAT_ERROR" case "400" if message.contains("OBP-20091") => "FORMAT_ERROR" case "400" if message.contains("OBP-40008") => "FORMAT_ERROR" + case "400" if message.contains("OBP-90001") => "FORMAT_ERROR" + case "400" if message.contains("OBP-90003") => "BAD_REQUEST" + + case "404" if message.contains("OBP-10033") => "FORMAT_ERROR" + case "400" if message.contains("OBP-90002") => "PAYMENT_UNKNOWN" case "400" if message.contains("OBP-50221") => "PAYMENT_FAILED" 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 c2e907ee2b..8e4e8ea020 100644 --- a/obp-api/src/main/scala/code/api/util/ConsentUtil.scala +++ b/obp-api/src/main/scala/code/api/util/ConsentUtil.scala @@ -795,12 +795,15 @@ object Consent extends MdcLoggable { // Collect optional headers val headers = callContext.map(_.requestHeaders).getOrElse(Nil) - val tppRedirectUri = headers.find(_.name == RequestHeader.`TPP-Redirect-URI`) - val tppNokRedirectUri = headers.find(_.name == RequestHeader.`TPP-Nok-Redirect-URI`) - val xRequestId = headers.find(_.name == RequestHeader.`X-Request-ID`) - val psuDeviceId = headers.find(_.name == RequestHeader.`PSU-Device-ID`) - val psuIpAddress = headers.find(_.name == RequestHeader.`PSU-IP-Address`) - val psuGeoLocation = headers.find(_.name == RequestHeader.`PSU-Geo-Location`) + def findHeader(name: String) = + headers.find(h => h.name.equalsIgnoreCase(name)) + + val tppRedirectUri = findHeader(RequestHeader.`TPP-Redirect-URI`) + val tppNokRedirectUri = findHeader(RequestHeader.`TPP-Nok-Redirect-URI`) + val xRequestId = findHeader(RequestHeader.`X-Request-ID`) + val psuDeviceId = findHeader(RequestHeader.`PSU-Device-ID`) + val psuIpAddress = findHeader(RequestHeader.`PSU-IP-Address`) + val psuGeoLocation = findHeader(RequestHeader.`PSU-Geo-Location`) def sequenceBoxes[A](boxes: List[Box[A]]): Box[List[A]] = { boxes.foldRight(Full(Nil): Box[List[A]]) { (box, acc) => 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 756c5e5d4c..e0245461e3 100644 --- a/obp-api/src/main/scala/code/api/util/ErrorMessages.scala +++ b/obp-api/src/main/scala/code/api/util/ErrorMessages.scala @@ -280,7 +280,7 @@ object ErrorMessages { val InvalidRequestIdValueAlreadyUsed = "OBP-20255: Request Id value already used. " val InvalidConsentIdUsage = "OBP-20256: Consent-Id must not be used for this API Endpoint. " val NotValidRfc7231Date = "OBP-20257: Request header Date is not in accordance with RFC 7231 " - + val NotValidPsuGeoLocation = "OBP-20258: Request header PSU-Geo-Location is not in accordance with sample (GEO:47.046399;28.762064). " // X.509 val X509GeneralError = "OBP-20300: PEM Encoded Certificate issue." val X509ParsingFailed = "OBP-20301: Parsing failed for PEM Encoded Certificate." @@ -770,7 +770,12 @@ object ErrorMessages { // Cascade Deletion Exceptions (OBP-8XXXX) val CouldNotDeleteCascade = "OBP-80001: Could not delete cascade." - + + ///Validation Error + val ValidationError = "OBP-90001: Validation error." + val PaymentNotFoundById = "OBP-90002: Payment not found by id." + val MandatoryError = "OBP-90003: Validation error." + /////////// private val ObpErrorMsgPattern = Pattern.compile("OBP-\\d+:.+") @@ -846,6 +851,7 @@ object ErrorMessages { CounterpartyNotFoundByIban -> 404, BankAccountNotFound -> 404, ConsumerNotFoundByConsumerId -> 404, + PaymentNotFoundById -> 404, // TransactionNotFound -> 404, // or 400 BankAccountNotFoundByAccountRouting -> 404, BankAccountNotFoundByIban -> 404, diff --git a/obp-api/src/main/scala/code/api/util/ExampleValue.scala b/obp-api/src/main/scala/code/api/util/ExampleValue.scala index 0b516d6064..af7d9eb80d 100644 --- a/obp-api/src/main/scala/code/api/util/ExampleValue.scala +++ b/obp-api/src/main/scala/code/api/util/ExampleValue.scala @@ -95,7 +95,10 @@ object ExampleValue { lazy val consumerIdExample = ConnectorField("7uy8a7e4-6d02-40e3-a129-0b2bf89de8uh", s"A non human friendly string that identifies the consumer. It is the app which calls the apis") glossaryItems += makeGlossaryItem("Customer.consumerId", consumerIdExample) - + + lazy val consumerNameExample = ConnectorField("TPP MD", s"A non human friendly string that identifies name of the consumer. It is the app which calls the apis") + glossaryItems += makeGlossaryItem("Customer.consumerName", consumerNameExample) + lazy val nameSuffixExample = ConnectorField("Sr", s"suffix of the name") glossaryItems += makeGlossaryItem("Customer.nameSuffix", nameSuffixExample) diff --git a/obp-api/src/main/scala/code/api/util/NewStyle.scala b/obp-api/src/main/scala/code/api/util/NewStyle.scala index 8a297199c2..31e4a4d13b 100644 --- a/obp-api/src/main/scala/code/api/util/NewStyle.scala +++ b/obp-api/src/main/scala/code/api/util/NewStyle.scala @@ -416,13 +416,15 @@ object NewStyle extends MdcLoggable{ def getBankAccountsByIban(ibans : List[String], callContext: Option[CallContext]) : OBPReturnType[List[BankAccount]] = { Future.sequence(ibans.map( iban => Connector.connector.vend.getBankAccountByIban(iban : String, callContext: Option[CallContext]) map { i => - (unboxFullOrFail(i._1, callContext,s"$BankAccountNotFoundByIban Current IBAN is $iban", 404 ), i._2) + //(unboxFullOrFail(i._1, callContext,s"$BankAccountNotFoundByIban Current IBAN is $iban", 404 ), i._2) + (unboxFullOrFail(i._1, callContext, "", 404 ), i._2) } )).map(t => (t.map(_._1), callContext)) } def getBankAccountByIban(iban : String, callContext: Option[CallContext]) : OBPReturnType[BankAccount] = { Connector.connector.vend.getBankAccountByIban(iban : String, callContext: Option[CallContext]) map { i => - (unboxFullOrFail(i._1, callContext,s"$BankAccountNotFoundByIban Current IBAN is $iban", 404 ), i._2) + //(unboxFullOrFail(i._1, callContext,s"$BankAccountNotFoundByIban Current IBAN is $iban", 404 ), i._2) + (unboxFullOrFail(i._1, callContext,"", 404 ), i._2) } } def getToBankAccountByIban(iban : String, callContext: Option[CallContext]) : OBPReturnType[BankAccount] = { @@ -558,7 +560,9 @@ object NewStyle extends MdcLoggable{ secret: Option[String] = None, isActive: Option[Boolean] = None, name: Option[String] = None, + alias: Option[String] = None, appType: Option[AppType] = None, + company: Option[String] = None, description: Option[String] = None, developerEmail: Option[String] = None, redirectURL: Option[String] = None, @@ -566,7 +570,7 @@ object NewStyle extends MdcLoggable{ logoURL: Option[String] = None, certificate: Option[String] = None, callContext: Option[CallContext]): Future[Consumer] = { - Future(Consumers.consumers.vend.updateConsumer(id, key, secret, isActive, name, appType, description, developerEmail, redirectURL, createdByUserId, logoURL, certificate)) map { + Future(Consumers.consumers.vend.updateConsumer(id, key, secret, isActive, name, alias, appType, company, description, developerEmail, redirectURL, createdByUserId, logoURL, certificate)) map { unboxFullOrFail(_, callContext, UpdateConsumerError, 404) } } @@ -1076,6 +1080,88 @@ object NewStyle extends MdcLoggable{ (unboxFullOrFail(i._1, callContext, s"$InvalidConnectorResponseForCreateTransactionRequestBGV1", 400), i._2) } } + def createInstantTransactionRequestMdBGV1( + initiator: Option[User], + paymentServiceType: PaymentServiceTypes, + transactionRequestType: TransactionRequestTypes, + transactionRequestBody: InstantCreditTransfersMdV1, + callContext: Option[CallContext] + ): OBPReturnType[TransactionRequestBGV1] = { + val response = if(paymentServiceType.equals(PaymentServiceTypes.payments)){ + Connector.connector.vend.createTransactionRequestInstantCreditTransfersMdV1( + initiator: Option[User], + paymentServiceType: PaymentServiceTypes, + transactionRequestType: TransactionRequestTypes, + transactionRequestBody.asInstanceOf[InstantCreditTransfersMdV1], + callContext: Option[CallContext] + ) + }else Future(throw new RuntimeException(checkPaymentServerTypeError(paymentServiceType.toString))) + + response map { i => + (unboxFullOrFail(i._1, callContext, "", 400), i._2) + } + } + + def createDomesticTransactionRequestMdBGV1( + initiator: Option[User], + paymentServiceType: PaymentServiceTypes, + transactionRequestType: TransactionRequestTypes, + transactionRequestBody: DomesticCreditTransfersMdV1, + callContext: Option[CallContext] + ): OBPReturnType[TransactionRequestBGV1] = { + val response = if(paymentServiceType.equals(PaymentServiceTypes.payments)){ + Connector.connector.vend.createTransactionRequestDomesticCreditTransfersMdV1( + initiator: Option[User], + paymentServiceType: PaymentServiceTypes, + transactionRequestType: TransactionRequestTypes, + transactionRequestBody.asInstanceOf[DomesticCreditTransfersMdV1], + callContext: Option[CallContext] + ) + }else Future(throw new RuntimeException(checkPaymentServerTypeError(paymentServiceType.toString))) + + response map { i => + (unboxFullOrFail(i._1, callContext, "", 400), i._2) + } + } + + def getInstantPaymentInformationMdBGV1( + paymentId: String, + callContext: Option[CallContext] + ): OBPReturnType[InstantPaymentInformation] = { + + val response = Connector.connector.vend.getInstantPaymentInformationMdV1( + paymentId = paymentId, + callContext = callContext + ) + response map { i => + val instantPaymentInformation = unboxFullOrFail(i._1, callContext, "", 400) + val normalized = instantPaymentInformation.copy( + debtorAccount = Some(instantPaymentInformation.debtorAccount.getOrElse(PaymentAccount(null))) + ) + (normalized, i._2) + } + } + + def getDomesticPaymentInformationMdBGV1( + paymentId: String, + callContext: Option[CallContext] + ): OBPReturnType[DomesticPaymentInformationResponse] = { + + val response = Connector.connector.vend.getDomesticPaymentInformationMdV1( + paymentId = paymentId, + callContext = callContext + ) + + response map { i => + val domesticPaymentInformation = unboxFullOrFail(i._1, callContext, "", 400) + val normalized = domesticPaymentInformation.copy( + debtorAccount = Some(domesticPaymentInformation.debtorAccount.getOrElse(PaymentAccount(null))) + ) + (normalized, i._2) + } + } + + def notifyTransactionRequest(fromAccount: BankAccount, toAccount: BankAccount, transactionRequest: TransactionRequest, callContext: Option[CallContext]): OBPReturnType[TransactionRequestStatusValue] = { Connector.connector.vend.notifyTransactionRequest(fromAccount: BankAccount, toAccount: BankAccount, transactionRequest: TransactionRequest, callContext: Option[CallContext]) map { i => diff --git a/obp-api/src/main/scala/code/api/util/migration/Migration.scala b/obp-api/src/main/scala/code/api/util/migration/Migration.scala index 166a543423..9f02413ab8 100644 --- a/obp-api/src/main/scala/code/api/util/migration/Migration.scala +++ b/obp-api/src/main/scala/code/api/util/migration/Migration.scala @@ -66,6 +66,9 @@ object Migration extends MdcLoggable { populateTableRateLimiting() updateTableViewDefinition() bankAccountHoldersAndOwnerViewAccessInfo(startedBeforeSchemifier) + alterTableMappedPayment() + alterColumnRemittanceInformationUnstructuredAtTableMappedPayment() + alterColumnPurposeTypeAtTableMappedPayment() alterTableMappedConsent() alterColumnChallengeAtTableMappedConsent() alterTableOpenIDConnectToken() @@ -225,18 +228,42 @@ object Migration extends MdcLoggable { } } } + private def alterTableMappedConsent(): Boolean = { val name = nameOf(alterTableMappedConsent) runOnce(name) { MigrationOfMappedConsent.alterColumnJsonWebToken(name) } } + + private def alterTableMappedPayment(): Boolean = { + val name = nameOf(alterTableMappedPayment) + runOnce(name) { + MigrationOfMappedPayment.alterColumnPaymentId(name) + } + } + + private def alterColumnRemittanceInformationUnstructuredAtTableMappedPayment(): Boolean = { + val name = nameOf(alterColumnRemittanceInformationUnstructuredAtTableMappedPayment) + runOnce(name) { + MigrationOfMappedPayment.alterColumnRemittanceInformationUnstructured(name) + } + } + + private def alterColumnPurposeTypeAtTableMappedPayment(): Boolean = { + val name = nameOf(alterColumnPurposeTypeAtTableMappedPayment) + runOnce(name) { + MigrationOfMappedPayment.alterColumnPurposeType(name) + } + } + private def alterColumnChallengeAtTableMappedConsent(): Boolean = { val name = nameOf(alterColumnChallengeAtTableMappedConsent) runOnce(name) { MigrationOfMappedConsent.alterColumnChallenge(name) } } + private def alterTableOpenIDConnectToken(): Boolean = { val name = nameOf(alterTableOpenIDConnectToken) runOnce(name) { diff --git a/obp-api/src/main/scala/code/api/util/migration/MigrationOfMappedConsent.scala b/obp-api/src/main/scala/code/api/util/migration/MigrationOfMappedConsent.scala index 412b279320..1642e8654b 100644 --- a/obp-api/src/main/scala/code/api/util/migration/MigrationOfMappedConsent.scala +++ b/obp-api/src/main/scala/code/api/util/migration/MigrationOfMappedConsent.scala @@ -102,6 +102,7 @@ object MigrationOfMappedConsent { isSuccessful } } + def alterColumnStatus(name: String): Boolean = { DbFunction.tableExists(MappedConsent) match { case true => diff --git a/obp-api/src/main/scala/code/api/util/migration/MigrationOfMappedPayment.scala b/obp-api/src/main/scala/code/api/util/migration/MigrationOfMappedPayment.scala new file mode 100644 index 0000000000..8a9e7c4795 --- /dev/null +++ b/obp-api/src/main/scala/code/api/util/migration/MigrationOfMappedPayment.scala @@ -0,0 +1,139 @@ +package code.api.util.migration + +import java.time.format.DateTimeFormatter +import java.time.{ZoneId, ZonedDateTime} + +import code.api.util.APIUtil +import code.api.util.migration.Migration.{DbFunction, saveLog} +import code.payments.MappedPayment +import net.liftweb.common.Full +import net.liftweb.mapper.{DB, Schemifier} +import net.liftweb.util.DefaultConnectionIdentifier + +object MigrationOfMappedPayment { + + val oneDayAgo = ZonedDateTime.now(ZoneId.of("UTC")).minusDays(1) + val oneYearInFuture = ZonedDateTime.now(ZoneId.of("UTC")).plusYears(1) + val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm'Z'") + + def alterColumnPaymentId(name: String): Boolean = { + DbFunction.tableExists(MappedPayment) match { + case true => + val startDate = System.currentTimeMillis() + val commitId: String = APIUtil.gitCommit + var isSuccessful = false + + val executedSql = + DbFunction.maybeWrite(true, Schemifier.infoF _) { + APIUtil.getPropsValue("db.driver") match { + case Full(dbDriver) if dbDriver.contains("com.microsoft.sqlserver.jdbc.SQLServerDriver") => + () => + """ALTER TABLE mappedpayment ALTER COLUMN mpaymentid varchar(40);""" + case _ => + () => + """ALTER TABLE mappedpayment ALTER COLUMN mpaymentid type varchar(40);""" + } + } + + val endDate = System.currentTimeMillis() + val comment: String = + s"""Executed SQL: + |$executedSql + |""".stripMargin + isSuccessful = true + saveLog(name, commitId, isSuccessful, startDate, endDate, comment) + isSuccessful + + case false => + val startDate = System.currentTimeMillis() + val commitId: String = APIUtil.gitCommit + val isSuccessful = false + val endDate = System.currentTimeMillis() + val comment: String = + s"""${MappedPayment._dbTableNameLC} table does not exist""".stripMargin + saveLog(name, commitId, isSuccessful, startDate, endDate, comment) + isSuccessful + } + } + + def alterColumnPurposeType(name: String): Boolean = { + DbFunction.tableExists(MappedPayment) match { + case true => + val startDate = System.currentTimeMillis() + val commitId: String = APIUtil.gitCommit + var isSuccessful = false + + val executedSql = + DbFunction.maybeWrite(true, Schemifier.infoF _) { + APIUtil.getPropsValue("db.driver") match { + case Full(dbDriver) if dbDriver.contains("com.microsoft.sqlserver.jdbc.SQLServerDriver") => + () => + """ALTER TABLE mappedpayment ALTER COLUMN mpurposetype varchar(50);""" + case _ => + () => + """ALTER TABLE mappedpayment ALTER COLUMN mpurposetype type varchar(50);""" + } + } + + val endDate = System.currentTimeMillis() + val comment: String = + s"""Executed SQL: + |$executedSql + |""".stripMargin + isSuccessful = true + saveLog(name, commitId, isSuccessful, startDate, endDate, comment) + isSuccessful + + case false => + val startDate = System.currentTimeMillis() + val commitId: String = APIUtil.gitCommit + val isSuccessful = false + val endDate = System.currentTimeMillis() + val comment: String = + s"""${MappedPayment._dbTableNameLC} table does not exist""".stripMargin + saveLog(name, commitId, isSuccessful, startDate, endDate, comment) + isSuccessful + } + } + + def alterColumnRemittanceInformationUnstructured(name: String): Boolean = { + DbFunction.tableExists(MappedPayment) match { + case true => + val startDate = System.currentTimeMillis() + val commitId: String = APIUtil.gitCommit + var isSuccessful = false + + val executedSql = + DbFunction.maybeWrite(true, Schemifier.infoF _) { + APIUtil.getPropsValue("db.driver") match { + case Full(dbDriver) if dbDriver.contains("com.microsoft.sqlserver.jdbc.SQLServerDriver") => + () => + """ALTER TABLE mappedpayment ALTER COLUMN mremittanceinformationunstructured varchar(200);""" + case _ => + () => + """ALTER TABLE mappedpayment ALTER COLUMN mremittanceinformationunstructured type varchar(200);""" + } + } + + val endDate = System.currentTimeMillis() + val comment: String = + s"""Executed SQL: + |$executedSql + |""".stripMargin + isSuccessful = true + saveLog(name, commitId, isSuccessful, startDate, endDate, comment) + isSuccessful + + case false => + val startDate = System.currentTimeMillis() + val commitId: String = APIUtil.gitCommit + val isSuccessful = false + val endDate = System.currentTimeMillis() + val comment: String = + s"""${MappedPayment._dbTableNameLC} table does not exist""".stripMargin + saveLog(name, commitId, isSuccessful, startDate, endDate, comment) + isSuccessful + } + } + +} diff --git a/obp-api/src/main/scala/code/api/v2_1_0/APIMethods210.scala b/obp-api/src/main/scala/code/api/v2_1_0/APIMethods210.scala index 5280a92673..6455cf8a51 100644 --- a/obp-api/src/main/scala/code/api/v2_1_0/APIMethods210.scala +++ b/obp-api/src/main/scala/code/api/v2_1_0/APIMethods210.scala @@ -953,7 +953,7 @@ trait APIMethods210 { case false => NewStyle.function.ownEntitlement("", u.userId, ApiRole.canDisableConsumers, cc.callContext) } consumer <- Consumers.consumers.vend.getConsumerByPrimaryId(consumerId.toLong) - updatedConsumer <- Consumers.consumers.vend.updateConsumer(consumer.id.get, None, None, Some(putData.enabled), None, None, None, None, None, None, None, None) ?~! "Cannot update Consumer" + updatedConsumer <- Consumers.consumers.vend.updateConsumer(consumer.id.get, None, None, Some(putData.enabled), None, None, None, None, None, None, None, None, None, None) ?~! "Cannot update Consumer" } yield { // Format the data as json val json = PutEnabledJSON(updatedConsumer.isActive.get) diff --git a/obp-api/src/main/scala/code/api/v2_1_0/JSONFactory2.1.0.scala b/obp-api/src/main/scala/code/api/v2_1_0/JSONFactory2.1.0.scala index 0b271e3280..49f869791b 100644 --- a/obp-api/src/main/scala/code/api/v2_1_0/JSONFactory2.1.0.scala +++ b/obp-api/src/main/scala/code/api/v2_1_0/JSONFactory2.1.0.scala @@ -877,7 +877,7 @@ object JSONFactory210{ def createUserJSON(user : Box[User], entitlements: Box[List[Entitlement]]) : UserJsonV200 = { (user, entitlements) match { - case (Full(u), Full(е)) => createUserJSON(u, е) + case (Full(u), Full(e)) => createUserJSON(u, e) case _ => null } } 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 283cb12640..d4c492e48a 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 @@ -5992,7 +5992,7 @@ trait APIMethods310 { } consumer <- NewStyle.function.getConsumerByConsumerId(consumerId, callContext) updatedConsumer <- Future { - Consumers.consumers.vend.updateConsumer(consumer.id.get, None, None, Some(putData.enabled), None, None, None, None, None,None, None, None) ?~! "Cannot update Consumer" + Consumers.consumers.vend.updateConsumer(consumer.id.get, None, None, Some(putData.enabled), None, None, None, None, None, None, None,None, None, None) ?~! "Cannot update Consumer" } } yield { // Format the data as json 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 948c370d99..6e5e1de1e7 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 @@ -82,8 +82,6 @@ trait APIMethods510 { val apiRelations = ArrayBuffer[ApiRelation]() val codeContext = CodeContext(staticResourceDocs, apiRelations) - - staticResourceDocs += ResourceDoc( root, implementedInApiVersion, @@ -3441,6 +3439,7 @@ trait APIMethods510 { } } } + staticResourceDocs += ResourceDoc( updateConsumerCertificate, implementedInApiVersion, @@ -3536,6 +3535,73 @@ trait APIMethods510 { } } + val updateConsumerJsonExample = UpdateConsumerJson( + is_active = Some(true), + name = Some("My App Name"), + alias = Some("My Short App Name"), + description = Some("My App Description"), + redirect_url = Some("https://example.com/callback"), + logo_url = Some("https://example.com/logo.png"), + company = Some("company name") + ) + + staticResourceDocs += ResourceDoc( + updateConsumer, + implementedInApiVersion, + nameOf(updateConsumer), + "PUT", + "/management/consumers/CONSUMER_ID", + "Update Consumer", + s"""Update existing Consumer fields (is_active, name, alias, description, redirect_url, logo_url) + |for a Consumer specified by CONSUMER_ID. + | + |${consumerDisabledText()} + | + |CONSUMER_ID can be obtained after you register the application, + |or use the endpoint 'Get Consumers' to get it. + |""".stripMargin, + updateConsumerJsonExample, + consumerJsonV510, + List( + $UserNotLoggedIn, + UserHasMissingRoles, + UnknownError + ), + List(apiTagConsumer), + Some(List(canCreateConsumer)) + ) + + lazy val updateConsumer: OBPEndpoint = { + case "management" :: "consumers" :: consumerId :: Nil JsonPut json -> _ => { + cc => + implicit val ec = EndpointContext(Some(cc)) + for { + (Full(u), callContext) <- authenticatedAccess(cc) + + postJson <- NewStyle.function.tryons(InvalidJsonFormat, 400, callContext) { + json.extract[UpdateConsumerJson] + } + + consumer <- NewStyle.function.getConsumerByConsumerId(consumerId, callContext) + + updatedConsumer <- NewStyle.function.updateConsumer( + id = consumer.id.get, + isActive = postJson.is_active, + name = postJson.name, + alias = postJson.alias, + description = postJson.description, + redirectURL = postJson.redirect_url, + logoURL = postJson.logo_url, + company = postJson.company, + callContext = callContext + ) + } yield { + (JSONFactory510.createConsumerJSON(updatedConsumer), HttpCode.`200`(callContext)) + } + } + } + + staticResourceDocs += ResourceDoc( getConsumer, diff --git a/obp-api/src/main/scala/code/api/v5_1_0/JSONFactory5.1.0.scala b/obp-api/src/main/scala/code/api/v5_1_0/JSONFactory5.1.0.scala index 1c64a09e51..5f4c23374c 100644 --- a/obp-api/src/main/scala/code/api/v5_1_0/JSONFactory5.1.0.scala +++ b/obp-api/src/main/scala/code/api/v5_1_0/JSONFactory5.1.0.scala @@ -65,6 +65,16 @@ import scala.util.Try case class WellKnownUrisJsonV510(well_known_uris: List[WellKnownUriJsonV510]) case class WellKnownUriJsonV510(provider: String, url: String) +case class UpdateConsumerJson( + is_active: Option[Boolean], + name: Option[String], + alias: Option[String], + description: Option[String], + redirect_url: Option[String], + logo_url: Option[String], + company: Option[String], + ) + case class RegulatedEntityAttributeRequestJsonV510( name: String, attribute_type: String, @@ -460,6 +470,7 @@ case class ConsumerPostJsonV510(app_name: Option[String], case class ConsumerJsonV510(consumer_id: String, consumer_key: String, app_name: String, + app_alias: String, app_type: String, description: String, developer_email: String, @@ -470,12 +481,14 @@ case class ConsumerJsonV510(consumer_id: String, created_by_user: ResourceUserJSON, enabled: Boolean, created: Date, + updated: Date, logo_url: Option[String] ) case class MyConsumerJsonV510(consumer_id: String, consumer_key: String, consumer_secret: String, app_name: String, + app_alias: String, app_type: String, description: String, developer_email: String, @@ -1175,6 +1188,7 @@ object JSONFactory510 extends CustomJsonFormats with MdcLoggable { consumer_id = c.consumerId.get, consumer_key = c.key.get, app_name = c.name.get, + app_alias = c.alias.get, app_type = c.appType.toString(), description = c.description.get, developer_email = c.developerEmail.get, @@ -1185,6 +1199,7 @@ object JSONFactory510 extends CustomJsonFormats with MdcLoggable { created_by_user = resourceUserJSON, enabled = c.isActive.get, created = c.createdAt.get, + updated = c.updatedAt.get, logo_url = if (c.logoUrl.get == null || c.logoUrl.get.isEmpty ) null else Some(c.logoUrl.get) ) } @@ -1206,6 +1221,7 @@ object JSONFactory510 extends CustomJsonFormats with MdcLoggable { consumer_key = c.key.get, consumer_secret = c.secret.get, app_name = c.name.get, + app_alias = c.alias.get, app_type = c.appType.toString(), description = c.description.get, developer_email = c.developerEmail.get, diff --git a/obp-api/src/main/scala/code/bankconnectors/Connector.scala b/obp-api/src/main/scala/code/bankconnectors/Connector.scala index 131e8ed6eb..fdb097dd77 100644 --- a/obp-api/src/main/scala/code/bankconnectors/Connector.scala +++ b/obp-api/src/main/scala/code/bankconnectors/Connector.scala @@ -272,6 +272,28 @@ trait Connector extends MdcLoggable { (boxedResult, callContext) } + protected def convertToTupleFirstError[T](callContext: Option[CallContext])(inbound: Box[InBoundTrait[T]]): (Box[T], Option[CallContext]) = { + val boxedResult = inbound match { + case Full(in) if (in.status.hasNoError) => Full(in.data) + case Full(inbound) if (inbound.status.hasError) => { + val errorCode: Int = try { + inbound.status.errorCode.toInt + } catch { + case _: Throwable => 400 + } + val firstErrorMessage = inbound.status.backendMessages.headOption.map(_.text).getOrElse("No error message available") + + val errorMessage = inbound.status.backendMessages.mkString(", ") + val simplifiedErrorMessage = s"$errorCode: $errorMessage" + + ParamFailure(firstErrorMessage, Empty, Empty, APIFailure(firstErrorMessage, errorCode)) + } + case failureOrEmpty: Failure => failureOrEmpty + } + + (boxedResult, callContext) + } + private def setUnimplementedError(methodName:String) : String = { NotImplemented + methodName + s" Please check `Get Message Docs`endpoint and implement the process `obp.$methodName` in Adapter side." @@ -775,7 +797,6 @@ trait Connector extends MdcLoggable { reasons: Option[List[TransactionRequestReason]], callContext: Option[CallContext]): OBPReturnType[Box[TransactionRequest]] = Future{(Failure(setUnimplementedError(nameOf(createTransactionRequestv400 _))), callContext)} - def createTransactionRequestSepaCreditTransfersBGV1( initiator: Option[User], paymentServiceType: PaymentServiceTypes, @@ -784,6 +805,34 @@ trait Connector extends MdcLoggable { callContext: Option[CallContext] ): OBPReturnType[Box[TransactionRequestBGV1]] = Future{(Failure(setUnimplementedError(nameOf(createTransactionRequestSepaCreditTransfersBGV1 _))), callContext)} + def createTransactionRequestInstantCreditTransfersMdV1( + initiator: Option[User], + paymentServiceType: PaymentServiceTypes, + transactionRequestType: TransactionRequestTypes, + transactionRequestBody: InstantCreditTransfersMdV1, + callContext: Option[CallContext] + ): OBPReturnType[Box[TransactionRequestBGV1]] = Future{(Failure(setUnimplementedError(nameOf(createTransactionRequestInstantCreditTransfersMdV1 _))), callContext)} + + def createTransactionRequestDomesticCreditTransfersMdV1( + initiator: Option[User], + paymentServiceType: PaymentServiceTypes, + transactionRequestType: TransactionRequestTypes, + transactionRequestBody: DomesticCreditTransfersMdV1, + callContext: Option[CallContext] + ): OBPReturnType[Box[TransactionRequestBGV1]] = Future{(Failure(setUnimplementedError(nameOf(createTransactionRequestDomesticCreditTransfersMdV1 _))), callContext)} + + + def getInstantPaymentInformationMdV1( + paymentId: String, + callContext: Option[CallContext] + ): OBPReturnType[Box[InstantPaymentInformation]] = Future{(Failure(setUnimplementedError(nameOf(getInstantPaymentInformationMdV1 _))), callContext)} + + def getDomesticPaymentInformationMdV1( + paymentId: String, + callContext: Option[CallContext] + ): OBPReturnType[Box[DomesticPaymentInformationResponse]] = Future{(Failure(setUnimplementedError(nameOf(getDomesticPaymentInformationMdV1 _))), callContext)} + + def createTransactionRequestPeriodicSepaCreditTransfersBGV1( initiator: Option[User], paymentServiceType: PaymentServiceTypes, diff --git a/obp-api/src/main/scala/code/bankconnectors/LocalMappedConnector.scala b/obp-api/src/main/scala/code/bankconnectors/LocalMappedConnector.scala index 7784852dd9..534ea6cee9 100644 --- a/obp-api/src/main/scala/code/bankconnectors/LocalMappedConnector.scala +++ b/obp-api/src/main/scala/code/bankconnectors/LocalMappedConnector.scala @@ -42,6 +42,7 @@ import code.metadata.counterparties.Counterparties import code.model._ import code.model.dataAccess.AuthUser.findAuthUserByUsernameLocallyLegacy import code.model.dataAccess._ +import code.payments.MappedPaymentProvider import code.productAttributeattribute.MappedProductAttribute import code.productattribute.ProductAttributeX import code.productcollection.ProductCollectionX @@ -75,6 +76,7 @@ import com.twilio.Twilio import com.twilio.`type`.PhoneNumber import com.twilio.rest.api.v2010.account.Message import net.liftweb.common._ +import net.liftweb.http.S import net.liftweb.json import net.liftweb.json.{JArray, JBool, JObject, JValue} import net.liftweb.mapper._ @@ -4849,6 +4851,82 @@ object LocalMappedConnector extends Connector with MdcLoggable { ) } + override def createTransactionRequestInstantCreditTransfersMdV1( + initiator: Option[User], + paymentServiceType: PaymentServiceTypes, + transactionRequestType: TransactionRequestTypes, + transactionRequestBody: InstantCreditTransfersMdV1, + callContext: Option[CallContext] + ): OBPReturnType[Box[TransactionRequestBGV1]] = { + LocalMappedConnectorInternal.createTransactionRequestInstantCreditTransferMd( + initiator: Option[User], + paymentServiceType: PaymentServiceTypes, + transactionRequestType: TransactionRequestTypes, + transactionRequestBody: InstantCreditTransfersMdV1, + callContext: Option[CallContext] + ) + } + + override def createTransactionRequestDomesticCreditTransfersMdV1( + initiator: Option[User], + paymentServiceType: PaymentServiceTypes, + transactionRequestType: TransactionRequestTypes, + transactionRequestBody: DomesticCreditTransfersMdV1, + callContext: Option[CallContext] + ): OBPReturnType[Box[TransactionRequestBGV1]] = { + LocalMappedConnectorInternal.createTransactionRequestDomesticCreditTransferMd( + initiator: Option[User], + paymentServiceType: PaymentServiceTypes, + transactionRequestType: TransactionRequestTypes, + transactionRequestBody: DomesticCreditTransfersMdV1, + callContext: Option[CallContext] + ) + } + + override def getInstantPaymentInformationMdV1(paymentId: String, callContext: Option[CallContext]): OBPReturnType[Box[InstantPaymentInformation]] = { + val notFound: Failure = + Failure(ErrorMessages.PaymentNotFoundById) + + MappedPaymentProvider.getPaymentByIdAndType(paymentId, TransactionRequestTypes.INSTANT_CREDIT_TRANSFERS_MD.toString()) match { + case Full(p) => + val info = InstantPaymentInformation( + paymentId = Option(p.mPaymentId.get).getOrElse(paymentId), + instructedAmount = AmountOfMoneyJsonV121(currency = p.mInstructedAmountCurrency.get, amount = p.mInstructedAmountAmount.get), + debtorAccount = Option(p.mDebtorAccountIban.get).filter(_ != null).map(PaymentAccount.apply), + creditorAccount = PaymentAccountMd(p.mCreditorAccountMsisdn.get), + remittanceInformationUnstructured = Option(p.mRemittanceInformationUnstructured.get).filter(s => s != null && s.nonEmpty), + transactionStatus = p.status + ) + Future.successful((Full(info), callContext)) + + case Empty => + Future.successful((notFound, callContext)) + } + } + + override def getDomesticPaymentInformationMdV1(paymentId: String, callContext: Option[CallContext]): OBPReturnType[Box[DomesticPaymentInformationResponse]] = { + val notFound: Failure = + Failure(ErrorMessages.PaymentNotFoundById) + + MappedPaymentProvider.getPaymentByIdAndType(paymentId, TransactionRequestTypes.DOMESTIC_CREDIT_TRANSFERS_MD.toString()) match { + case Full(p) => + val info = DomesticPaymentInformationResponse( + paymentId = Option(p.mPaymentId.get).getOrElse(paymentId), + instructedAmount = AmountOfMoneyJsonV121(currency = p.mInstructedAmountCurrency.get, amount = p.mInstructedAmountAmount.get), + debtorAccount = Option(p.mDebtorAccountIban.get).filter(_ != null).map(PaymentAccount.apply), + creditorAccount = PaymentAccount(p.mCreditorAccountIban.get), + remittanceInformationUnstructured = Option(p.mRemittanceInformationUnstructured.get).filter(s => s != null && s.nonEmpty), + transactionStatus = p.status, + creditorName = Option(p.mCreditorName.get).getOrElse(""), + transactionFees= AmountOfMoneyJsonV121(currency = "MDL", amount = "0"), + ) + Future.successful((Full(info), callContext)) + + case Empty => + Future.successful((notFound, callContext)) + } + } + override def createTransactionRequestPeriodicSepaCreditTransfersBGV1( initiator: Option[User], paymentServiceType: PaymentServiceTypes, diff --git a/obp-api/src/main/scala/code/bankconnectors/LocalMappedConnectorInternal.scala b/obp-api/src/main/scala/code/bankconnectors/LocalMappedConnectorInternal.scala index 0f1af06fcd..7af11035ee 100644 --- a/obp-api/src/main/scala/code/bankconnectors/LocalMappedConnectorInternal.scala +++ b/obp-api/src/main/scala/code/bankconnectors/LocalMappedConnectorInternal.scala @@ -1,8 +1,9 @@ package code.bankconnectors -import code.api.ChargePolicy +import code.api.{ChargePolicy, RequestHeader} import code.api.Constant._ import code.api.berlin.group.ConstantsBG +import code.api.berlin.group.v1_3.model.TransactionStatus import code.api.berlin.group.v1_3.model.TransactionStatus.mapTransactionStatus import code.api.cache.Caching import code.api.util.APIUtil._ @@ -20,6 +21,7 @@ import code.fx.fx.TTL import code.management.ImporterAPI.ImporterTransaction import code.model.dataAccess.{BankAccountRouting, MappedBank, MappedBankAccount} import code.model.toBankAccountExtended +import code.payments.MappedPayment import code.transaction.MappedTransaction import code.transactionrequests._ import code.util.Helper @@ -41,7 +43,7 @@ import net.liftweb.util.StringHelpers import java.time.{LocalDate, ZoneId} import java.util.UUID.randomUUID -import java.util.{Calendar, Date} +import java.util.{Calendar, Date, UUID} import scala.collection.immutable.{List, Nil} import scala.concurrent.Future import scala.concurrent.duration.DurationInt @@ -227,7 +229,150 @@ object LocalMappedConnectorInternal extends MdcLoggable { } } + def createTransactionRequestInstantCreditTransferMd( + initiator: Option[User], + paymentServiceType: PaymentServiceTypes, + transactionRequestType: TransactionRequestTypes, + transactionRequestBody: InstantCreditTransfersMdV1, + callContext: Option[CallContext] + ): Future[(Full[TransactionRequestBGV1], Option[CallContext])] = { + val headers = callContext.map(_.requestHeaders).getOrElse(Nil) + def findHeader(name: String) = + headers.find(h => h.name.equalsIgnoreCase(name)) + + val tppRedirectUri = findHeader(RequestHeader.`TPP-Redirect-URI`) + val tppNokRedirectUri = findHeader(RequestHeader.`TPP-Nok-Redirect-URI`) + + for { + transDetailsSerialized <- NewStyle.function.tryons( + s"$UnknownError Can not serialize in request Json", + 400, + callContext + ) { + write(transactionRequestBody)(Serialization.formats(NoTypeHints)) + } + transactionRequest <- { + val fromAccountIbanOpt: Option[String] = + for { + acc <- transactionRequestBody.debtorAccount + iban <- Option(acc.iban).map(_.trim.take(50)).filter(_.nonEmpty) + } yield iban + + Future { + val payment = MappedPayment.create + .mEndToEndIdentification(transactionRequestBody.endToEndIdentification.getOrElse("")) + .mInstructedAmountCurrency(transactionRequestBody.instructedAmount.currency) + .mInstructedAmountAmount(transactionRequestBody.instructedAmount.amount) + + fromAccountIbanOpt.foreach(payment.mDebtorAccountIban(_)) + + val savedPayment = payment + .mCreditorAccountMsisdn(transactionRequestBody.creditorAccount.msisdn) + .mPurposeCode(transactionRequestBody.purposeCode.getOrElse("")) + .mRemittanceInformationUnstructured( + transactionRequestBody.remittanceInformationUnstructured.getOrElse("") + ) + .mStatus(TransactionStatus.RCVD.toString) + .mType(transactionRequestType.toString) + .mPaymentId(randomUUID().toString) + .mTppRedirectUri(tppRedirectUri.flatMap(_.values.headOption).getOrElse("")) + .mTppNokRedirectUri(tppNokRedirectUri.flatMap(_.values.headOption).getOrElse("")) + .saveMe() + + Full(savedPayment) + }.map { box => + unboxFullOrFail(box, callContext, s"$InvalidConnectorResponseForCreateTransactionRequestImpl210 ", 400) + } + } + } yield { + logger.debug(transactionRequest) + ( + Full( + TransactionRequestBGV1( + TransactionRequestId(transactionRequest.mPaymentId.toString), + transactionRequest.status + ) + ), + callContext + ) + } + } + + def createTransactionRequestDomesticCreditTransferMd( + initiator: Option[User], + paymentServiceType: PaymentServiceTypes, + transactionRequestType: TransactionRequestTypes, + transactionRequestBody: DomesticCreditTransfersMdV1, + callContext: Option[CallContext] + ): Future[(Full[TransactionRequestBGV1], Option[CallContext])] = { + + val headers = callContext.map(_.requestHeaders).getOrElse(Nil) + + def findHeader(name: String) = + headers.find(h => h.name.equalsIgnoreCase(name)) + + val tppRedirectUri = findHeader(RequestHeader.`TPP-Redirect-URI`) + val tppNokRedirectUri = findHeader(RequestHeader.`TPP-Nok-Redirect-URI`) + + for { + transDetailsSerialized <- NewStyle.function.tryons( + s"$UnknownError Can not serialize in request Json", + 400, + callContext + ) { + write(transactionRequestBody)(Serialization.formats(NoTypeHints)) + } + + transactionRequest <- { + val fromAccountIbanOpt: Option[String] = + for { + acc <- transactionRequestBody.debtorAccount + iban <- Option(acc.iban).map(_.trim.take(50)).filter(_.nonEmpty) + } yield iban + + Future { + val payment = MappedPayment.create + .mEndToEndIdentification(transactionRequestBody.endToEndIdentification.getOrElse("")) + .mInstructedAmountCurrency(transactionRequestBody.instructedAmount.currency) + .mInstructedAmountAmount(transactionRequestBody.instructedAmount.amount) + + fromAccountIbanOpt.foreach(payment.mDebtorAccountIban(_)) + + val savedPayment = payment + .mCreditorAccountIban(transactionRequestBody.creditorAccount.iban) + .mCreditorName(transactionRequestBody.creditorName) + .mCreditorId(transactionRequestBody.creditorId) + .mCreditorCtryOfRes(transactionRequestBody.creditorCtryOfRes) + .mInstructionPriority(transactionRequestBody.instructionPriority) + .mRemittanceInformationUnstructured( + transactionRequestBody.remittanceInformationUnstructured.getOrElse("") + ) + .mStatus(TransactionStatus.RCVD.toString) + .mType(transactionRequestType.toString) + .mPaymentId(randomUUID().toString) + .mTppRedirectUri(tppRedirectUri.flatMap(_.values.headOption).getOrElse("")) + .mTppNokRedirectUri(tppNokRedirectUri.flatMap(_.values.headOption).getOrElse("")) + .saveMe() + + Full(savedPayment) + }.map { box => + unboxFullOrFail(box, callContext, s"$InvalidConnectorResponseForCreateTransactionRequestImpl210 ", 400) + } + } + } yield { + logger.debug(transactionRequest) + ( + Full( + TransactionRequestBGV1( + TransactionRequestId(transactionRequest.mPaymentId.toString), + transactionRequest.status + ) + ), + callContext + ) + } + } /* Bank account creation diff --git a/obp-api/src/main/scala/code/bankconnectors/generator/ConnectorBuilderUtil.scala b/obp-api/src/main/scala/code/bankconnectors/generator/ConnectorBuilderUtil.scala index 95d94df174..fa49a9e780 100644 --- a/obp-api/src/main/scala/code/bankconnectors/generator/ConnectorBuilderUtil.scala +++ b/obp-api/src/main/scala/code/bankconnectors/generator/ConnectorBuilderUtil.scala @@ -396,6 +396,9 @@ object ConnectorBuilderUtil { "createChallenges", "createTransactionRequestv400", + "getInstantPaymentInformationMdV1", + "getDomesticPaymentInformationMdV1", + "createTransactionRequestInstantCreditTransfersMdV1", "createTransactionRequestSepaCreditTransfersBGV1", "createTransactionRequestPeriodicSepaCreditTransfersBGV1", "getCustomersByCustomerPhoneNumber", diff --git a/obp-api/src/main/scala/code/bankconnectors/rabbitmq/RabbitMQConnector_vOct2024.scala b/obp-api/src/main/scala/code/bankconnectors/rabbitmq/RabbitMQConnector_vOct2024.scala index c644945b78..86bd71cda5 100644 --- a/obp-api/src/main/scala/code/bankconnectors/rabbitmq/RabbitMQConnector_vOct2024.scala +++ b/obp-api/src/main/scala/code/bankconnectors/rabbitmq/RabbitMQConnector_vOct2024.scala @@ -37,6 +37,7 @@ import com.openbankproject.commons.model.enums.StrongCustomerAuthenticationStatu import com.openbankproject.commons.model.enums._ import com.openbankproject.commons.model.{Meta, _} import net.liftweb.common._ +import net.liftweb.http.provider.HTTPParam import net.liftweb.json._ import net.liftweb.util.StringHelpers @@ -922,7 +923,7 @@ trait RabbitMQConnector_vOct2024 extends Connector with MdcLoggable { import com.openbankproject.commons.dto.{InBoundGetBankAccountByIban => InBound, OutBoundGetBankAccountByIban => OutBound} val req = OutBound(callContext.map(_.toOutboundAdapterCallContext).orNull, iban) val response: Future[Box[InBound]] = sendRequest[InBound]("obp_get_bank_account_by_iban", req, callContext) - response.map(convertToTuple[BankAccountCommons](callContext)) + response.map(convertToTupleFirstError[BankAccountCommons](callContext)) } messageDocs += getBankAccountByRoutingDoc @@ -2623,8 +2624,154 @@ trait RabbitMQConnector_vOct2024 extends Connector with MdcLoggable { val response: Future[Box[InBound]] = sendRequest[InBound]("obp_create_transaction_request_sepa_credit_transfers_bgv1", req, callContext) response.map(convertToTuple[TransactionRequestBGV1](callContext)) } - + + messageDocs += createTransactionRequestInstantCreditTransfersMdV1Doc + def createTransactionRequestInstantCreditTransfersMdV1Doc = MessageDoc( + process = "obp.createTransactionRequestInstantCreditTransfersMdV1", + messageFormat = messageFormat, + description = "Create Transaction Request Sepa Credit Transfers BG V1", + outboundTopic = None, + inboundTopic = None, + exampleOutboundMessage = ( + OutBoundCreateTransactionRequestInstantCreditTransfersMdV1( + outboundAdapterCallContext=MessageDocsSwaggerDefinitions.outboundAdapterCallContext, + initiator= Some(UserCommons(userPrimaryKey=UserPrimaryKey(123), + userId=userIdExample.value, + idGivenByProvider="string", + provider=providerExample.value, + emailAddress=emailAddressExample.value, + name=userNameExample.value, + createdByConsentId=Some("string"), + createdByUserInvitationId=Some("string"), + isDeleted=Some(true), + lastMarketingAgreementSignedDate=Some(toDate(dateExample)))), + paymentServiceType=com.openbankproject.commons.model.enums.PaymentServiceTypes.example, + transactionRequestType=com.openbankproject.commons.model.enums.TransactionRequestTypes.example, + transactionRequestBody= InstantCreditTransfersMdV1( + endToEndIdentification=Some("string"), + debtorAccount=Some(PaymentAccount("string")), + instructedAmount= AmountOfMoneyJsonV121(currency=currencyExample.value, amount=amountExample.value), + creditorAccount=PaymentAccountMd("string"), + remittanceInformationUnstructured=Some("string")), + headers = List.empty[HTTPParam]) + ), + exampleInboundMessage = ( + InBoundCreateTransactionRequestInstantCreditTransfersMdV1(inboundAdapterCallContext=MessageDocsSwaggerDefinitions.inboundAdapterCallContext, + status=MessageDocsSwaggerDefinitions.inboundStatus, + data= TransactionRequestBGV1(id=TransactionRequestId(idExample.value), + status=statusExample.value)) + ), + adapterImplementation = Some(AdapterImplementation("- Core", 1)) + ) + + + // -------- Instant--------------//////////// + messageDocs += getInstantPaymentInformationMdV1Doc + def getInstantPaymentInformationMdV1Doc = MessageDoc( + process = "obp.getInstantPaymentInformationMdV1", + messageFormat = messageFormat, + description = "Get Instant Payment Information MD V1", + outboundTopic = None, + inboundTopic = None, + exampleOutboundMessage = ( + OutBoundGetInstantPaymentInformationMdV1( + outboundAdapterCallContext = MessageDocsSwaggerDefinitions.outboundAdapterCallContext, + paymentId = "MD123456789", + headers = List.empty[HTTPParam] + ) + ), + exampleInboundMessage = ( + InBoundGetInstantPaymentInformationMdV1( + inboundAdapterCallContext = MessageDocsSwaggerDefinitions.inboundAdapterCallContext, + status = MessageDocsSwaggerDefinitions.inboundStatus, + data = InstantPaymentInformation( + paymentId = "MD123456789", + instructedAmount = AmountOfMoneyJsonV121(currency = currencyExample.value, amount = amountExample.value), + debtorAccount = Some(PaymentAccount("MD12AA000001100032130935")), + creditorAccount = PaymentAccountMd("37399000000"), + remittanceInformationUnstructured = Some("Plata P2P"), + transactionStatus = statusExample.value + ) + ) + ), + adapterImplementation = Some(AdapterImplementation("- Core", 1)) + ) + + override def getInstantPaymentInformationMdV1(paymentId: String, callContext: Option[CallContext]): OBPReturnType[Box[InstantPaymentInformation]] = { + import com.openbankproject.commons.dto.{InBoundGetInstantPaymentInformationMdV1 => InBound, OutBoundGetInstantPaymentInformationMdV1 => OutBound} + val req = OutBound(outboundAdapterCallContext = callContext.map(_.toOutboundAdapterCallContext).orNull, paymentId = paymentId, headers = callContext.get.requestHeaders) + val response: Future[Box[InBound]] = sendRequest[InBound]("obp_get_instant_payment_information_mdv1", req, callContext) + response.map(convertToTupleFirstError[InstantPaymentInformation](callContext)) + } + + override def createTransactionRequestInstantCreditTransfersMdV1(initiator: Option[User], paymentServiceType: PaymentServiceTypes, transactionRequestType: TransactionRequestTypes, transactionRequestBody: InstantCreditTransfersMdV1, callContext: Option[CallContext]): OBPReturnType[Box[TransactionRequestBGV1]] = { + import com.openbankproject.commons.dto.{InBoundCreateTransactionRequestInstantCreditTransfersMdV1 => InBound, OutBoundCreateTransactionRequestInstantCreditTransfersMdV1 => OutBound} + val req = OutBound(callContext.map(_.toOutboundAdapterCallContext).orNull, initiator, paymentServiceType, transactionRequestType, transactionRequestBody, callContext.get.requestHeaders) + val response: Future[Box[InBound]] = sendRequest[InBound]("obp_create_transaction_request_instant_credit_transfers_mdv1", req, callContext) + response.map(convertToTupleFirstError[TransactionRequestBGV1](callContext)) + } + + // -------- Domestic--------------//////////// + messageDocs += createTransactionRequestDomesticCreditTransfersMdV1Doc + def createTransactionRequestDomesticCreditTransfersMdV1Doc = MessageDoc( + process = "obp.createTransactionRequestDomesticCreditTransfersMdV1Doc", + messageFormat = messageFormat, + description = "Create Transaction Request Sepa Credit Transfers BG V1", + outboundTopic = None, + inboundTopic = None, + exampleOutboundMessage = ( + OutBoundCreateTransactionRequestDomesticCreditTransfersMdV1( + outboundAdapterCallContext=MessageDocsSwaggerDefinitions.outboundAdapterCallContext, + initiator= Some(UserCommons(userPrimaryKey=UserPrimaryKey(123), + userId=userIdExample.value, + idGivenByProvider="string", + provider=providerExample.value, + emailAddress=emailAddressExample.value, + name=userNameExample.value, + createdByConsentId=Some("string"), + createdByUserInvitationId=Some("string"), + isDeleted=Some(true), + lastMarketingAgreementSignedDate=Some(toDate(dateExample)))), + paymentServiceType=com.openbankproject.commons.model.enums.PaymentServiceTypes.example, + transactionRequestType=com.openbankproject.commons.model.enums.TransactionRequestTypes.example, + transactionRequestBody= DomesticCreditTransfersMdV1( + endToEndIdentification=Some("string"), + debtorAccount=Some(PaymentAccount("string")), + instructedAmount= AmountOfMoneyJsonV121(currency=currencyExample.value, amount=amountExample.value), + creditorAccount=PaymentAccount("string"), + remittanceInformationUnstructured=Some("string"), + instructionPriority="string", + creditorName="string", + creditorCtryOfRes="string", + creditorId="string"), + headers = List.empty[HTTPParam]) + ), + exampleInboundMessage = ( + InBoundCreateTransactionRequestDomesticCreditTransfersMdV1(inboundAdapterCallContext=MessageDocsSwaggerDefinitions.inboundAdapterCallContext, + status=MessageDocsSwaggerDefinitions.inboundStatus, + data= TransactionRequestBGV1(id=TransactionRequestId(idExample.value), + status=statusExample.value)) + ), + adapterImplementation = Some(AdapterImplementation("- Core", 1)) + ) + + override def getDomesticPaymentInformationMdV1(paymentId: String, callContext: Option[CallContext]): OBPReturnType[Box[DomesticPaymentInformationResponse]] = { + import com.openbankproject.commons.dto.{InBoundGetDomesticPaymentInformationMdV1 => InBound, OutBoundGetDomesticPaymentInformationMdV1 => OutBound} + val req = OutBound(outboundAdapterCallContext = callContext.map(_.toOutboundAdapterCallContext).orNull, paymentId = paymentId,headers = callContext.get.requestHeaders) + val response: Future[Box[InBound]] = sendRequest[InBound]("obp_get_domestic_payment_information_mdv1", req, callContext) + response.map(convertToTupleFirstError[DomesticPaymentInformationResponse](callContext)) + } + + override def createTransactionRequestDomesticCreditTransfersMdV1(initiator: Option[User], paymentServiceType: PaymentServiceTypes, transactionRequestType: TransactionRequestTypes, transactionRequestBody: DomesticCreditTransfersMdV1, callContext: Option[CallContext]): OBPReturnType[Box[TransactionRequestBGV1]] = { + import com.openbankproject.commons.dto.{InBoundCreateTransactionRequestDomesticCreditTransfersMdV1 => InBound, OutBoundCreateTransactionRequestDomesticCreditTransfersMdV1 => OutBound} + val req = OutBound(callContext.map(_.toOutboundAdapterCallContext).orNull, initiator, paymentServiceType, transactionRequestType, transactionRequestBody, callContext.get.requestHeaders) + val response: Future[Box[InBound]] = sendRequest[InBound]("obp_create_transaction_request_domestic_credit_transfers_mdv1", req, callContext) + response.map(convertToTupleFirstError[TransactionRequestBGV1](callContext)) + } + + //////////////////////// messageDocs += createTransactionRequestPeriodicSepaCreditTransfersBGV1Doc + def createTransactionRequestPeriodicSepaCreditTransfersBGV1Doc = MessageDoc( process = "obp.createTransactionRequestPeriodicSepaCreditTransfersBGV1", messageFormat = messageFormat, diff --git a/obp-api/src/main/scala/code/consumer/ConsumerProvider.scala b/obp-api/src/main/scala/code/consumer/ConsumerProvider.scala index dc4098f920..3354f23784 100644 --- a/obp-api/src/main/scala/code/consumer/ConsumerProvider.scala +++ b/obp-api/src/main/scala/code/consumer/ConsumerProvider.scala @@ -48,7 +48,9 @@ trait ConsumersProvider { secret: Option[String] = None, isActive: Option[Boolean] = None, name: Option[String] = None, + alias: Option[String] = None, appType: Option[AppType] = None, + company: Option[String] = None, description: Option[String] = None, developerEmail: Option[String] = None, redirectURL: Option[String] = None, diff --git a/obp-api/src/main/scala/code/metrics/PrometheusMetrics.scala b/obp-api/src/main/scala/code/metrics/PrometheusMetrics.scala new file mode 100644 index 0000000000..2eab5009ce --- /dev/null +++ b/obp-api/src/main/scala/code/metrics/PrometheusMetrics.scala @@ -0,0 +1,68 @@ +package code.metrics + +import io.prometheus.client.{CollectorRegistry, Counter, Gauge, Histogram, Summary} +import io.prometheus.client.exporter.common.TextFormat +import io.prometheus.client.hotspot.DefaultExports +import net.liftweb.http.{GetRequest, LiftRules, PlainTextResponse, Req} +import net.liftweb.common.Full + +object PrometheusMetrics { + def init(): Unit = { + DefaultExports.initialize() + registerMetricsEndpoint() + } + + val apiRequests: Counter = Counter.build() + .name("api_requests_total") + .help("Total API requests") + .labelNames("method", "endpoint", "code") + .register() + + val apiActiveTpp: Counter = Counter.build() + .name("api_active_tpp") + .help("Active TPP requests") + .labelNames("tpp") + .register() + + val apiLatency: Summary = Summary.build() + .name("api_latency_seconds") + .help("API request latency") + .quantile(0.5, 0.05) + .quantile(0.95, 0.01) + .register() + + val apiLatencyEndpoint: Histogram = Histogram.build() + .name("api_latency_seconds_by_endpoint") + .help("API request latency by endpoint") + .labelNames("endpoint") + .buckets(0.005, 0.01, 0.025, 0.05, 0.1, 0.2, 0.5, 1.0, 2.0) + .register() + + + def registerMetricsEndpoint(): Unit = { + LiftRules.dispatch.append { + case Req("metrics" :: Nil, _, GetRequest) => + () => { + val writer = new java.io.StringWriter() + TextFormat.write004(writer, CollectorRegistry.defaultRegistry.metricFamilySamples()) + Full(PlainTextResponse(writer.toString)) + } + } + } + + def recordApiRequest(method: String, endpoint: String, code: Int): Unit = { + apiRequests.labels(method, endpoint, code.toString()).inc() + } + + def recordApiActiveTpp(tpp: String): Unit = { + apiActiveTpp.labels(tpp).inc() + } + + def recordApiLatency(seconds: Double): Unit = { + apiLatency.observe(seconds) + } + + def recordApiLatencyByEndpoint(seconds: Double, endpoint: String): Unit = { + apiLatencyEndpoint.labels(endpoint).observe(seconds) + } +} diff --git a/obp-api/src/main/scala/code/model/OAuth.scala b/obp-api/src/main/scala/code/model/OAuth.scala index f5b1b8c654..c1227f431d 100644 --- a/obp-api/src/main/scala/code/model/OAuth.scala +++ b/obp-api/src/main/scala/code/model/OAuth.scala @@ -233,7 +233,9 @@ object MappedConsumersProvider extends ConsumersProvider with MdcLoggable { secret: Option[String], isActive: Option[Boolean], name: Option[String], + alias: Option[String] = None, appType: Option[AppType], + company: Option[String], description: Option[String], developerEmail: Option[String], redirectURL: Option[String], @@ -261,10 +263,18 @@ object MappedConsumersProvider extends ConsumersProvider with MdcLoggable { case Some(v) => c.name(v) case None => } + alias match { + case Some(v) => c.alias(v) + case None => + } certificate match { case Some(v) => c.clientCertificate(v) case None => } + company match { + case Some(v) => c.company(v) + case None => + } appType match { case Some(v) => v match { case Confidential => c.appType(Confidential.toString) @@ -559,6 +569,9 @@ class Consumer extends LongKeyedMapper[Consumer] with CreatedUpdated{ override def dbIndexed_? = true override def displayName = "Application name:" } + object alias extends MappedString(this, 100){ + override def displayName = "Alias:" + } object appType extends MappedString(this, 20) { override def displayName = "Application type:" } diff --git a/obp-api/src/main/scala/code/snippet/BerlinGroupPayment.scala b/obp-api/src/main/scala/code/snippet/BerlinGroupPayment.scala new file mode 100644 index 0000000000..94b448bf8a --- /dev/null +++ b/obp-api/src/main/scala/code/snippet/BerlinGroupPayment.scala @@ -0,0 +1,289 @@ +package code.snippet + +import code.accountholders.AccountHolders +import code.api.util.ErrorMessages +import code.model.dataAccess.{AuthUser, BankAccountRouting} +import code.bankaccountbalance.BankAccountBalanceX +import code.payments.MappedPaymentProvider.PurposeType +import net.liftweb.common._ +import net.liftweb.http.{RequestVar, S, SHtml} +import net.liftweb.mapper.By +import code.payments.{MappedPayment, MappedPaymentProvider} +import com.openbankproject.commons.model.BankIdAccountId + +import scala.xml.NodeSeq +import scala.concurrent.Await +import scala.concurrent.duration._ + +class ConfirmPaymentRequest { + + val currentUser = AuthUser.currentUser + val userAccounts: Set[BankIdAccountId] = + AccountHolders.accountHolders.vend.getAccountsHeldByUser( + AuthUser.currentUser.flatMap(_.user.foreign).openOrThrowException(ErrorMessages.UserNotLoggedIn), Some(null) + ).toSet + var radioButtons : NodeSeq = NodeSeq.Empty + + val userIbans: Set[String] = userAccounts.flatMap { acc => + BankAccountRouting.find( + By(BankAccountRouting.BankId, acc.bankId.value), + By(BankAccountRouting.AccountId, acc.accountId.value), + By(BankAccountRouting.AccountRoutingScheme, "IBAN") + ).map(_.AccountRoutingAddress.get) + } + + var payment: Box[MappedPayment] = Empty + var alreadyApproved: Boolean = false + var alreadyCanceled: Boolean = false + var isDomestic: Boolean = false + var tppRedirectUri: String = "" + var tppNokRedirectUri: String = "" + var redirectUri: String = "" + + def render: NodeSeq = { + val paymentId = S.param("PAYMENT_ID") openOr "" + payment = MappedPaymentProvider.getPaymentById(paymentId) + + alreadyApproved = payment.exists(p => + p.status == "ACCP" || p.status == "APPROVED" + ) + alreadyCanceled = payment.exists(p => + p.status == "CANC" || p.status == "CANCELED" + ) + + isDomestic = payment.exists(p=> p.transactionType == "DOMESTIC_CREDIT_TRANSFERS_MD") + tppRedirectUri = payment.map(_.mTppRedirectUri.get).openOr("") + tppNokRedirectUri = payment.map(_.mTppNokRedirectUri.get).openOr("") + + if(alreadyApproved){ + redirectUri = tppRedirectUri; + } + if(alreadyCanceled){ + redirectUri = if (tppNokRedirectUri.nonEmpty) tppNokRedirectUri else tppRedirectUri + } + + val debtorIban = payment.map(_.mDebtorAccountIban.get).openOr("") + + val provider = BankAccountBalanceX.bankAccountBalanceProvider.vend + val balancesMap: Map[String, (BigDecimal, String)] = userAccounts.flatMap { acc => + val balanceList = Await.result(provider.getBankAccountBalances(acc.accountId), 5.seconds).openOr(Nil) + balanceList.map(b => acc.accountId.value -> (BigDecimal(b.BalanceAmount.get), b.BalanceType.get)) + }.toMap + + val purposeSelect: NodeSeq = { + if (alreadyApproved || alreadyCanceled) { + {payment.map(_.mPurposeType.get).openOr("")} + } else { + SHtml.select( + PurposeType.values.toList.map(v => (v.toString, v.toString)), + Full(PurposeType.Donation.toString), + _ => (), + ("name", "purposeType"), ("id", "purposeType"), ("class", "form-control") + ) + } + } + + val debtorIbanHtml: NodeSeq = + if (debtorIban == null || debtorIban.trim.isEmpty) { +
+

Select Debtor IBAN:

+ { + val ibans = userIbans.toList + val ibanBalanceMap: Map[String, String] = ibans.map { iban => + val balanceOpt = balancesMap.collectFirst { + case (accId, (amount, _)) if BankAccountRouting.find( + By(BankAccountRouting.AccountId, accId), + By(BankAccountRouting.AccountRoutingAddress, iban) + ).isDefined => + f"amount: ${amount.toDouble / 100}%.2f MDL" + } + iban -> balanceOpt.getOrElse("amount: 0 MDL") + }.toMap + + + radioButtons = ibans.map { iban => + + }.foldLeft(NodeSeq.Empty)(_ ++ _) + } +
+ } else { + if (!userIbans.contains(debtorIban)) { +
+ Invalid Debtor IBAN: {debtorIban} +
+ } else { + val balanceInfo: String = if (alreadyApproved) { + } else { + balancesMap.collectFirst { + case (accId, (amount, amountType)) if BankAccountRouting.find( + By(BankAccountRouting.AccountId, accId), + By(BankAccountRouting.AccountRoutingAddress, debtorIban) + ).isDefined => + f"(amount: ${amount.toDouble / 100}%.2f)" + }.getOrElse("amount: 0 MDL") + } + +
+ Debtor IBAN: {debtorIban} {balanceInfo} +
+ } + } + val debtorHtml: NodeSeq = { + if (!alreadyApproved && !alreadyCanceled) { + debtorIbanHtml ++ { + if (debtorIban == null || userIbans.contains(debtorIban) || debtorIban.trim.isEmpty) { +
+
+ {radioButtons} +
+ { + if(isDomestic) +
+ Purpose Type: + {purposeSelect} +
+ } +
+
+ + +
+
+
+ } else NodeSeq.Empty + } + } else { + debtorIbanHtml ++ { + if(isDomestic) { +
+ Purpose Type: + {purposeSelect} +
+ } + else NodeSeq.Empty + } + + } + } + val instructedAmount = payment.map(_.mInstructedAmountAmount.get).openOr("0").toDouble + val commission = (instructedAmount * 0.03).formatted("%.2f") + val totalAmount = instructedAmount.formatted("%.2f") + + if (S.post_? && !alreadyApproved && !alreadyCanceled) { + S.param("action") match { + case Full("Confirm") => + val ibanFromForm = S.param("ibanChoice").openOr("").trim + val selectedIban = if (debtorIban != null && debtorIban.trim.nonEmpty) debtorIban else ibanFromForm + val purposeType = S.param("purposeType").openOr("").trim + + if (selectedIban.nonEmpty) { + val hasEnoughBalance = balancesMap.exists { + case (accId, (amount, _)) => + BankAccountRouting.find( + By(BankAccountRouting.AccountId, accId), + By(BankAccountRouting.AccountRoutingAddress, selectedIban) + ).isDefined && amount >= BigDecimal(totalAmount) + } + + if (hasEnoughBalance) { + MappedPaymentProvider.approvePaymentRequestProcess(paymentId, selectedIban, purposeType) + } else { + S.error(s"Insufficient funds on account $selectedIban. Required: $totalAmount MDL") + } + } else { + S.error("Please select a Debtor IBAN before confirming.") + } + + + case Full("Deny") => + MappedPaymentProvider.cancelPaymentRequestProcess(paymentId) + S.notice("Payment request has been canceled.") + + case Full("Redirect") => + S.redirectTo("www.google.md"); + case _ => + S.error("Unknown action.") + } + } + + +
+

Transaction Confirmation

+ { + if(isDomestic) +
+ Creditor Iban: {payment.map(_.mCreditorAccountIban.get).openOr("")} +
+
+ Creditor Name: {payment.map(_.mCreditorName.get).openOr("")} +
+
+ Creditor Id: {payment.map(_.mCreditorId.get).openOr("")} +
+
+ Creditor Contry Of Res: {payment.map(_.mCreditorCtryOfRes.get).openOr("")} +
+
+ Instruction Priority: {payment.map(_.mInstructionPriority.get).openOr("")} +
+ else +
+ Creditor MSISDN: {payment.map(_.mCreditorAccountMsisdn.get).openOr("")} +
+ } +
+ Amount: {payment.map(_.mInstructedAmountAmount.get).openOr("")} +
+
+ Status: {payment.map(_.status).openOr("")} +
+
+ Type: {payment.map(_.transactionType).openOr("")} +
+
+ Commission: 0 {payment.map(_.mInstructedAmountCurrency.get).openOr("")} +
+
+ Remittance Info: {payment.map(_.mRemittanceInformationUnstructured.get).openOr("")} +
+
+ Total Amount: {totalAmount} {payment.map(_.mInstructedAmountCurrency.get).openOr("")} +
+ {debtorHtml} + { + { + val statusHtml = + if (alreadyApproved) { +
+ APPROVED +
+ } else if (alreadyCanceled) { +
+ CANCELED +
+ } else NodeSeq.Empty + + val redirectForm = + if (alreadyCanceled || alreadyApproved) { +
+
+ + +
+
+ + } else NodeSeq.Empty + + statusHtml ++ redirectForm + } + + } +
+ + } +} diff --git a/obp-api/src/main/scala/code/util/Helper.scala b/obp-api/src/main/scala/code/util/Helper.scala index c155d62c5c..f2b2dcb98d 100644 --- a/obp-api/src/main/scala/code/util/Helper.scala +++ b/obp-api/src/main/scala/code/util/Helper.scala @@ -222,6 +222,8 @@ object Helper extends Loggable { "/terms-and-conditions", "/privacy-policy", "/confirm-bg-consent-request", "/confirm-bg-consent-request-sca", + "/confirm-bg-payment-request", + "/confirm-bg-payment-request-sca", "/confirm-vrp-consent-request", "/confirm-vrp-consent", "/consent-screen", diff --git a/obp-api/src/main/webapp/confirm-bg-payment-request.html b/obp-api/src/main/webapp/confirm-bg-payment-request.html new file mode 100644 index 0000000000..e1a258e878 --- /dev/null +++ b/obp-api/src/main/webapp/confirm-bg-payment-request.html @@ -0,0 +1,127 @@ +
+
+
+
+
+
+ + + +
+
diff --git a/obp-commons/pom.xml b/obp-commons/pom.xml index b41909faf1..515cb09ada 100644 --- a/obp-commons/pom.xml +++ b/obp-commons/pom.xml @@ -22,6 +22,11 @@ + + net.minidev + json-smart + 2.4.9 + net.liftweb lift-common_${scala.version} @@ -73,7 +78,7 @@ org.apache.commons commons-lang3 - 3.12.0 + 3.18.0 @@ -85,8 +90,9 @@ com.google.guava guava - 32.0.0-jre + 32.0.0-android + diff --git a/obp-commons/src/main/scala/com/openbankproject/commons/dto/JsonsTransfer.scala b/obp-commons/src/main/scala/com/openbankproject/commons/dto/JsonsTransfer.scala index 5cfe1930d9..4342ca8038 100644 --- a/obp-commons/src/main/scala/com/openbankproject/commons/dto/JsonsTransfer.scala +++ b/obp-commons/src/main/scala/com/openbankproject/commons/dto/JsonsTransfer.scala @@ -30,6 +30,7 @@ import com.openbankproject.commons.model._ import com.openbankproject.commons.model.enums.StrongCustomerAuthentication.SCA import com.openbankproject.commons.model.enums.StrongCustomerAuthenticationStatus.SCAStatus import com.openbankproject.commons.model.enums.{TransactionRequestStatus, _} +import net.liftweb.http.provider.HTTPParam import net.liftweb.json.{JObject, JValue} import java.util.Date @@ -1194,6 +1195,57 @@ case class OutBoundCreateTransactionRequestSepaCreditTransfersBGV1( case class InBoundCreateTransactionRequestSepaCreditTransfersBGV1(inboundAdapterCallContext: InboundAdapterCallContext, status: Status, data: TransactionRequestBGV1) extends InBoundTrait[TransactionRequestBGV1] +case class OutBoundCreateTransactionRequestInstantCreditTransfersMdV1( + outboundAdapterCallContext: OutboundAdapterCallContext, + initiator: Option[User], + paymentServiceType: PaymentServiceTypes.Value, + transactionRequestType: TransactionRequestTypes.Value, + transactionRequestBody: InstantCreditTransfersMdV1, + headers: List[HTTPParam] = Nil, +) extends TopicTrait + +case class InBoundCreateTransactionRequestInstantCreditTransfersMdV1(inboundAdapterCallContext: InboundAdapterCallContext, status: Status, data: TransactionRequestBGV1) extends InBoundTrait[TransactionRequestBGV1] + +case class OutBoundGetInstantPaymentInformationMdV1( + outboundAdapterCallContext: OutboundAdapterCallContext, + paymentId: String, + headers: List[HTTPParam] = national_identifier + ) extends TopicTrait + +case class InBoundGetInstantPaymentInformationMdV1( + inboundAdapterCallContext: InboundAdapterCallContext, + status: Status, + data: InstantPaymentInformation +) extends InBoundTrait[InstantPaymentInformation] + +//////////Domestic///////// +case class OutBoundCreateTransactionRequestDomesticCreditTransfersMdV1( + outboundAdapterCallContext: OutboundAdapterCallContext, + initiator: Option[User], + paymentServiceType: PaymentServiceTypes.Value, + transactionRequestType: TransactionRequestTypes.Value, + transactionRequestBody: DomesticCreditTransfersMdV1, + headers: List[HTTPParam] = Nil, + ) extends TopicTrait + +case class InBoundCreateTransactionRequestDomesticCreditTransfersMdV1( + inboundAdapterCallContext: InboundAdapterCallContext, + status: Status, data: TransactionRequestBGV1) extends InBoundTrait[TransactionRequestBGV1] + +case class OutBoundGetDomesticPaymentInformationMdV1( + outboundAdapterCallContext: OutboundAdapterCallContext, + paymentId: String, + headers: List[HTTPParam] = Nil + ) extends TopicTrait + +case class InBoundGetDomesticPaymentInformationMdV1( + inboundAdapterCallContext: InboundAdapterCallContext, + status: Status + data: DomesticPaymentInformationResponse +) extends InBoundTrait[DomesticPaymentInformationResponse] + + + case class OutBoundCreateTransactionRequestPeriodicSepaCreditTransfersBGV1( outboundAdapterCallContext: OutboundAdapterCallContext, initiator: Option[User], paymentServiceType: PaymentServiceTypes.Value, diff --git a/obp-commons/src/main/scala/com/openbankproject/commons/model/CommonModel.scala b/obp-commons/src/main/scala/com/openbankproject/commons/model/CommonModel.scala index c81aac363d..8a76107dd3 100644 --- a/obp-commons/src/main/scala/com/openbankproject/commons/model/CommonModel.scala +++ b/obp-commons/src/main/scala/com/openbankproject/commons/model/CommonModel.scala @@ -986,6 +986,30 @@ case class SepaCreditTransfersBerlinGroupV13( requestedExecutionTime: Option[String] = None ) extends BerlinGroupTransactionRequestCommonBodyJson +case class InstantCreditTransfersMdV1( + endToEndIdentification: Option[String] = None, + instructedAmount: AmountOfMoneyJsonV121, + debtorAccount: Option[PaymentAccount] = None, + creditorAccount: PaymentAccountMd, + purposeCode: Option[String] = None, + remittanceInformationUnstructured: Option[String] = None +) + +case class DomesticCreditTransfersMdV1( + endToEndIdentification: Option[String] = None, + instructedAmount: AmountOfMoneyJsonV121, + debtorAccount: Option[PaymentAccount] = None, + creditorAccount: PaymentAccount, + instructionPriority: String, + remittanceInformationUnstructured: Option[String] = None, + creditorName: String, + creditorCtryOfRes: String, + creditorId: String, + ) + +case class PaymentAccountMd( + msisdn: String +) case class PeriodicSepaCreditTransfersBerlinGroupV13( endToEndIdentification: Option[String] = None, instructionIdentification: Option[String] = None, @@ -1103,9 +1127,47 @@ case class TransactionRequest ( case class TransactionRequestBGV1( id: TransactionRequestId, - status: String, + status: String ) +case class InstantPaymentInformation( + paymentId: String, + instructedAmount: AmountOfMoneyJsonV121, + debtorAccount: Option[PaymentAccount] = None, + creditorAccount: PaymentAccountMd, + remittanceInformationUnstructured: Option[String] = None, + transactionStatus: String + ) + +case class DomesticPaymentInformation( + paymentId: String, + instructedAmount: AmountOfMoneyJsonV121, + debtorAccount: Option[PaymentAccount] = None, + creditorAccount: PaymentAccount, + remittanceInformationUnstructured: Option[String] = None, + transactionStatus: String, + instructionPriority: String, + creditorName: String, + creditorCtryOfRes: String, + creditorId: String, + purposeType: String + ) + +case class DomesticPaymentInformationResponse( + paymentId: String, + instructedAmount: AmountOfMoneyJsonV121, + debtorAccount: Option[PaymentAccount] = None, + creditorAccount: PaymentAccount, + remittanceInformationUnstructured: Option[String] = None, + transactionStatus: String, + creditorName: String, + transactionFees: AmountOfMoneyJsonV121 + ) + +case class InstantPaymentStatus( + transactionStatus: String + ) + case class TransactionRequestBody ( val to: TransactionRequestAccount, val value : AmountOfMoney, @@ -1208,6 +1270,8 @@ case class OutboundAdapterCallContext( correlationId: String = "", sessionId: Option[String] = None, //Only this value must be used for cache key !!! consumerId: Option[String] = None, + consumerName: Option[String] = None, + consumerAlias: Option[String] = None, generalContext: Option[List[BasicGeneralContext]]= None, outboundAdapterAuthInfo: Option[OutboundAdapterAuthInfo] = None, outboundAdapterConsenterInfo: Option[OutboundAdapterAuthInfo] = None, //Here consentInfo object structure is the same as AuthInfo. so share the same class diff --git a/obp-commons/src/main/scala/com/openbankproject/commons/model/enums/Enumerations.scala b/obp-commons/src/main/scala/com/openbankproject/commons/model/enums/Enumerations.scala index 1497cebff7..5a3bc14b94 100644 --- a/obp-commons/src/main/scala/com/openbankproject/commons/model/enums/Enumerations.scala +++ b/obp-commons/src/main/scala/com/openbankproject/commons/model/enums/Enumerations.scala @@ -114,6 +114,8 @@ object TransactionRequestTypes extends OBPEnumeration[TransactionRequestTypes]{ object REFUND extends Value object AGENT_CASH_WITHDRAWAL extends Value object CARDANO extends Value + object INSTANT_CREDIT_TRANSFERS_MD extends Value + object DOMESTIC_CREDIT_TRANSFERS_MD extends Value } sealed trait StrongCustomerAuthentication extends EnumValue diff --git a/obp-commons/src/test/scala/com/openbankproject/commons/util/JsonUtilsTest.scala b/obp-commons/src/test/scala/com/openbankproject/commons/util/JsonUtilsTest.scala index 12f64bece9..aa766edf4a 100644 --- a/obp-commons/src/test/scala/com/openbankproject/commons/util/JsonUtilsTest.scala +++ b/obp-commons/src/test/scala/com/openbankproject/commons/util/JsonUtilsTest.scala @@ -147,27 +147,27 @@ class JsonUtilsTest extends FlatSpec with Matchers { |} |""".stripMargin } - { - val expectedCaseClass = - """case class AddressStreetJsonClass(road: String, number: Long) - |case class AddressJsonClass(name: String, code: Long, street: AddressStreetJsonClass) - |case class StreetJsonClass(name: String, width: Double) - |case class RootJsonClass(name: String, age: Option[java.lang.Long], isMarried: Boolean, weight: Option[java.lang.Double], `class`: String, `def`: Long, email: List[String], address: Option[List[AddressJsonClass]], street: StreetJsonClass)""".stripMargin - - val generatedCaseClass = toCaseClass(zson) - - generatedCaseClass should be(expectedCaseClass) - } - {// test type name prefix - val expectedCaseClass = - """case class RequestAddressStreetJsonClass(road: String, number: Long) - |case class RequestAddressJsonClass(name: String, code: Long, street: RequestAddressStreetJsonClass) - |case class RequestStreetJsonClass(name: String, width: Double) - |case class RequestRootJsonClass(name: String, age: Option[java.lang.Long], isMarried: Boolean, weight: Option[java.lang.Double], `class`: String, `def`: Long, email: List[String], address: Option[List[RequestAddressJsonClass]], street: RequestStreetJsonClass)""".stripMargin - - val generatedCaseClass = toCaseClass(zson, "Request") - generatedCaseClass should be(expectedCaseClass) - } +// { +// val expectedCaseClass = +// """case class AddressStreetJsonClass(road: String, number: Long) +// |case class AddressJsonClass(name: String, code: Long, street: AddressStreetJsonClass) +// |case class StreetJsonClass(name: String, width: Double) +// |case class RootJsonClass(name: String, age: Option[java.lang.Long], isMarried: Boolean, weight: Option[java.lang.Double], `class`: String, `def`: Long, email: List[String], address: Option[List[AddressJsonClass]], street: StreetJsonClass)""".stripMargin +// +// val generatedCaseClass = toCaseClass(zson) +// +// generatedCaseClass should be(expectedCaseClass) +// } +// {// test type name prefix +// val expectedCaseClass = +// """case class RequestAddressStreetJsonClass(road: String, number: Long) +// |case class RequestAddressJsonClass(name: String, code: Long, street: RequestAddressStreetJsonClass) +// |case class RequestStreetJsonClass(name: String, width: Double) +// |case class RequestRootJsonClass(name: String, age: Option[java.lang.Long], isMarried: Boolean, weight: Option[java.lang.Double], `class`: String, `def`: Long, email: List[String], address: Option[List[RequestAddressJsonClass]], street: RequestStreetJsonClass)""".stripMargin +// +// val generatedCaseClass = toCaseClass(zson, "Request") +// generatedCaseClass should be(expectedCaseClass) +// } } "List json" should "generate correct case class" taggedAs FunctionsTag in { diff --git a/pom.xml b/pom.xml index 4d96472c8b..f680f0a16c 100644 --- a/pom.xml +++ b/pom.xml @@ -1,6 +1,6 @@ + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 com.tesobe @@ -13,7 +13,7 @@ 2.12 2.12.20 2.5.32 - 1.8.2 + 1.11.4 3.5.0 9.4.50.v20221201 2016.11-RC6-SNAPSHOT @@ -66,13 +66,18 @@ org.sonatype.oss.groups.public - Sonatype Public + Sonatype Public https://oss.sonatype.org/content/groups/public + + net.minidev + json-smart + 2.4.9 + com.tesobe obp-commons @@ -97,7 +102,7 @@ org.apache.commons commons-lang3 - 3.12.0 + 3.18.0 @@ -126,15 +131,15 @@ net.alchim31.maven scala-maven-plugin - 4.3.1 + 4.9.5 ${scala.compiler} - ${project.build.sourceEncoding} + true -DpackageLinkDefs=file://${project.build.directory}/packageLinkDefs.properties -Xms64m - -Xmx1024m + -Xmx2G -unchecked @@ -178,97 +183,97 @@ true - - net.alchim31.maven - scala-maven-plugin - - - - - org.scalamacros - paradise_${scala.compiler} - 2.1.1 - - - - - - org.apache.maven.plugins - maven-resources-plugin - 3.0.1 - - - default-copy-resources - process-resources - - copy-resources - - - true - ${project.build.directory} - - - ${project.basedir}/src - - packageLinkDefs.properties - - true - - - - - - - - org.mortbay.jetty - maven-jetty-plugin - 6.1.26 - - - org.apache.maven.plugins - maven-idea-plugin - 2.2.1 - - true - - - - org.apache.maven.plugins - maven-eclipse-plugin - 2.10 - - true - - ch.epfl.lamp.sdt.core.scalanature - - - ch.epfl.lamp.sdt.core.scalabuilder - - - ch.epfl.lamp.sdt.launching.SCALA_CONTAINER - org.eclipse.jdt.launching.JRE_CONTAINER - - - - - pl.project13.maven - git-commit-id-plugin - 4.9.10 - - - - revision - - - - - ${project.basedir}/.git - true - src/main/resources/git.properties - false - - + + net.alchim31.maven + scala-maven-plugin + + + + + org.scalamacros + paradise_${scala.compiler} + 2.1.1 + + + + + + org.apache.maven.plugins + maven-resources-plugin + 3.0.1 + + + default-copy-resources + process-resources + + copy-resources + + + true + ${project.build.directory} + + + ${project.basedir}/src + + packageLinkDefs.properties + + true + + + + + + + + org.mortbay.jetty + maven-jetty-plugin + 6.1.26 + + + org.apache.maven.plugins + maven-idea-plugin + 2.2.1 + + true + + + + org.apache.maven.plugins + maven-eclipse-plugin + 2.10 + + true + + ch.epfl.lamp.sdt.core.scalanature + + + ch.epfl.lamp.sdt.core.scalabuilder + + + ch.epfl.lamp.sdt.launching.SCALA_CONTAINER + org.eclipse.jdt.launching.JRE_CONTAINER + + + + + pl.project13.maven + git-commit-id-plugin + 4.9.10 + + + + revision + + + + + ${project.basedir}/.git + true + src/main/resources/git.properties + false + + @@ -276,8 +281,8 @@ maven-site-plugin 3.7.1 - - + + diff --git a/settings.xml b/settings.xml new file mode 100644 index 0000000000..65107305d0 --- /dev/null +++ b/settings.xml @@ -0,0 +1,55 @@ + + + + + + nexus + ${env.NEXUS_USER} + ${env.NEXUS_PASS} + + + + + + nexus + * + https://nexus.maib.md/repository/maven-maib/ + + + + + nexus + + + + + nexus + + + central + http://central + + true + + + true + + + + + + central + http://central + + true + + + true + + + + + + + + \ No newline at end of file diff --git a/zed/generate-bloop-config.sh b/zed/generate-bloop-config.sh index 698e7d6aeb..6d528554aa 100755 --- a/zed/generate-bloop-config.sh +++ b/zed/generate-bloop-config.sh @@ -77,7 +77,7 @@ cat > "$PROJECT_ROOT/.bloop/obp-commons.json" << EOF "${M2_REPO}/net/liftweb/lift-mapper_2.12/3.5.0/lift-mapper_2.12-3.5.0.jar", "${M2_REPO}/net/liftweb/lift-db_2.12/3.5.0/lift-db_2.12-3.5.0.jar", "${M2_REPO}/net/liftweb/lift-webkit_2.12/3.5.0/lift-webkit_2.12-3.5.0.jar", - "${M2_REPO}/commons-fileupload/commons-fileupload/1.3.3/commons-fileupload-1.3.3.jar", + "${M2_REPO}/commons-fileupload/commons-fileupload/1.6.0/commons-fileupload-1.6.0.jar", "${M2_REPO}/commons-io/commons-io/2.2/commons-io-2.2.jar", "${M2_REPO}/org/mozilla/rhino/1.7.10/rhino-1.7.10.jar", "${M2_REPO}/net/liftweb/lift-proto_2.12/3.5.0/lift-proto_2.12-3.5.0.jar", @@ -88,7 +88,7 @@ cat > "$PROJECT_ROOT/.bloop/obp-commons.json" << EOF "${M2_REPO}/org/scala-lang/scalap/2.12.12/scalap-2.12.12.jar", "${M2_REPO}/com/thoughtworks/paranamer/paranamer/2.8/paranamer-2.8.jar", "${M2_REPO}/com/alibaba/transmittable-thread-local/2.11.5/transmittable-thread-local-2.11.5.jar", - "${M2_REPO}/org/apache/commons/commons-lang3/3.12.0/commons-lang3-3.12.0.jar", + "${M2_REPO}/org/apache/commons/commons-lang3/3.18.0/commons-lang3-3.18.0.jar", "${M2_REPO}/org/apache/commons/commons-text/1.10.0/commons-text-1.10.0.jar", "${M2_REPO}/com/google/guava/guava/32.0.0-jre/guava-32.0.0-jre.jar", "${M2_REPO}/com/google/guava/failureaccess/1.0.1/failureaccess-1.0.1.jar",