Skip to content

Commit 46af397

Browse files
committed
Open Corridor promise
1 parent e61b456 commit 46af397

10 files changed

Lines changed: 187 additions & 24 deletions

File tree

CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,7 @@ Note: `executeFutureCreated` returns 201; pair it with `cc.user.openOrThrowExcep
241241

242242
**Use `NEW_ACCOUNT_ID` for PUT-creates-account URLs**: When a `PUT /banks/BANK_ID/accounts/ACCOUNT_ID` *creates* the account (it doesn't exist yet), the middleware's `validateAccount` keys off the literal `ACCOUNT_ID` template var and tries to look it up → 404 before the handler runs. Change the ResourceDoc URL template to `/banks/BANK_ID/accounts/NEW_ACCOUNT_ID` (or any non-standard ALL_CAPS variant) — middleware treats it as a wildcard and skips the lookup, but the path still matches the route pattern. The handler can check "already exists" inline with `Connector.connector.vend.checkBankAccountExists(...)` and return 409/400 as needed.
243243

244-
**Reserved ALL_CAPS literals — don't use them as placeholders**: `ResourceDocMatcher` in `Http4sSupport.scala` keeps an explicit `literalAllCapsSegments` set: `SANDBOX_TAN`, `COUNTERPARTY`, `SEPA`, `FREE_FORM`, `ACCOUNT`, `ACCOUNT_OTP`, `REFUND`, `SIMPLE`, `AGENT_CASH_WITHDRAWAL`, `CARD`, `EMAIL`, `SMS`, `IMPLICIT`, `NOT_EMAIL_NEITHER_SMS`. These are matched as **literals** (real Lift endpoints register them as concrete SCA-method / transaction-request-type segments — e.g. `/banks/BANK_ID/my/consents/EMAIL`). Any other ALL_CAPS segment is a wildcard. If you migrate an endpoint whose URL template uses one of these names as a *placeholder variable* (e.g. v3.0/v4.0 `getUsersByEmail` had `/users/email/EMAIL/terminator` with EMAIL meaning "any email value"), the matcher will only fire when the URL segment is literally `EMAIL` — real callers pass actual addresses and miss the doc entirely → middleware skips auth/role validation → handler 500s on the empty CallContext. Rename the placeholder to something outside the literal set (e.g. `EMAIL` → `USER_EMAIL`), and apply the rename in **both** the http4s `ResourceDoc` and the original Lift `ResourceDoc` (resource-docs aggregation reads both, and `collectResourceDocs` dedup keys off URL + verb).
244+
**Reserved ALL_CAPS literals — don't use them as placeholders**: `ResourceDocMatcher` in `Http4sSupport.scala` keeps an explicit `literalAllCapsSegments` set: `SANDBOX_TAN`, `COUNTERPARTY`, `SEPA`, `FREE_FORM`, `ACCOUNT`, `ACCOUNT_OTP`, `REFUND`, `SIMPLE`, `AGENT_CASH_WITHDRAWAL`, `CARD`, `OPEN_CORRIDOR_PROMISE`, `OPEN_CORRIDOR_SETTLEMENT`, `EMAIL`, `SMS`, `IMPLICIT`, `NOT_EMAIL_NEITHER_SMS`. These are matched as **literals** (real Lift endpoints register them as concrete SCA-method / transaction-request-type segments — e.g. `/banks/BANK_ID/my/consents/EMAIL`). Any other ALL_CAPS segment is a wildcard. If you migrate an endpoint whose URL template uses one of these names as a *placeholder variable* (e.g. v3.0/v4.0 `getUsersByEmail` had `/users/email/EMAIL/terminator` with EMAIL meaning "any email value"), the matcher will only fire when the URL segment is literally `EMAIL` — real callers pass actual addresses and miss the doc entirely → middleware skips auth/role validation → handler 500s on the empty CallContext. Rename the placeholder to something outside the literal set (e.g. `EMAIL` → `USER_EMAIL`), and apply the rename in **both** the http4s `ResourceDoc` and the original Lift `ResourceDoc` (resource-docs aggregation reads both, and `collectResourceDocs` dedup keys off URL + verb).
245245

246246
**Bypass roles vs required roles**: Some Lift handlers check entitlements inline as **bypass** conditions inside authorisation helpers — e.g. `checkAuthorisationToCreateTransactionRequest` honours `canCreateAnyTransactionRequest` to let the caller skip the view-permission check, but the role is never a hard requirement. These roles are correctly absent from the Lift ResourceDoc role list — putting them in the doc would make Lift enforce them as required (since Lift DOES enforce doc roles by default), breaking the "view permission OR role" intent. The same holds for http4s middleware. So the trap on migration is the reflex copy: don't move a bypass role from inline-only into `Some(List(...))` just because it appears in the handler. Audit before copying: if the role appears in the Lift handler only inside an authorisation OR-chain ("has view permission OR has role X"), it belongs as `None` in the doc with the inline view/role logic preserved. Bypass roles must stay out of the doc.
247247

obp-api/src/main/scala/code/api/util/http4s/Http4sSupport.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -741,6 +741,7 @@ object ResourceDocMatcher extends code.util.Helper.MdcLoggable {
741741
"SANDBOX_TAN", "COUNTERPARTY", "SEPA", "FREE_FORM",
742742
"ACCOUNT", "ACCOUNT_OTP", "REFUND", "SIMPLE",
743743
"AGENT_CASH_WITHDRAWAL", "CARD",
744+
"OPEN_CORRIDOR_PROMISE", "OPEN_CORRIDOR_SETTLEMENT",
744745
// SCA methods (POST /banks/BANK_ID/my/consents/{EMAIL|SMS|IMPLICIT})
745746
"EMAIL", "SMS", "IMPLICIT", "NOT_EMAIL_NEITHER_SMS"
746747
)

obp-api/src/main/scala/code/api/v7_0_0/Http4s700.scala

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3175,14 +3175,14 @@ object Http4s700 {
31753175

31763176
// ── End UTILITY ───────────────────────────────────────────────────────────
31773177

3178-
// ── OPEN_CORRIDOR transaction request ─────────────────────────────────────
3178+
// ── OPEN_CORRIDOR_PROMISE transaction request ─────────────────────────────
31793179
// Travel-Rule-friendly TR with FATF Recommendation 16 originator block.
31803180
// Money-movement is identical to SIMPLE; the originator is persisted as a
31813181
// side-car on the TR row and surfaced on the v7 response. Lives natively at
31823182
// v7 (rather than bridging to v4) because only v7's response shape carries
31833183
// the originator block.
31843184
val createTransactionRequestOpenCorridor: HttpRoutes[IO] = HttpRoutes.of[IO] {
3185-
case req @ POST -> `prefixPath` / "banks" / _ / "accounts" / _ / _ / "transaction-request-types" / "OPEN_CORRIDOR" / "transaction-requests" =>
3185+
case req @ POST -> `prefixPath` / "banks" / _ / "accounts" / _ / _ / "transaction-request-types" / "OPEN_CORRIDOR_PROMISE" / "transaction-requests" =>
31863186
EndpointHelpers.withViewAndBodyCreated[JSONFactory700.TransactionRequestBodyOpenCorridorJsonV700, JSONFactory700.TransactionRequestWithChargeOpenCorridorJsonV700](req) { (user, fromAccount, view, body, cc) =>
31873187
val callCtx = Some(cc)
31883188
for {
@@ -3211,7 +3211,7 @@ object Http4s700 {
32113211
other_branch_routing_address = ""
32123212
),
32133213
value = com.openbankproject.commons.model.AmountOfMoneyJsonV121(currency = "EUR", amount = "100.00"),
3214-
description = "OPEN_CORRIDOR Travel-Rule payment",
3214+
description = "Open Corridor Travel-Rule payment",
32153215
charge_policy = "SHARED",
32163216
originator = com.openbankproject.commons.model.TransactionRequestOriginator(
32173217
name = "Alice Sender",
@@ -3228,17 +3228,17 @@ object Http4s700 {
32283228
implementedInApiVersion,
32293229
nameOf(createTransactionRequestOpenCorridor),
32303230
"POST",
3231-
"/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/transaction-request-types/OPEN_CORRIDOR/transaction-requests",
3232-
"Create Transaction Request (OPEN_CORRIDOR)",
3233-
"""Initiate an OPEN_CORRIDOR Transaction Request — a Travel-Rule-friendly payment that carries FATF Recommendation 16 originator information about the actual payer.
3231+
"/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/transaction-request-types/OPEN_CORRIDOR_PROMISE/transaction-requests",
3232+
"Create Transaction Request (OPEN_CORRIDOR_PROMISE)",
3233+
"""Initiate an OPEN_CORRIDOR_PROMISE Transaction Request — an Open Corridor Travel-Rule-friendly payment that carries FATF Recommendation 16 originator information about the actual payer.
32343234
|
32353235
|Money-movement is identical to the SIMPLE transaction request type (same beneficiary routing fields). What's distinct: the `originator` block is mandatory and is persisted alongside the transaction request. The v7 response includes a populated originator block.
32363236
|
32373237
|Authentication is Required.""".stripMargin,
32383238
openCorridorBodyExample,
32393239
JSONFactory700.TransactionRequestWithChargeOpenCorridorJsonV700(
32403240
id = "4050046c-63b3-4868-8a22-14b4181d33a6",
3241-
`type` = "OPEN_CORRIDOR",
3241+
`type` = "OPEN_CORRIDOR_PROMISE",
32423242
from = code.api.v1_4_0.JSONFactory1_4_0.TransactionRequestAccountJsonV140(
32433243
bank_id = "gh.29.uk",
32443244
account_id = "8ca8a7e4-6d02-40e3-a129-0b2bf89de9f1"
@@ -3271,7 +3271,7 @@ object Http4s700 {
32713271
http4sPartialFunction = Some(createTransactionRequestOpenCorridor)
32723272
)
32733273

3274-
// ── End OPEN_CORRIDOR ─────────────────────────────────────────────────────
3274+
// ── End OPEN_CORRIDOR_PROMISE ─────────────────────────────────────────────
32753275

32763276
// ── BULK transaction request ──────────────────────────────────────────────
32773277
// One TransactionRequest with type=BULK serves as the envelope; N actual

obp-api/src/main/scala/code/api/v7_0_0/JSONFactory7.0.0.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1011,7 +1011,7 @@ object JSONFactory700 extends MdcLoggable with code.api.util.CustomJsonFormats {
10111011
)
10121012
}
10131013

1014-
// ─── OPEN_CORRIDOR Transaction Request type ────────────────────────────────
1014+
// ─── OPEN_CORRIDOR_PROMISE Transaction Request type ────────────────────────
10151015
//
10161016
// SIMPLE-shaped beneficiary routing plus a REQUIRED `originator` block carrying
10171017
// FATF Recommendation 16 (Travel Rule) information about the actual payer. The
@@ -1042,7 +1042,7 @@ object JSONFactory700 extends MdcLoggable with code.api.util.CustomJsonFormats {
10421042
address: String
10431043
)
10441044

1045-
// OPEN_CORRIDOR response wrapper — v4 TransactionRequestWithChargeJSON400 shape
1045+
// OPEN_CORRIDOR_PROMISE response wrapper — v4 TransactionRequestWithChargeJSON400 shape
10461046
// plus the originator block. `originator` is None when there's no explicit value
10471047
// stored AND no customer_account_link for the from-account; serializes as null.
10481048
case class TransactionRequestWithChargeOpenCorridorJsonV700(

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5259,12 +5259,12 @@ object LocalMappedConnector extends Connector with MdcLoggable {
52595259
} yield {
52605260
(transactionId, callContext)
52615261
}
5262-
// OPEN_CORRIDOR: money-movement is identical to SIMPLE today (same beneficiary
5262+
// OPEN_CORRIDOR_PROMISE: money-movement is identical to SIMPLE today (same beneficiary
52635263
// routing shape). The originator block is persisted on the TR row by
52645264
// MappedTransactionRequestProvider and surfaced on v7 responses by
52655265
// JSONFactory700.buildTransactionRequestOriginatorJson — neither is needed here.
52665266
// Reuses the SIMPLE path via body.to_simple.
5267-
case OPEN_CORRIDOR =>
5267+
case OPEN_CORRIDOR_PROMISE =>
52685268
for {
52695269
bodyToSimple <- NewStyle.function.tryons(s"$TransactionRequestDetailsExtractException It can not extract to $TransactionRequestBodyCounterpartyJSON", 400, callContext) {
52705270
body.to_simple.get

obp-api/src/main/scala/code/bankconnectors/opencorridor/OpenCorridorProcessor.scala

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,16 @@ import net.liftweb.util.StringHelpers
1717

1818
import scala.concurrent.Future
1919

20-
// OPEN_CORRIDOR Transaction Request — Travel-Rule-friendly payment.
20+
// OPEN_CORRIDOR_PROMISE Transaction Request — Travel-Rule-friendly payment.
2121
//
2222
// Money-movement is identical to SIMPLE today (same beneficiary routing shape).
2323
// What's distinct is the mandatory `originator` block, persisted alongside the TR
24-
// and surfaced on v7 responses. Lives in its own package so future OPEN_CORRIDOR
24+
// and surfaced on v7 responses. Lives in its own package so future Open Corridor
2525
// extensions (Cardano Promise, netting, settlement) have a home that does not bloat
2626
// `LocalMappedConnector`.
2727
object OpenCorridorProcessor {
2828

29-
// Create an OPEN_CORRIDOR Transaction Request: validate the originator block,
29+
// Create an OPEN_CORRIDOR_PROMISE Transaction Request: validate the originator block,
3030
// resolve the destination counterparty (via the same getOrCreateCounterparty
3131
// path SIMPLE uses), persist the TR with the originator side-car, and return it.
3232
def create(
@@ -39,7 +39,7 @@ object OpenCorridorProcessor {
3939
callContext: Option[CallContext]
4040
): Future[(TransactionRequest, Option[CallContext])] = {
4141

42-
val transactionRequestType = TransactionRequestType("OPEN_CORRIDOR")
42+
val transactionRequestType = TransactionRequestType("OPEN_CORRIDOR_PROMISE")
4343

4444
for {
4545
_ <- Helper.booleanToFuture(s"$InvalidJsonValue originator.name must be non-empty", cc = callContext) {

obp-api/src/main/scala/code/transactionrequests/MappedTransactionRequestProvider.scala

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ object MappedTransactionRequestProvider extends TransactionRequestProvider with
104104
val consentOption = consentIdOption.map(consentId =>Consents.consentProvider.vend.getConsentByConsentId(consentId).toOption).flatten
105105
val consentReferenceIdOption = consentOption.map(_.consentReferenceId)
106106

107-
// Explicit originator (FATF Rec 16). Only the OPEN_CORRIDOR body carries this today;
107+
// Explicit originator (FATF Rec 16). Only the OPEN_CORRIDOR_PROMISE body carries this today;
108108
// other TR types leave the columns null.
109109
val explicitOriginator: Option[TransactionRequestOriginator] = transactionRequestCommonBody match {
110110
case openCorridorBody: TransactionRequestBodyOpenCorridorJsonV700 => Some(openCorridorBody.originator)
@@ -168,7 +168,7 @@ object MappedTransactionRequestProvider extends TransactionRequestProvider with
168168
.mUserId(callContext.flatMap(_.user.map(_.userId)).getOrElse(null))
169169
.mOnBehalfOfUserId(callContext.flatMap(cc => cc.onBehalfOfUser.or(cc.consenter).map(_.userId)).getOrElse(null))
170170

171-
// Explicit originator fields (FATF Rec 16, OPEN_CORRIDOR type only — null otherwise).
171+
// Explicit originator fields (FATF Rec 16, OPEN_CORRIDOR_PROMISE type only — null otherwise).
172172
.mOriginator_Name(explicitOriginator.map(_.name).getOrElse(null))
173173
.mOriginator_Address(explicitOriginator.map(_.address).getOrElse(null))
174174
.mOriginator_AccountRoutingScheme(explicitOriginator.map(_.account_routing.scheme).getOrElse(null))
@@ -272,7 +272,7 @@ class MappedTransactionRequest extends LongKeyedMapper[MappedTransactionRequest]
272272
object mIsBeneficiary extends MappedBoolean(this)
273273

274274
// Originator fields (FATF Recommendation 16 "Travel Rule" — who the payment is from).
275-
// Populated only for OPEN_CORRIDOR Transaction Requests. For other TR types these are null
275+
// Populated only for OPEN_CORRIDOR_PROMISE Transaction Requests. For other TR types these are null
276276
// and the v7 JSON response layer can virtually fill from customer_account_link.
277277
object mOriginator_Name extends MappedString(this, 140)
278278
object mOriginator_Address extends MappedString(this, 2000)
@@ -344,10 +344,10 @@ class MappedTransactionRequest extends LongKeyedMapper[MappedTransactionRequest]
344344
else
345345
None
346346

347-
// OPEN_CORRIDOR's persisted body has the same `to: PostSimpleCounterpartyJson400` shape
347+
// OPEN_CORRIDOR_PROMISE's persisted body has the same `to: PostSimpleCounterpartyJson400` shape
348348
// as SIMPLE, so we reuse this SIMPLE branch's JSON-field extraction.
349349
val t_to_simple = if ((TransactionRequestTypes.withName(transactionType) == TransactionRequestTypes.SIMPLE ||
350-
TransactionRequestTypes.withName(transactionType) == TransactionRequestTypes.OPEN_CORRIDOR) && details.nonEmpty){
350+
TransactionRequestTypes.withName(transactionType) == TransactionRequestTypes.OPEN_CORRIDOR_PROMISE) && details.nonEmpty){
351351
val transactionRequestSimples = for {
352352
JObject(child) <- parsedDetails
353353
JField("other_bank_routing_scheme", JString(otherBankRoutingScheme)) <- child

0 commit comments

Comments
 (0)