Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
0e04357
feat(v6.0.0): Phase 0 — Http4s600 skeleton + override audit
constantine2nd May 14, 2026
d0c0685
feat(v6.0.0): Phase 1 starter — migrate root + wire Http4s600 into chain
constantine2nd May 15, 2026
1e38856
feat(v6.0.0): migrate 14 more override endpoints to http4s (15 of 35 …
constantine2nd May 15, 2026
934194a
feat(v6.0.0): migrate getCustomerByCustomerNumber and getCustomersByL…
constantine2nd May 15, 2026
b11227f
feat(v6.0.0): migrate 5 dynamic-entity mutations + updateSystemView (…
constantine2nd May 15, 2026
fc1b471
feat(v6.0.0): migrate the remaining 12 override endpoints (35/35 done)
constantine2nd May 15, 2026
8af3c38
Merge origin/develop into v6.0.0 migration work
constantine2nd May 15, 2026
7017e6a
feat(v6.0.0): Phase 2 starter — migrate 8 system bucket originals
constantine2nd May 15, 2026
15129c7
feat(v6.0.0): Phase 2 batch 2 — migrate 10 mandate endpoints
constantine2nd May 15, 2026
aa857d0
feat(v6.0.0): Phase 2 batch 3 — migrate 9 api-product endpoints
constantine2nd May 15, 2026
70138ba
fix(v6.0.0): DynamicEntityTest — 2 of 4 fixes from CI shard 2
constantine2nd May 15, 2026
9bb427a
feat(v6.0.0): Phase 2 batch 4 — migrate 4 featured-api-collection end…
constantine2nd May 15, 2026
e69c843
feat(v6.0.0): Phase 2 batch — 34 endpoints + DynamicEntityTest fixes
constantine2nd May 15, 2026
e3828e5
feat(v6.0.0): Phase 2 batch — 44 endpoints across customer/product/ac…
constantine2nd May 15, 2026
bf3f0cc
fix(v6.0.0): CI failures across 6 v6 suites from prior batch
constantine2nd May 15, 2026
8fd4051
feat(v6.0.0): Phase 2 batch — 41 endpoints across user-auth/access-re…
constantine2nd May 15, 2026
c0c1853
feat(v6.0.0): Phase 2 batch — 90 endpoints, reaches ~95.7% (199/208)
constantine2nd May 15, 2026
c4796e7
feat(v6.0.0): Phase 2 final batch — 9 endpoints, 100% of v6 originals…
constantine2nd May 15, 2026
7659410
fix(v6.0.0): DirectLogin / WebUiProps / RetailCustomer test failures …
constantine2nd May 15, 2026
f561aaf
feat(v3.1.0): migrate updateCustomerAddress to Http4s310
constantine2nd May 16, 2026
d070005
WIP(v4.0.0): checkpoint — 108 endpoints migrated to Http4s400 (164/25…
constantine2nd May 16, 2026
5895ecf
fix(v4.0.0): CI shard1 test failures (13 tests across 9 suites)
constantine2nd May 16, 2026
f7e551a
feat(v4.0.0): migrate 50 more endpoints to Http4s400 (176/258, 68%)
constantine2nd May 16, 2026
0281423
feat(v4.0.0): migrate final 43 endpoints — v4 100% on http4s (258/258)
constantine2nd May 16, 2026
305bfd9
fix(http4s): keep raw 400 for Old-Style versions in ErrorResponseConv…
constantine2nd May 16, 2026
669171d
Merge remote-tracking branch 'upstream/develop' into develop
constantine2nd May 18, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 11 additions & 13 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -342,23 +342,21 @@ Per-endpoint integration test cost stays roughly constant as endpoints move Lift

## TODO / Phase Progress

### Phase 1 — Simple GETs (98 remaining in v6.0.0)
GET + no body. Purely mechanical — 1:1 copy of `NewStyle.function.*` calls, pick helper from Rule 4 matrix, 3 test scenarios per endpoint (401 / 403 / 200).
### Per-version completeness (from `comm -23 lift http4s` on each version's `lazy val ... : OBPEndpoint` declarations)

| Batch | Endpoints | Status |
|---|---|---|
| Batches 1–3 | 9 endpoints | ✓ done |
| Batch 4 | getCacheConfig, getCacheInfo, getDatabasePoolInfo, getStoredProcedureConnectorHealth, getMigrations, getCacheNamespaces | ✓ done |
| Remaining | 98 GETs | todo |
| Version | Genuine Lift handlers still on the bridge |
|---|---|
| v1.2.1, v1.3.0, v2.0.0, v2.1.0, v2.2.0, v3.0.0, v4.0.0, v5.0.0, v5.1.0, v6.0.0 | 0 — fully on http4s |
| v1.4.0 | `testResourceDoc` (dev-mode-only stub; tracked in "Per-version Lift leftovers") |
| v3.1.0 | `getMessageDocsSwagger`, `getObpConnectorLoopback` (both tracked as leftovers; retire via Resource-docs / bridge-removal workstreams) |

### Phase 2Account/View/Counterparty GETs (subset of the 98 above)
`withBankAccount` / `withView` / `withCounterparty` helpers ready. Same mechanical pattern.
### v6.0.0 migrationdone (243 / 243)
Phase 1 (35 overrides) and Phase 2 (208 originals) both complete. All v6 routes live in `Http4s600.scala`, wired into `Http4sApp.baseServices` ahead of the Lift bridge.

### Phase 3 — POST / PUT / DELETE (57 + 33 + 26 = 116 endpoints in v6.0.0)
Body helpers and DELETE 204 helpers ready. Velocity: 6–8 endpoints/day.
Architectural note from the v6 migration: around the 140-endpoint mark `Implementations6_0_0`'s `<init>` hit the JVM 64KB bytecode-per-method limit. The fix that ships in `Http4s600.scala` — and that future per-version files should adopt — is two-part:

### Phase 4 — Complex endpoints (~50 endpoints)
Dynamic entities, ABAC rules, mandate workflows, polymorphic bodies. ~45–60 min each.
1. Declare endpoints as `lazy val xxx: HttpRoutes[IO] = HttpRoutes.of[IO] { ... }` instead of `val`. Lambda materialisation moves out of `<init>` into per-field `lzycompute` methods (each with its own 64KB budget).
2. Group `resourceDocs += ResourceDoc(...)` calls into `private def initXxxResourceDocs(): Unit` blocks of ~10–15 endpoints, called once each from the object body. Each helper def gets its own 64KB.

### Other TODOs
- **OBP-Trading**: trading endpoints (createTradingOffer, getTradingOffer, getTradingOffers, cancelTradingOffer, createMarketOrder, getMarketOrder, cancelMarketOrder, createMarketMatch, getMarketTrade, requestSettlement, requestWithdrawal) are now in `Http4s700.scala`. 5 payment-auth endpoints remain commented out (notifyDeposit, createPaymentAuth, capturePaymentAuth, releasePaymentAuth, getPaymentAuth) — see `ideas/CAPTURE_RELEASE_TRANSACTION_REQUEST_TYPES.md`.
Expand Down
140 changes: 119 additions & 21 deletions LIFT_HTTP4S_MIGRATION.md

Large diffs are not rendered by default.

650 changes: 650 additions & 0 deletions LIFT_HTTP4S_MIGRATION_V6_AUDIT.md

Large diffs are not rendered by default.

18 changes: 18 additions & 0 deletions obp-api/src/main/scala/code/api/util/ErrorMessages.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1029,6 +1029,24 @@ object ErrorMessages {
*/
def getCode(errorMsg: String): Int = errorToCode.get(errorMsg).getOrElse(400)

/**
* Resolve HTTP status code for an OBP-prefixed error message that may have a
* runtime suffix appended (e.g. "OBP-20020: User does not have access to the view.
* Current ViewId is owner"). Extracts the OBP-XXXXX prefix and returns the
* canonical status code from errorToCode, but ONLY when the canonical code is
* one of the auth-overrides Lift's `errorJsonResponse` performs (401, 403, 408,
* 429). For other codes (e.g. 404 BankNotFound) the default failCode the caller
* supplied wins — callers that want 404 must request it explicitly via
* `unboxFullOrFail(..., emptyBoxErrorCode = 404)` or similar, matching Lift's
* intent that 400 is a deliberate "validation failure" code.
*/
def getCodeByOBPPrefix(errorMsg: String): Int = {
val prefixOpt = "OBP-\\d{5}".r.findFirstIn(errorMsg)
prefixOpt.flatMap { prefix =>
errorToCode.find { case (key, _) => key.startsWith(prefix + ":") }.map(_._2)
}.filter(Set(401, 403, 408, 429).contains).getOrElse(400)
}

/****** special error message, start with $, mark as do validation according ResourceDoc errorResponseBodies *****/
/**
* validate method: APIUtil.authorizedAccess
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ object ErrorResponseConverter {
implicit val formats: Formats = CustomJsonFormats.formats
private val jsonContentType: `Content-Type` = `Content-Type`(MediaType.application.json)

private val obpErrorCodePrefix = "^OBP-\\d{5}: ".r

private def tryExtractApiFailureFromExceptionMessage(error: Throwable): Option[APIFailureNewStyle] = {
val msg = Option(error.getMessage).getOrElse("").trim
if (msg.startsWith("{") && msg.contains("\"failCode\"") && msg.contains("\"failMsg\"")) {
Expand All @@ -45,6 +47,9 @@ object ErrorResponseConverter {
} catch {
case _: Throwable => None
}
} else if (obpErrorCodePrefix.findFirstIn(msg).isDefined) {
// Plain Exception("OBP-XXXXX: ...") thrown by fullBoxOrException(Failure(msg)) — Lift treats these as 400.
Some(APIFailureNewStyle(msg, 400))
} else {
None
}
Expand Down Expand Up @@ -80,13 +85,31 @@ object ErrorResponseConverter {
}
}

/** Old-style versions keep raw 400 codes — they never promote to 403/401/etc.
* Mirrors the same set used in ResourceDocMiddleware.authenticate.
*/
private val oldStyleShortVersions = Set("v1.2.1", "v1.3.0", "v1.4.0", "v2.0.0")

/**
* Translate a 400 default with an OBP-prefixed message to the canonical status
* Lift assigns (403 for role/view-access codes, 401 for auth codes, etc.) via
* ErrorMessages.getCodeByOBPPrefix. Leaves non-400 failCodes (caller set
* status explicitly) untouched. Old-style versions (v1.x, v2.0.0) keep the
* 400 — they return 400 for every error per the long-standing OBP convention.
*/
private def resolveStatusCode(failCode: Int, failMsg: String, callContext: CallContext): Int =
if (failCode == 400 && !oldStyleShortVersions.contains(callContext.implementedInVersion))
code.api.util.ErrorMessages.getCodeByOBPPrefix(failMsg)
else failCode

/**
* Convert APIFailureNewStyle to http4s Response.
* Uses failCode as HTTP status and failMsg as error message.
*/
def apiFailureToResponse(failure: APIFailureNewStyle, callContext: CallContext): IO[Response[IO]] = {
val errorJson = OBPErrorResponse(failure.failCode, failure.failMsg)
val status = org.http4s.Status.fromInt(failure.failCode).getOrElse(org.http4s.Status.BadRequest)
val resolvedCode = resolveStatusCode(failure.failCode, failure.failMsg, callContext)
val errorJson = OBPErrorResponse(resolvedCode, failure.failMsg)
val status = org.http4s.Status.fromInt(resolvedCode).getOrElse(org.http4s.Status.BadRequest)
IO.pure(
Response[IO](status)
.withEntity(toJsonString(errorJson))
Expand Down
2 changes: 2 additions & 0 deletions obp-api/src/main/scala/code/api/util/http4s/Http4sApp.scala
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ object Http4sApp {
private val v400Routes: HttpRoutes[IO] = gate(ApiVersion.v4_0_0, code.api.v4_0_0.Http4s400.wrappedRoutesV400Services)
private val v500Routes: HttpRoutes[IO] = gate(ApiVersion.v5_0_0, code.api.v5_0_0.Http4s500.wrappedRoutesV500Services)
private val v510Routes: HttpRoutes[IO] = gate(ApiVersion.v5_1_0, code.api.v5_1_0.Http4s510.wrappedRoutesV510Services)
private val v600Routes: HttpRoutes[IO] = gate(ApiVersion.v6_0_0, code.api.v6_0_0.Http4s600.wrappedRoutesV600Services)
private val v700Routes: HttpRoutes[IO] = gate(ApiVersion.v7_0_0, code.api.v7_0_0.Http4s700.wrappedRoutesV700Services)

/**
Expand Down Expand Up @@ -106,6 +107,7 @@ object Http4sApp {
.orElse(AppsPage.routes.run(req))
.orElse(StatusPage.routes.run(req))
.orElse(v510Routes.run(req))
.orElse(v600Routes.run(req))
.orElse(v500Routes.run(req))
.orElse(v700Routes.run(req))
.orElse(code.api.berlin.group.v2.Http4sBGv2.wrappedRoutes.run(req))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -376,7 +376,11 @@ object ResourceDocMiddleware extends MdcLoggable {
case Full(user) =>
val bankId = pathParams.getOrElse("BANK_ID", "")
val consumerId = APIUtil.getConsumerPrimaryKey(Some(ctx.callContext))
val ok = APIUtil.handleAccessControlRegardingEntitlementsAndScopes(bankId, user.userId, consumerId, roles)
// Use handleAccessControlWithAuthMode so authMode = UserOrApplication
// accepts consumer-scope-only requests (the auth-only handleAccess...
// function checks user-entitlements + scopes ANDed and rejects pure
// consumer-scope requests with 403 even under UserOrApplication).
val ok = APIUtil.handleAccessControlWithAuthMode(bankId, user.userId, consumerId, roles, resourceDoc.authMode)
if (ok) success(ctx)
else EitherT[IO, Response[IO], ValidationContext](
ErrorResponseConverter.createErrorResponse(403, UserHasMissingRoles + roles.mkString(" or "), ctx.callContext)
Expand Down
34 changes: 34 additions & 0 deletions obp-api/src/main/scala/code/api/v3_1_0/Http4s310.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1706,6 +1706,39 @@ object Http4s310 {
List(apiTagCustomer), Some(List(canCreateCustomerAddress)),
http4sPartialFunction = Some(createCustomerAddress))

// ─── updateCustomerAddress (PUT) ─────────────────────────────────────────

val updateCustomerAddress: HttpRoutes[IO] = HttpRoutes.of[IO] {
case req @ PUT -> `prefixPath` / "banks" / _ / "customers" / customerIdStr / "addresses" / customerAddressIdStr =>
EndpointHelpers.withUserAndBankAndBody[PostCustomerAddressJsonV310, Any](req) { (user, bank, postedData, cc) =>
for {
_ <- NewStyle.function.hasEntitlement(bank.bankId.value, user.userId, canCreateCustomer, Some(cc))
(_, _) <- NewStyle.function.getCustomerByCustomerId(customerIdStr, Some(cc))
(address, _) <- NewStyle.function.updateCustomerAddress(
customerAddressIdStr,
postedData.line_1, postedData.line_2, postedData.line_3,
postedData.city, postedData.county, postedData.state,
postedData.postcode, postedData.country_code,
postedData.state,
postedData.tags.mkString(","),
Some(cc))
} yield JSONFactory310.createAddress(address)
}
}

resourceDocs += ResourceDoc(
null, implementedInApiVersion, nameOf(updateCustomerAddress), "PUT",
"/banks/BANK_ID/customers/CUSTOMER_ID/addresses/CUSTOMER_ADDRESS_ID",
"Update the Address of a Customer",
s"""Update an Address of the Customer specified by CUSTOMER_ADDRESS_ID.
|
|${userAuthenticationMessage(true)}
|""",
postCustomerAddressJsonV310, customerAddressJsonV310,
List(AuthenticatedUserIsRequired, UserHasMissingRoles, InvalidJsonFormat, UnknownError),
List(apiTagCustomer), Some(List(canCreateCustomer)),
http4sPartialFunction = Some(updateCustomerAddress))

// ─── createUserAuthContext (POST) ────────────────────────────────────────

val createUserAuthContext: HttpRoutes[IO] = HttpRoutes.of[IO] {
Expand Down Expand Up @@ -3581,6 +3614,7 @@ object Http4s310 {
.orElse(revokeConsent.run(req))
.orElse(createTaxResidence.run(req))
.orElse(createCustomerAddress.run(req))
.orElse(updateCustomerAddress.run(req))
.orElse(createUserAuthContext.run(req))
.orElse(createProductAttribute.run(req))
.orElse(createAccountWebhook.run(req))
Expand Down
Loading
Loading