Skip to content

Commit e9644aa

Browse files
authored
Merge pull request #2729 from hongwei1/develop
feature/TweakedBgGroupAndProviderToken
2 parents 15eba29 + ad92844 commit e9644aa

9 files changed

Lines changed: 289 additions & 83 deletions

File tree

obp-api/src/main/scala/code/api/OAuth2.scala

Lines changed: 48 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -358,8 +358,8 @@ object OAuth2Login extends RestHelper with MdcLoggable {
358358
}
359359
}
360360

361-
def getClaim(name: String, idToken: String): Option[String] = {
362-
val claim = JwtUtil.getClaim(name = name, jwtToken = idToken)
361+
def getClaim(name: String, jwtToken: String): Option[String] = {
362+
val claim = JwtUtil.getClaim(name = name, jwtToken = jwtToken)
363363
claim match {
364364
case null => None
365365
case string => Some(string)
@@ -385,7 +385,7 @@ object OAuth2Login extends RestHelper with MdcLoggable {
385385
* It is mapped in next way:
386386
* iss => ResourceUser.provider_
387387
* sub => ResourceUser.providerId
388-
* @param idToken Google's response example:
388+
* @param jwtToken Google's response example:
389389
* {
390390
* "access_token": "ya29.GluUBg5DflrJciFikW5hqeKEp9r1whWnU5x2JXCm9rKkRMs2WseXX8O5UugFMDsIKuKCZlE7tTm1fMII_YYpvcMX6quyR5DXNHH8Lbx5TrZN__fA92kszHJEVqPc",
391391
* "id_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6IjA4ZDMyNDVjNjJmODZiNjM2MmFmY2JiZmZlMWQwNjk4MjZkZDFkYzEiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJhenAiOiI0MDc0MDg3MTgxOTIuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJhdWQiOiI0MDc0MDg3MTgxOTIuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJzdWIiOiIxMTM5NjY4NTQyNDU3ODA4OTI5NTkiLCJlbWFpbCI6Im1hcmtvLm1pbGljLnNyYmlqYUBnbWFpbC5jb20iLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwiYXRfaGFzaCI6Im5HS1JUb0tOblZBMjhINk1od1hCeHciLCJuYW1lIjoiTWFya28gTWlsacSHIiwicGljdHVyZSI6Imh0dHBzOi8vbGg1Lmdvb2dsZXVzZXJjb250ZW50LmNvbS8tWGQ0NGhuSjZURG8vQUFBQUFBQUFBQUkvQUFBQUFBQUFBQUEvQUt4cndjYWR3emhtNE40dFdrNUU4QXZ4aS1aSzZrczRxZy9zOTYtYy9waG90by5qcGciLCJnaXZlbl9uYW1lIjoiTWFya28iLCJmYW1pbHlfbmFtZSI6Ik1pbGnEhyIsImxvY2FsZSI6ImVuIiwiaWF0IjoxNTQ3NzA1NjkxLCJleHAiOjE1NDc3MDkyOTF9.iUxhF_SU2vi76zPuRqAKJvFOzpb_EeP3lc5u9FO9o5xoXzVq3QooXexTfK2f1YAcWEy9LSftA34PB0QTuCZpkQChZVM359n3a3hplf6oWWkBXZN2_IG10NwEH4g0VVBCsjWBDMp6lvepN_Zn15x8opUB7272m4-smAou_WmUPTeivXRF8yPcp4J55DigcY31YP59dMQr2X-6Rr1vCRnJ6niqqJ1UDldfsgt4L7dXmUCnkDdXHwEQAZwbKbR4dUoEha3QeylCiBErmLdpIyqfKECphC6piGXZB-rRRqLz41WNfuF-3fswQvGmIkzTJDR7lQaletMp7ivsfVw8N5jFxg",
@@ -396,23 +396,23 @@ object OAuth2Login extends RestHelper with MdcLoggable {
396396
* }
397397
* @return an existing or a new user
398398
*/
399-
def getOrCreateResourceUserFuture(idToken: String): Future[Box[User]] = {
400-
val uniqueIdGivenByProvider = JwtUtil.getSubject(idToken).getOrElse("")
401-
val provider = resolveProvider(idToken)
399+
def getOrCreateResourceUserFuture(jwtToken: String): Future[Box[User]] = {
400+
val uniqueIdGivenByProvider = JwtUtil.getSubject(jwtToken).getOrElse("")
401+
val provider = resolveProvider(jwtToken)
402402
Users.users.vend.getOrCreateUserByProviderIdFuture(
403403
provider = provider,
404404
idGivenByProvider = uniqueIdGivenByProvider,
405405
consentId = None,
406-
name = getClaim(name = "given_name", idToken = idToken).orElse(Some(uniqueIdGivenByProvider)),
407-
email = getClaim(name = "email", idToken = idToken)
406+
name = getClaim(name = "given_name", jwtToken = jwtToken).orElse(Some(uniqueIdGivenByProvider)),
407+
email = getClaim(name = "email", jwtToken = jwtToken)
408408
).map(_._1)
409409
}
410410
/** Old Style Endpoints
411411
* This function creates user based on "iss" and "sub" fields
412412
* It is mapped in next way:
413413
* iss => ResourceUser.provider_
414414
* sub => ResourceUser.providerId
415-
* @param idToken Google's response example:
415+
* @param jwtToken Google's response example:
416416
* {
417417
* "access_token": "ya29.GluUBg5DflrJciFikW5hqeKEp9r1whWnU5x2JXCm9rKkRMs2WseXX8O5UugFMDsIKuKCZlE7tTm1fMII_YYpvcMX6quyR5DXNHH8Lbx5TrZN__fA92kszHJEVqPc",
418418
* "id_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6IjA4ZDMyNDVjNjJmODZiNjM2MmFmY2JiZmZlMWQwNjk4MjZkZDFkYzEiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJhenAiOiI0MDc0MDg3MTgxOTIuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJhdWQiOiI0MDc0MDg3MTgxOTIuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJzdWIiOiIxMTM5NjY4NTQyNDU3ODA4OTI5NTkiLCJlbWFpbCI6Im1hcmtvLm1pbGljLnNyYmlqYUBnbWFpbC5jb20iLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwiYXRfaGFzaCI6Im5HS1JUb0tOblZBMjhINk1od1hCeHciLCJuYW1lIjoiTWFya28gTWlsacSHIiwicGljdHVyZSI6Imh0dHBzOi8vbGg1Lmdvb2dsZXVzZXJjb250ZW50LmNvbS8tWGQ0NGhuSjZURG8vQUFBQUFBQUFBQUkvQUFBQUFBQUFBQUEvQUt4cndjYWR3emhtNE40dFdrNUU4QXZ4aS1aSzZrczRxZy9zOTYtYy9waG90by5qcGciLCJnaXZlbl9uYW1lIjoiTWFya28iLCJmYW1pbHlfbmFtZSI6Ik1pbGnEhyIsImxvY2FsZSI6ImVuIiwiaWF0IjoxNTQ3NzA1NjkxLCJleHAiOjE1NDc3MDkyOTF9.iUxhF_SU2vi76zPuRqAKJvFOzpb_EeP3lc5u9FO9o5xoXzVq3QooXexTfK2f1YAcWEy9LSftA34PB0QTuCZpkQChZVM359n3a3hplf6oWWkBXZN2_IG10NwEH4g0VVBCsjWBDMp6lvepN_Zn15x8opUB7272m4-smAou_WmUPTeivXRF8yPcp4J55DigcY31YP59dMQr2X-6Rr1vCRnJ6niqqJ1UDldfsgt4L7dXmUCnkDdXHwEQAZwbKbR4dUoEha3QeylCiBErmLdpIyqfKECphC6piGXZB-rRRqLz41WNfuF-3fswQvGmIkzTJDR7lQaletMp7ivsfVw8N5jFxg",
@@ -423,9 +423,9 @@ object OAuth2Login extends RestHelper with MdcLoggable {
423423
* }
424424
* @return an existing or a new user
425425
*/
426-
def getOrCreateResourceUser(idToken: String): Box[User] = {
427-
val uniqueIdGivenByProvider = JwtUtil.getSubject(idToken).getOrElse("")
428-
val provider = resolveProvider(idToken)
426+
def getOrCreateResourceUser(jwtToken: String): Box[User] = {
427+
val uniqueIdGivenByProvider = JwtUtil.getSubject(jwtToken).getOrElse("")
428+
val provider = resolveProvider(jwtToken)
429429
KeycloakFederatedUserReference.parse(uniqueIdGivenByProvider) match {
430430
case Right(fedRef) => // Users log on via Keycloak, which uses User Federation to access the external OBP database.
431431
logger.debug(s"External ID = ${fedRef.externalId}")
@@ -438,8 +438,8 @@ object OAuth2Login extends RestHelper with MdcLoggable {
438438
provider = provider,
439439
providerId = Some(uniqueIdGivenByProvider),
440440
None,
441-
name = getClaim(name = "given_name", idToken = idToken).orElse(Some(uniqueIdGivenByProvider)),
442-
email = getClaim(name = "email", idToken = idToken),
441+
name = getClaim(name = "given_name", jwtToken = jwtToken).orElse(Some(uniqueIdGivenByProvider)),
442+
email = getClaim(name = "email", jwtToken = jwtToken),
443443
userId = None,
444444
createdByUserInvitationId = None,
445445
company = None,
@@ -449,21 +449,31 @@ object OAuth2Login extends RestHelper with MdcLoggable {
449449
}
450450
}
451451

452-
def resolveProvider(idToken: String) = {
453-
HydraUtil.integrateWithHydra && isIssuer(jwtToken = idToken, identityProvider = hydraPublicUrl) match {
454-
case true if HydraUtil.hydraUsesObpUserCredentials => // Case that source of the truth of Hydra user management is the OBP-API mapper DB
455-
logger.debug("resolveProvider says: we are in Hydra ")
456-
// In case that ORY Hydra login url is "hostname/user_mgt/login" we MUST override hydraPublicUrl as provider
457-
// in order to avoid creation of a new user
458-
Constant.localIdentityProvider
459-
// if its OBPOIDC issuer
460-
case false if OBPOIDC.isIssuer(idToken) =>
461-
logger.debug("resolveProvider says: we are in OBPOIDC ")
462-
Constant.localIdentityProvider
463-
case _ => // All other cases implies a new user creation
464-
logger.debug("resolveProvider says: Other cases ")
465-
// TODO raise exception in case of else case
466-
JwtUtil.getIssuer(idToken).getOrElse("")
452+
def resolveProvider(jwtToken: String) = {
453+
// First try to get provider from token's provider claim
454+
val providerFromToken = JwtUtil.getProvider(jwtToken)
455+
456+
providerFromToken match {
457+
case Some(provider) =>
458+
logger.debug(s"resolveProvider says: using provider from token claim: $provider")
459+
provider
460+
case None =>
461+
// Fallback to existing logic if provider claim is not present
462+
HydraUtil.integrateWithHydra && isIssuer(jwtToken = jwtToken, identityProvider = hydraPublicUrl) match {
463+
case true if HydraUtil.hydraUsesObpUserCredentials => // Case that source of the truth of Hydra user management is the OBP-API mapper DB
464+
logger.debug(s"resolveProvider says: we are in Hydra, use Constant.localIdentityProvider ${Constant.localIdentityProvider}")
465+
// In case that ORY Hydra login url is "hostname/user_mgt/login" we MUST override hydraPublicUrl as provider
466+
// in order to avoid creation of a new user
467+
Constant.localIdentityProvider
468+
// if its OBPOIDC issuer
469+
case false if OBPOIDC.isIssuer(jwtToken) =>
470+
logger.debug(s"resolveProvider says: we are in OBP-OIDC, use Constant.localIdentityProvider ${Constant.localIdentityProvider}")
471+
Constant.localIdentityProvider
472+
case _ => // All other cases implies a new user creation
473+
logger.debug("resolveProvider says: Other cases ")
474+
// TODO raise exception in case of else case
475+
JwtUtil.getIssuer(jwtToken).getOrElse("")
476+
}
467477
}
468478
}
469479

@@ -473,7 +483,7 @@ object OAuth2Login extends RestHelper with MdcLoggable {
473483
* Unique criteria to decide do we create or get a consumer is pair o values: < sub : azp > i.e.
474484
* We cannot find consumer by sub and azp => Create
475485
* We can find consumer by sub and azp => Get
476-
* @param idToken Google's response example:
486+
* @param jwtToken Google's response example:
477487
* {
478488
* "access_token": "ya29.GluUBg5DflrJciFikW5hqeKEp9r1whWnU5x2JXCm9rKkRMs2WseXX8O5UugFMDsIKuKCZlE7tTm1fMII_YYpvcMX6quyR5DXNHH8Lbx5TrZN__fA92kszHJEVqPc",
479489
* "id_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6IjA4ZDMyNDVjNjJmODZiNjM2MmFmY2JiZmZlMWQwNjk4MjZkZDFkYzEiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJhenAiOiI0MDc0MDg3MTgxOTIuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJhdWQiOiI0MDc0MDg3MTgxOTIuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJzdWIiOiIxMTM5NjY4NTQyNDU3ODA4OTI5NTkiLCJlbWFpbCI6Im1hcmtvLm1pbGljLnNyYmlqYUBnbWFpbC5jb20iLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwiYXRfaGFzaCI6Im5HS1JUb0tOblZBMjhINk1od1hCeHciLCJuYW1lIjoiTWFya28gTWlsacSHIiwicGljdHVyZSI6Imh0dHBzOi8vbGg1Lmdvb2dsZXVzZXJjb250ZW50LmNvbS8tWGQ0NGhuSjZURG8vQUFBQUFBQUFBQUkvQUFBQUFBQUFBQUEvQUt4cndjYWR3emhtNE40dFdrNUU4QXZ4aS1aSzZrczRxZy9zOTYtYy9waG90by5qcGciLCJnaXZlbl9uYW1lIjoiTWFya28iLCJmYW1pbHlfbmFtZSI6Ik1pbGnEhyIsImxvY2FsZSI6ImVuIiwiaWF0IjoxNTQ3NzA1NjkxLCJleHAiOjE1NDc3MDkyOTF9.iUxhF_SU2vi76zPuRqAKJvFOzpb_EeP3lc5u9FO9o5xoXzVq3QooXexTfK2f1YAcWEy9LSftA34PB0QTuCZpkQChZVM359n3a3hplf6oWWkBXZN2_IG10NwEH4g0VVBCsjWBDMp6lvepN_Zn15x8opUB7272m4-smAou_WmUPTeivXRF8yPcp4J55DigcY31YP59dMQr2X-6Rr1vCRnJ6niqqJ1UDldfsgt4L7dXmUCnkDdXHwEQAZwbKbR4dUoEha3QeylCiBErmLdpIyqfKECphC6piGXZB-rRRqLz41WNfuF-3fswQvGmIkzTJDR7lQaletMp7ivsfVw8N5jFxg",
@@ -484,13 +494,13 @@ object OAuth2Login extends RestHelper with MdcLoggable {
484494
* }
485495
* @return an existing or a new consumer
486496
*/
487-
def getOrCreateConsumer(idToken: String, userId: Box[String], description: Option[String]): Box[Consumer] = {
488-
val aud = Some(JwtUtil.getAudience(idToken).mkString(","))
489-
val azp = getClaim(name = "azp", idToken = idToken)
490-
val iss = getClaim(name = "iss", idToken = idToken)
491-
val sub = getClaim(name = "sub", idToken = idToken)
492-
val email = getClaim(name = "email", idToken = idToken)
493-
val name = getClaim(name = "name", idToken = idToken).orElse(description)
497+
def getOrCreateConsumer(jwtToken: String, userId: Box[String], description: Option[String]): Box[Consumer] = {
498+
val aud = Some(JwtUtil.getAudience(jwtToken).mkString(","))
499+
val azp = getClaim(name = "azp", jwtToken = jwtToken)
500+
val iss = getClaim(name = "iss", jwtToken = jwtToken)
501+
val sub = getClaim(name = "sub", jwtToken = jwtToken)
502+
val email = getClaim(name = "email", jwtToken = jwtToken)
503+
val name = getClaim(name = "name", jwtToken = jwtToken).orElse(description)
494504
val consumerId = if(APIUtil.checkIfStringIsUUID(azp.getOrElse(""))) azp else Some(s"{$azp}_${APIUtil.generateUUID()}")
495505
Consumers.consumers.vend.getOrCreateConsumer(
496506
consumerId = consumerId, // Use azp as consumer id if it is uuid value
@@ -679,7 +689,7 @@ object OAuth2Login extends RestHelper with MdcLoggable {
679689
val sourceOfTruth = APIUtil.getPropsAsBoolValue(nameOfProperty = "oauth2.keycloak.source_of_truth", defaultValue = false)
680690
// Consumers allowed to use the source of truth feature
681691
val resourceAccessName = APIUtil.getPropsValue(nameOfProperty = "oauth2.keycloak.resource_access_key_name_to_trust", "open-bank-project")
682-
val consumerId = getClaim(name = "azp", idToken = token).getOrElse("")
692+
val consumerId = getClaim(name = "azp", jwtToken = token).getOrElse("")
683693
if(sourceOfTruth) {
684694
logger.debug("Extracting roles from Access Token")
685695
import net.liftweb.json._

obp-api/src/main/scala/code/api/berlin/group/v1_3/PaymentInitiationServicePISApi.scala

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -127,10 +127,18 @@ or * access method is generally applicable, but further authorisation processes
127127
(ibanChecker, callContext) <- NewStyle.function.validateAndCheckIbanNumber(toAccountIban, callContext)
128128
_ <- Helper.booleanToFuture(invalidIban, cc=callContext) { ibanChecker.isValid == true }
129129
(_, callContext) <- NewStyle.function.getToBankAccountByIban(toAccountIban, callContext)
130+
// Check payment status and determine if cancellation is allowed
131+
currentStatus = transactionRequest.status.toUpperCase()
132+
mappedStatus = mapTransactionStatus(currentStatus)
130133
(canBeCancelled, _, startSca) <- transactionRequestTypes match {
131134
case TransactionRequestTypes.SEPA_CREDIT_TRANSFERS => {
132-
transactionRequest.status.toUpperCase() match {
133-
case TransactionStatus.ACCP.code =>
135+
currentStatus match {
136+
case TransactionStatus.RCVD.code | "INITIATED" =>
137+
// INITIATED status (maps to RCVD externally) - direct cancellation
138+
NewStyle.function.saveTransactionRequestStatusImpl(transactionRequest.id, CANCELLED.toString, callContext)
139+
Future(true, callContext, Some(false))
140+
case TransactionStatus.ACCP.code | "COMPLETED" =>
141+
// ACCP status - may require SCA for cancellation
134142
NewStyle.function.cancelPaymentV400(TransactionId(transactionRequest.transaction_ids), callContext) map {
135143
x => x._1 match {
136144
case CancelPayment(true, Some(startSca)) if startSca == true =>
@@ -143,15 +151,33 @@ or * access method is generally applicable, but further authorisation processes
143151
(false, x._2, Some(false))
144152
}
145153
}
146-
case "INITIATED" =>
147-
NewStyle.function.saveTransactionRequestStatusImpl(transactionRequest.id, CANCELLED.toString, callContext)
148-
Future(true, callContext, Some(false))
149-
case "CANCELLED" =>
154+
case TransactionStatus.PDNG.code | "PENDING" =>
155+
// PENDING status (maps to PDNG externally) - may require SCA
156+
NewStyle.function.cancelPaymentV400(TransactionId(transactionRequest.transaction_ids), callContext) map {
157+
x => x._1 match {
158+
case CancelPayment(true, Some(startSca)) if startSca == true =>
159+
NewStyle.function.saveTransactionRequestStatusImpl(transactionRequest.id, CANCELLATION_PENDING.toString, callContext)
160+
(true, x._2, Some(startSca))
161+
case CancelPayment(true, Some(startSca)) if startSca == false =>
162+
NewStyle.function.saveTransactionRequestStatusImpl(transactionRequest.id, CANCELLED.toString, callContext)
163+
(true, x._2, Some(startSca))
164+
case CancelPayment(false, _) =>
165+
(false, x._2, Some(false))
166+
}
167+
}
168+
case TransactionStatus.CANC.code | "CANCELLED" =>
169+
// Already cancelled - return success
150170
Future(true, callContext, Some(false))
171+
case _ =>
172+
// Other statuses cannot be cancelled
173+
Future(false, callContext, Some(false))
151174
}
152175
}
153176
}
154-
_ <- Helper.booleanToFuture(failMsg= TransactionRequestCannotBeCancelled, cc=callContext) { canBeCancelled == true }
177+
_ <- Helper.booleanToFuture(
178+
failMsg = s"$TransactionRequestCannotBeCancelled Payment status: $mappedStatus. Only payments in RCVD, ACCP, PDNG, or CANC status can be cancelled.",
179+
cc = callContext
180+
) { canBeCancelled == true }
155181
(updatedTransactionRequest, callContext) <- NewStyle.function.getTransactionRequestImpl(TransactionRequestId(paymentId), callContext)
156182
} yield {
157183
startSca.getOrElse(false) match {

obp-api/src/main/scala/code/api/berlin/group/v1_3/model/TransactionStatus.scala

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,8 @@ object TransactionStatus extends ApiModel {
9999
status match {
100100
case "COMPLETED" => TransactionStatus.ACCP.code
101101
case "INITIATED" => TransactionStatus.RCVD.code
102+
case "PENDING" => TransactionStatus.PDNG.code
103+
case "CANCELLED" => TransactionStatus.CANC.code
102104
case other => other
103105
}
104106
}

obp-api/src/main/scala/code/api/openidconnect.scala

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -215,7 +215,8 @@ object OpenIdConnect extends OBPRestHelper with MdcLoggable {
215215
private def getOrCreateResourceUser(idToken: String): Box[User] = {
216216
val uniqueIdGivenByProvider = JwtUtil.getSubject(idToken)
217217
val preferredUsername = JwtUtil.getOptionalClaim("preferred_username", idToken)
218-
val provider = Hydra.resolveProvider(idToken)
218+
// Try to get provider from token first, fallback to Hydra resolver
219+
val provider = JwtUtil.getProvider(idToken).getOrElse(Hydra.resolveProvider(idToken))
219220
val providerId = preferredUsername.orElse(uniqueIdGivenByProvider)
220221
Users.users.vend.getUserByProviderId(provider = provider, idGivenByProvider = providerId.getOrElse("")).or { // Find a user
221222
Users.users.vend.createResourceUser( // Otherwise create a new one

obp-api/src/main/scala/code/api/util/JwtUtil.scala

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,15 @@ object JwtUtil extends MdcLoggable {
182182
}
183183
}
184184

185+
/**
186+
* Get the provider claim from the JWT token
187+
* @param jwtToken JSON Web Token (JWT) as a String value
188+
* @return The provider value or None if not available
189+
*/
190+
def getProvider(jwtToken: String): Option[String] = {
191+
getOptionalClaim("provider", jwtToken)
192+
}
193+
185194
/**
186195
* The Issuer Identifier for the Issuer of the response.
187196
* Get the value of the "iss" claim, or None if it's not available.

obp-api/src/main/scala/code/bankconnectors/LocalMappedConnector.scala

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2450,7 +2450,23 @@ object LocalMappedConnector extends Connector with MdcLoggable {
24502450

24512451
override def cancelPaymentV400(transactionId: TransactionId,
24522452
callContext: Option[CallContext]): OBPReturnType[Box[CancelPayment]] = Future {
2453-
(Full(CancelPayment(true, Some(true))), callContext)
2453+
// Get transaction to determine if SCA is needed based on amount
2454+
val transaction = MappedTransaction.find(By(MappedTransaction.transactionId, transactionId.value))
2455+
2456+
val startSca = transaction match {
2457+
case Full(t) =>
2458+
// Decide based on amount (similar to real CBS logic)
2459+
// Small amounts (<=100) don't need SCA, large amounts (>100) do
2460+
// Convert from smallest currency unit (cents) to actual decimal amount
2461+
val amount = Helper.smallestCurrencyUnitToBigDecimal(t.amount.get, t.currency.get).abs
2462+
val threshold = 100
2463+
Some(amount > threshold)
2464+
case _ =>
2465+
// If transaction not found, default to no SCA required
2466+
Some(false)
2467+
}
2468+
2469+
(Full(CancelPayment(canBeCancelled = true, startSca = startSca)), callContext)
24542470
}
24552471

24562472
override def saveTransactionRequestStatusImpl(transactionRequestId: TransactionRequestId, status: String, callContext: Option[CallContext]): OBPReturnType[Box[Boolean]] =

0 commit comments

Comments
 (0)