From 8b99e9c4545e1721ee9fc368d203333631a16fa0 Mon Sep 17 00:00:00 2001 From: Hongwei Date: Wed, 7 Oct 2026 07:58:41 +0000 Subject: [PATCH 01/29] test: a role-gated v7 endpoint must enforce its roles when a path parameter is empty GET /banks/BANK_ID/api-products/API_PRODUCT_CODE/subscriptions requires canGetApiProductSubscriptionAtOneBank, which only ResourceDocMiddleware enforces. With an empty API_PRODUCT_CODE segment the http4s route still matches but the middleware finds no ResourceDoc, so the handler runs unvalidated and answers 500 (Bank not found in CallContext) instead of 403 for a caller without the role. The empty-segment scenario fails until the middleware selects the ResourceDoc from the route that serves the request. --- .../EmptySegmentRoleValidationTest.scala | 77 +++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 obp-api/src/test/scala/code/api/v7_0_0/EmptySegmentRoleValidationTest.scala diff --git a/obp-api/src/test/scala/code/api/v7_0_0/EmptySegmentRoleValidationTest.scala b/obp-api/src/test/scala/code/api/v7_0_0/EmptySegmentRoleValidationTest.scala new file mode 100644 index 0000000000..5396bd49b4 --- /dev/null +++ b/obp-api/src/test/scala/code/api/v7_0_0/EmptySegmentRoleValidationTest.scala @@ -0,0 +1,77 @@ +/** +Open Bank Project - API +Copyright (C) 2011-2026, TESOBE GmbH. + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . + +Email: contact@tesobe.com +TESOBE GmbH. +Osloer Strasse 16/17 +Berlin 13359, Germany + +This product includes software developed at +TESOBE (http://www.tesobe.com/) + + */ + +package code.api.v7_0_0 + +import cats.effect.unsafe.IORuntime +import code.api.sweep.SweepFixtures +import code.api.util.ErrorMessages.UserHasMissingRoles +import code.setup.{DefaultUsers, ServerSetupWithTestData} +import org.json4s.native.JsonMethods.{compact, render} +import org.scalatest.Tag + +/** + * A role-gated v7.0.0 endpoint must enforce its roles whatever the path parameters hold. + * + * `GET /banks/BANK_ID/api-products/API_PRODUCT_CODE/subscriptions` declares + * `canGetApiProductSubscriptionAtOneBank`, and its handler does not check that role itself: the + * role is enforced only by ResourceDocMiddleware, from the doc it selects for the request. With an + * empty `API_PRODUCT_CODE` segment (`/api-products//subscriptions`) the http4s route still + * matches, but the middleware counts segments, finds no doc and runs the handler without any + * validation, so the caller gets the handler's own answer instead of the role check. + * + * The caller here is authenticated and holds no role at all. + */ +class EmptySegmentRoleValidationTest extends ServerSetupWithTestData with DefaultUsers with SweepFixtures { + + object EmptySegmentRoleValidationTag extends Tag("EmptySegmentRoleValidation") + + implicit val runtime: IORuntime = IORuntime.global + + private def callerWithoutRoles: Map[String, String] = Map("DirectLogin" -> s"token=${token1.value}") + + private def bank: String = realBankId.getOrElse(fail("the test fixtures created no bank")) + + feature("Roles of a v7.0.0 endpoint are enforced for every request its route serves") { + + scenario("A caller without the role is refused for a well-formed path", EmptySegmentRoleValidationTag) { + val (status, json) = callApi("GET", s"/obp/v7.0.0/banks/$bank/api-products/some-product/subscriptions", callerWithoutRoles) + withClue(s"body: ${compact(render(json))}\n") { + status shouldBe 403 + compact(render(json)) should include(UserHasMissingRoles.take(10)) + } + } + + scenario("A caller without the role is refused when a path parameter is empty", EmptySegmentRoleValidationTag) { + val (status, json) = callApi("GET", s"/obp/v7.0.0/banks/$bank/api-products//subscriptions", callerWithoutRoles) + withClue(s"body: ${compact(render(json))}\n") { + status shouldBe 403 + compact(render(json)) should include(UserHasMissingRoles.take(10)) + } + } + } +} From 462d649d49f4db46b21b25877d2590b2fabc85f0 Mon Sep 17 00:00:00 2001 From: Hongwei Date: Wed, 7 Oct 2026 09:05:09 +0000 Subject: [PATCH 02/29] refactor: let a ResourceDoc carry the route it is served by, and select the doc by route ResourceDocMiddleware finds the ResourceDoc of a request by matching the URL against the doc templates, separately from the http4s route that runs the handler. The two can disagree, and then the request is validated under one endpoint's roles, enable/disable switch and operationId while another endpoint's handler runs. Add Http4sRoute, which keeps the pattern match of an endpoint so that it can be asked whether it serves a request, and ResourceDocMatcher.selectByRoute, which returns the doc of the first route that does. When several docs share one route, their templates tell them apart. ResourceDocMiddleware tries the route-bound docs first and runs only the selected route, wrapped in the new `wrap` argument; docs without a route are still found by template, so versions that have not been converted behave as before. An implicit conversion from HttpRoutes[IO] keeps the existing `http4sPartialFunction = Some(routes)` sites compiling. --- .../main/scala/code/api/util/APIUtil.scala | 38 ++++- .../code/api/util/http4s/Http4sSupport.scala | 37 ++++- .../util/http4s/ResourceDocMiddleware.scala | 30 +++- .../scala/code/api/v7_0_0/Http4s700.scala | 1 + .../ResourceDocRouteSelectionTest.scala | 144 ++++++++++++++++++ 5 files changed, 241 insertions(+), 9 deletions(-) create mode 100644 obp-api/src/test/scala/code/api/util/http4s/ResourceDocRouteSelectionTest.scala 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 178f9abe01..46fd323c86 100644 --- a/obp-api/src/main/scala/code/api/util/APIUtil.scala +++ b/obp-api/src/main/scala/code/api/util/APIUtil.scala @@ -2792,7 +2792,43 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{ type OBPReturnType[T] = Future[(T, Option[CallContext])] - type Http4sEndpoint = Option[HttpRoutes[IO]] + /** The pattern match of one http4s endpoint, before `HttpRoutes.of` hides it. */ + type Http4sRoutePF = PartialFunction[org.http4s.Request[IO], IO[org.http4s.Response[IO]]] + + /** + * What a ResourceDoc runs. A [[Http4sRoute]] exposes its pattern match, so ResourceDocMiddleware + * can ask it whether it serves a request and take the doc of the route that does. Plain + * `HttpRoutes[IO]` (a [[LegacyRoutes]]) cannot be asked without running it, so the middleware + * still finds the doc of such an endpoint by matching the URL against the doc's template. + * Endpoints are converted to [[Http4sRoute]] one version at a time; the implicit conversion in + * the companion keeps the unconverted `http4sPartialFunction = Some(routes)` sites compiling. + */ + sealed trait Http4sHandler { + def routes: HttpRoutes[IO] + def route: Option[Http4sRoutePF] + } + + object Http4sHandler { + import scala.language.implicitConversions + implicit def fromHttpRoutes(routes: HttpRoutes[IO]): Http4sHandler = LegacyRoutes(routes) + } + + final case class LegacyRoutes(routes: HttpRoutes[IO]) extends Http4sHandler { + def route: Option[Http4sRoutePF] = None + } + + /** An endpoint whose pattern match is available: `Http4sRoute { case req @ GET -> ... => ... }`. */ + final class Http4sRoute(val pf: Http4sRoutePF) extends Http4sHandler { + lazy val routes: HttpRoutes[IO] = HttpRoutes.of[IO](pf) + def route: Option[Http4sRoutePF] = Some(pf) + def run(req: org.http4s.Request[IO]): cats.data.OptionT[IO, org.http4s.Response[IO]] = routes.run(req) + } + + object Http4sRoute { + def apply(pf: Http4sRoutePF): Http4sRoute = new Http4sRoute(pf) + } + + type Http4sEndpoint = Option[Http4sHandler] // Native http4s endpoint type for runtime-compiled dynamic endpoints (Piece C). // The dynamic-code template compiles to this, and Http4sDynamicEndpoint runs it directly. type Http4sEndpointIO = PartialFunction[org.http4s.Request[IO], CallContext => IO[org.http4s.Response[IO]]] diff --git a/obp-api/src/main/scala/code/api/util/http4s/Http4sSupport.scala b/obp-api/src/main/scala/code/api/util/http4s/Http4sSupport.scala index 5bae77aeea..8bbcc56ad1 100644 --- a/obp-api/src/main/scala/code/api/util/http4s/Http4sSupport.scala +++ b/obp-api/src/main/scala/code/api/util/http4s/Http4sSupport.scala @@ -870,7 +870,42 @@ object ResourceDocMatcher extends code.util.Helper.MdcLoggable { resourceDocs: ArrayBuffer[ResourceDoc] ): Option[ResourceDoc] = findResourceDoc(verb, path, buildIndex(resourceDocs)) - + + /** + * Select the ResourceDoc of the route that serves `req`. + * + * A ResourceDoc whose `http4sPartialFunction` is an [[code.api.util.APIUtil.Http4sRoute]] carries + * the pattern match of its handler, so the request is put to that match instead of being compared + * with the doc's URL template. The doc that comes back is the doc of the route that will run, + * whatever the template says, and nothing about a segment has to be guessed to be a placeholder + * or a fixed word. Docs without a route are ignored; they are found by [[findResourceDoc]]. + * + * `docs` are tried in the order given, which must be the order the routes are tried in when a + * request could be served by more than one. Several docs may share one route (a handler that + * serves every transaction-request type has a doc per type); the template then tells those docs + * apart, the one with the most fixed segments winning, and the first of them when none matches. + */ + def selectByRoute(req: Request[IO], docs: Iterable[ResourceDoc]): Option[ResourceDoc] = { + def routeOf(doc: ResourceDoc): Option[code.api.util.APIUtil.Http4sRoutePF] = + doc.http4sPartialFunction.flatMap(_.route) + + docs.iterator + .flatMap(doc => routeOf(doc).map(doc -> _)) + .find { case (_, route) => route.isDefinedAt(req) } + .map { case (first, route) => + val sharingTheRoute = docs.filter(doc => routeOf(doc).exists(_ eq route)).toList + if (sharingTheRoute.size <= 1) first + else { + val strippedPath = apiPrefixPattern.replaceFirstIn(req.uri.path.renderString, "") + sharingTheRoute + .filter(doc => doc.requestVerb.equalsIgnoreCase(req.method.name) && matchesUrlTemplate(strippedPath, doc.requestUrl)) + .sortBy(doc => -literalSegmentCount(doc.requestUrl)) + .headOption + .getOrElse(first) + } + } + } + /** * Check if a path matches a URL template * Template segments in uppercase are treated as variables diff --git a/obp-api/src/main/scala/code/api/util/http4s/ResourceDocMiddleware.scala b/obp-api/src/main/scala/code/api/util/http4s/ResourceDocMiddleware.scala index 8a7897581c..0e1efd6b29 100644 --- a/obp-api/src/main/scala/code/api/util/http4s/ResourceDocMiddleware.scala +++ b/obp-api/src/main/scala/code/api/util/http4s/ResourceDocMiddleware.scala @@ -142,10 +142,23 @@ object ResourceDocMiddleware extends MdcLoggable { /** * Middleware factory: wraps HttpRoutes with ResourceDoc validation. * Finds the matching ResourceDoc, validates the request, and enriches CallContext. + * + * A doc that carries its route ([[code.api.util.APIUtil.Http4sRoute]]) is found by asking the + * routes whether they serve the request (`ResourceDocMatcher.selectByRoute`, docs tried in the + * order given), and that route alone is run, wrapped in `wrap`. A doc without a route is found by + * matching the URL against its template, and the request goes to `routes`. */ - def apply(resourceDocs: ArrayBuffer[ResourceDoc]): HttpRoutes[IO] => HttpRoutes[IO] = { routes => - // Build the lookup index once per middleware instance (at startup), not per request. - val resourceDocIndex = ResourceDocMatcher.buildIndex(resourceDocs) + def apply( + resourceDocs: ArrayBuffer[ResourceDoc], + wrap: HttpRoutes[IO] => HttpRoutes[IO] = identity + ): HttpRoutes[IO] => HttpRoutes[IO] = { routes => + // Build the lookup structures once per middleware instance (at startup), not per request. + val routeBoundDocs = resourceDocs.filter(_.http4sPartialFunction.exists(_.route.isDefined)).toList + val resourceDocIndex = ResourceDocMatcher.buildIndex( + resourceDocs.filterNot(doc => doc.http4sPartialFunction.exists(_.route.isDefined))) + // A ResourceDoc is a case class holding JSON bodies: look it up by identity, not by equality. + val routeOfDoc = new java.util.IdentityHashMap[ResourceDoc, HttpRoutes[IO]]() + routeBoundDocs.foreach(doc => routeOfDoc.put(doc, wrap(doc.http4sPartialFunction.get.routes))) Kleisli[HttpF, Request[IO], Response[IO]] { req: Request[IO] => // Read enable/disable Props per request so runtime changes (e.g. `setPropsValues` in // tests or live config reloads) take effect immediately. Cost is a few Lift Props @@ -171,12 +184,15 @@ object ResourceDocMiddleware extends MdcLoggable { // Cache the body so bridge-cascade hops (v400→v310→v300→…) don't re-read the now-empty stream. // First read won the body in fromRequest; we replay it from cc.httpBody onwards. val reqWithOriginalBody = req.withAttribute(Http4sRequestAttributes.cachedBodyKey, cc.httpBody) - ResourceDocMatcher.findResourceDoc(req.method.name, req.uri.path, resourceDocIndex) match { - case Some(resourceDoc) if !endpointIsEnabled(resourceDoc) => + val selected: Option[(ResourceDoc, HttpRoutes[IO])] = + ResourceDocMatcher.selectByRoute(req, routeBoundDocs).map(doc => doc -> routeOfDoc.get(doc)) + .orElse(ResourceDocMatcher.findResourceDoc(req.method.name, req.uri.path, resourceDocIndex).map(_ -> routes)) + selected match { + case Some((resourceDoc, _)) if !endpointIsEnabled(resourceDoc) => // Disabled by api_disabled_endpoints / api_enabled_endpoints / api_disabled_versions / // api_enabled_versions. Fall through so the Lift bridge can serve or 404. OptionT.none[IO, Response[IO]] - case Some(resourceDoc) => + case Some((resourceDoc, routesToRun)) => Http4sRequestAttributes.trafficNote(req).foreach { note => note.operationId = Some(resourceDoc.operationId) note.apiVersion = Some(resourceDoc.implementedInApiVersion.apiShortVersion) @@ -206,7 +222,7 @@ object ResourceDocMiddleware extends MdcLoggable { user <- enrichedReq.attributes.lookup(Http4sRequestAttributes.callContextKey).flatMap(_.user.toOption) } note.userId = Some(user.userId) val routeIO = - routes.run(enrichedReq) + routesToRun.run(enrichedReq) .map(ensureJsonContentType) .getOrElseF(IO.pure(ensureJsonContentType(Response[IO](org.http4s.Status.NotFound)))) val executed = diff --git a/obp-api/src/main/scala/code/api/v7_0_0/Http4s700.scala b/obp-api/src/main/scala/code/api/v7_0_0/Http4s700.scala index 6c3262fe3b..343989b5b1 100644 --- a/obp-api/src/main/scala/code/api/v7_0_0/Http4s700.scala +++ b/obp-api/src/main/scala/code/api/v7_0_0/Http4s700.scala @@ -7486,6 +7486,7 @@ object Http4s700 { val sorted = resourceDocs .sortBy(rd => -rd.requestUrl.split("/").count(_.nonEmpty)) .flatMap(_.http4sPartialFunction) + .map(_.routes) sorted.foldLeft(HttpRoutes.empty[IO]) { (acc, route) => HttpRoutes[IO](req => acc.run(req).orElse(route.run(req))) } diff --git a/obp-api/src/test/scala/code/api/util/http4s/ResourceDocRouteSelectionTest.scala b/obp-api/src/test/scala/code/api/util/http4s/ResourceDocRouteSelectionTest.scala new file mode 100644 index 0000000000..100eec8599 --- /dev/null +++ b/obp-api/src/test/scala/code/api/util/http4s/ResourceDocRouteSelectionTest.scala @@ -0,0 +1,144 @@ +/** +Open Bank Project - API +Copyright (C) 2011-2026, TESOBE GmbH. + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . + +Email: contact@tesobe.com +TESOBE GmbH. +Osloer Strasse 16/17 +Berlin 13359, Germany + +This product includes software developed at +TESOBE (http://www.tesobe.com/) + + */ + +package code.api.util.http4s + +import cats.effect.IO +import code.api.util.APIUtil.{Http4sRoute, ResourceDoc} +import code.api.util.ApiTag.ResourceDocTag +import com.openbankproject.commons.util.ApiVersion +import org.http4s._ +import org.http4s.dsl.io._ +import org.json4s.JsonAST.JObject +import org.scalatest.{FeatureSpec, GivenWhenThen, Matchers, Tag} + +/** + * Unit tests for ResourceDocMatcher.selectByRoute. + * + * A ResourceDoc that carries its http4s route is selected by asking the route whether it serves + * the request, not by matching the request URL against the doc's template. The doc that is + * selected is therefore always the doc of the route that will run. + */ +class ResourceDocRouteSelectionTest extends FeatureSpec with Matchers with GivenWhenThen { + + object ResourceDocRouteSelectionTag extends Tag("ResourceDocRouteSelection") + + private val ok: IO[Response[IO]] = IO.pure(Response[IO]()) + + private def doc(name: String, verb: String, url: String, route: Option[Http4sRoute]): ResourceDoc = + ResourceDoc( + implementedInApiVersion = ApiVersion.v7_0_0, + partialFunctionName = name, + requestVerb = verb, + requestUrl = url, + summary = "Test endpoint", + description = "Test description", + exampleRequestBody = JObject(Nil), + successResponseBody = JObject(Nil), + errorResponseBodies = List.empty, + tags = List(ResourceDocTag("test")), + roles = None, + http4sPartialFunction = route + ) + + private def request(method: Method, path: String): Request[IO] = + Request[IO](method, Uri.unsafeFromString(path)) + + feature("ResourceDocMatcher.selectByRoute - the doc of the route that serves the request") { + + scenario("The doc is the route's, even when its template says something else", ResourceDocRouteSelectionTag) { + Given("a doc whose template names /things/THING_ID but whose route serves /gadgets/...") + val route = Http4sRoute { case GET -> Root / "obp" / "v7.0.0" / "gadgets" / _ => ok } + val drifted = doc("getGadget", "GET", "/things/THING_ID", Some(route)) + + When("a request for the gadget is made") + val selected = ResourceDocMatcher.selectByRoute(request(Method.GET, "/obp/v7.0.0/gadgets/g1"), List(drifted)) + + Then("the doc of the route that serves it is selected") + selected.map(_.partialFunctionName) shouldBe Some("getGadget") + + And("a request the template describes but the route does not serve selects nothing") + ResourceDocMatcher.selectByRoute(request(Method.GET, "/obp/v7.0.0/things/t1"), List(drifted)) shouldBe None + } + + scenario("Of several docs whose routes serve the request, the first in catalog order is selected", ResourceDocRouteSelectionTag) { + val broad = Http4sRoute { case GET -> Root / "obp" / "v7.0.0" / "a" / _ => ok } + val narrow = Http4sRoute { case GET -> Root / "obp" / "v7.0.0" / "a" / "special" => ok } + val broadDoc = doc("getAny", "GET", "/a/ANY_ID", Some(broad)) + val narrowDoc = doc("getSpecial", "GET", "/a/special", Some(narrow)) + val req = request(Method.GET, "/obp/v7.0.0/a/special") + + ResourceDocMatcher.selectByRoute(req, List(broadDoc, narrowDoc)) shouldBe Some(broadDoc) + ResourceDocMatcher.selectByRoute(req, List(narrowDoc, broadDoc)) shouldBe Some(narrowDoc) + } + + scenario("The selected doc's route is the route that serves the request", ResourceDocRouteSelectionTag) { + val first = Http4sRoute { case GET -> Root / "obp" / "v7.0.0" / "x" => ok } + val second = Http4sRoute { case GET -> Root / "obp" / "v7.0.0" / "y" => ok } + val docs = List(doc("getX", "GET", "/x", Some(first)), doc("getY", "GET", "/y", Some(second))) + + val req = request(Method.GET, "/obp/v7.0.0/y") + val selected = ResourceDocMatcher.selectByRoute(req, docs).get + selected.partialFunctionName shouldBe "getY" + selected.http4sPartialFunction.flatMap(_.route).get.isDefinedAt(req) shouldBe true + } + + scenario("Docs that share one route are told apart by their templates", ResourceDocRouteSelectionTag) { + Given("one route that serves every transaction-request type, and a generic doc and a SEPA doc that share it") + val anyType = Http4sRoute { case POST -> Root / "obp" / "v7.0.0" / "types" / _ / "requests" => ok } + val generic = doc("createAnyType", "POST", "/types/TRANSACTION_REQUEST_TYPE/requests", Some(anyType)) + val sepa = doc("createSepa", "POST", "/types/SEPA/requests", Some(anyType)) + + Then("a SEPA request gets the SEPA doc whichever is registered first") + ResourceDocMatcher.selectByRoute(request(Method.POST, "/obp/v7.0.0/types/SEPA/requests"), List(generic, sepa)) shouldBe Some(sepa) + ResourceDocMatcher.selectByRoute(request(Method.POST, "/obp/v7.0.0/types/SEPA/requests"), List(sepa, generic)) shouldBe Some(sepa) + + And("a type with no doc of its own gets the generic doc") + ResourceDocMatcher.selectByRoute(request(Method.POST, "/obp/v7.0.0/types/CARDANO/requests"), List(sepa, generic)) shouldBe Some(generic) + } + + scenario("A request no route serves selects nothing, so it can fall through to an older version", ResourceDocRouteSelectionTag) { + val route = Http4sRoute { case POST -> Root / "obp" / "v7.0.0" / "types" / "MOBILE_WALLET" / "requests" => ok } + val wallet = doc("createMobileWallet", "POST", "/types/MOBILE_WALLET/requests", Some(route)) + + ResourceDocMatcher.selectByRoute(request(Method.POST, "/obp/v7.0.0/types/CARDANO/requests"), List(wallet)) shouldBe None + } + + scenario("A route matches an empty path segment the template matcher would not count", ResourceDocRouteSelectionTag) { + val route = Http4sRoute { case GET -> Root / "obp" / "v7.0.0" / "products" / code / "subscriptions" => ok } + val subscriptions = doc("getSubscriptions", "GET", "/products/PRODUCT_CODE/subscriptions", Some(route)) + + ResourceDocMatcher.selectByRoute(request(Method.GET, "/obp/v7.0.0/products//subscriptions"), List(subscriptions)) shouldBe Some(subscriptions) + } + + scenario("A doc without a route is ignored", ResourceDocRouteSelectionTag) { + val legacy = doc("getLegacy", "GET", "/legacy/LEGACY_ID", None) + + ResourceDocMatcher.selectByRoute(request(Method.GET, "/obp/v7.0.0/legacy/l1"), List(legacy)) shouldBe None + } + } +} From 35769b3f450ba04815bc98f8342086b6d39a6564 Mon Sep 17 00:00:00 2001 From: Hongwei Date: Wed, 7 Oct 2026 09:07:23 +0000 Subject: [PATCH 03/29] refactor: bind every v7.0.0 ResourceDoc to its route Declare each v7.0.0 endpoint as an Http4sRoute instead of HttpRoutes.of, so the middleware can ask it whether it serves a request. The same ordered doc list now drives both selecting the doc and running the routes, and ResourceDocMiddleware runs only the selected route, wrapped in the idempotency middleware. The v7.0.0 to v6.0.0 bridge asks the routes whether any of them serves a path instead of matching the URL against templates. A request whose path parameter is empty (/banks/B/api-products//subscriptions) is now served by the route that matches it and validated under that route's doc, so its roles are enforced instead of the handler running unvalidated. ResourceDocRouteBindingTest checks that every v7.0.0 doc is bound to a route that serves its own URL and that selecting by route returns that doc. --- .../scala/code/api/v7_0_0/Http4s700.scala | 254 +++++++++--------- .../code/api/v7_0_0/Http4s700Assets.scala | 8 +- .../v7_0_0/Http4s700DeploymentChecks.scala | 4 +- .../code/api/v7_0_0/Http4s700DomainApis.scala | 12 +- .../Http4s700DynamicEntityDefinitions.scala | 14 +- .../code/api/v7_0_0/Http4s700Groups.scala | 8 +- .../api/v7_0_0/Http4s700IpPenalties.scala | 8 +- .../api/v7_0_0/Http4s700PlatformApps.scala | 10 +- .../code/api/v7_0_0/Http4s700Telemetry.scala | 4 +- .../api/v7_0_0/Http4s700TrafficSources.scala | 4 +- .../http4s/ResourceDocRouteBindingTest.scala | 101 +++++++ 11 files changed, 265 insertions(+), 162 deletions(-) create mode 100644 obp-api/src/test/scala/code/api/util/http4s/ResourceDocRouteBindingTest.scala diff --git a/obp-api/src/main/scala/code/api/v7_0_0/Http4s700.scala b/obp-api/src/main/scala/code/api/v7_0_0/Http4s700.scala index 343989b5b1..030528c69a 100644 --- a/obp-api/src/main/scala/code/api/v7_0_0/Http4s700.scala +++ b/obp-api/src/main/scala/code/api/v7_0_0/Http4s700.scala @@ -214,7 +214,7 @@ object Http4s700 { .mkString(" or ") // Route: GET /obp/v7.0.0/root - val root: HttpRoutes[IO] = HttpRoutes.of[IO] { + val root: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "root" => val responseJson = convertAnyToJsonString( JSONFactory700.getApiInfoJSON(implementedInApiVersion, versionStatus) @@ -266,7 +266,7 @@ object Http4s700 { // v3: { id, ..., views: [ { id, ... } ] } // v7: { account_id, ..., views: [ { view_id, ... } ] } - val corePrivateAccountsAllBanks: HttpRoutes[IO] = HttpRoutes.of[IO] { + val corePrivateAccountsAllBanks: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "my" / "accounts" => EndpointHelpers.withUser(req) { (user, cc) => for { @@ -350,7 +350,7 @@ object Http4s700 { else "On this instance, self-service bank creation is currently disabled." - val createMyBank: HttpRoutes[IO] = HttpRoutes.of[IO] { + val createMyBank: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "my" / "banks" => EndpointHelpers.executeFutureCreated(req) { implicit val cc: CallContext = req.callContext @@ -448,7 +448,7 @@ object Http4s700 { http4sPartialFunction = Some(createMyBank) ) - val getMyBanks: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getMyBanks: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "my" / "banks" => EndpointHelpers.withUser(req) { (user, cc) => for { @@ -493,7 +493,7 @@ object Http4s700 { ) // Category: withUserDelete (user auth, 204 No Content) - val deleteEntitlement: HttpRoutes[IO] = HttpRoutes.of[IO] { + val deleteEntitlement: Http4sRoute = Http4sRoute { case req @ DELETE -> `prefixPath` / "entitlements" / entitlementId => EndpointHelpers.withUserDelete(req) { (_, cc) => Entitlement.entitlement.vend.getEntitlementById(entitlementId) match { @@ -519,7 +519,7 @@ object Http4s700 { ) // Category: withUserAndBodyCreated (user auth, body parsing, 201 Created) - val addEntitlement: HttpRoutes[IO] = HttpRoutes.of[IO] { + val addEntitlement: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "users" / userId / "entitlements" => EndpointHelpers.withUserAndBodyCreated[CreateEntitlementJSON, AnyRef](req) { (user, body, cc) => for { @@ -584,7 +584,7 @@ object Http4s700 { // purpose: the middleware's VIEW_ID validation runs an access check on the // CALLING user, which is wrong for a diagnostic that asks about ANOTHER user. // The caller's authority comes from CanGetAccountAccessTrace. - val getAccountAccessTrace: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getAccountAccessTrace: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "accounts" / _ / "views" / targetViewIdStr / "users" / targetUserIdStr / "account-access-trace" => EndpointHelpers.withBankAccount(req) { (_, account, cc) => val bankIdAccountId = BankIdAccountId(account.bankId, account.accountId) @@ -745,7 +745,7 @@ object Http4s700 { // Route: GET /obp/v7.0.0/public/consent-config // Anonymous: operator-published policy that TPPs/agents need to know before issuing // a consent. The /public prefix marks client-facing config that needs no authentication. - val getConsentsConfig: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getConsentsConfig: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "public" / "consent-config" => EndpointHelpers.executeAndRespond(req) { _ => Future.successful(JSONFactory700.ConsentsConfigJsonV700( @@ -780,7 +780,7 @@ object Http4s700 { // Answers "which Consumer am I?" for whoever is calling: a logged-in User (via their Consumer) or an // Application on its own (client_credentials or a Consumer-Key). No Role: a caller may always learn // its own identity. Unlike GET /obp/v6.0.0/consumers/current it carries no rate limits or call counters. - val getCurrentConsumerIdentity: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getCurrentConsumerIdentity: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "consumers" / "current" / "identity" => EndpointHelpers.executeFuture(req) { implicit val cc: CallContext = req.callContext @@ -823,7 +823,7 @@ object Http4s700 { // Route: GET /obp/v7.0.0/consumers/current/scopes // The Roles the calling Consumer holds as Scopes. No Role, like the identity above: a service may always // learn what it has been granted, so its status page can say which Scopes it still needs. - val getCurrentConsumerScopes: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getCurrentConsumerScopes: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "consumers" / "current" / "scopes" => EndpointHelpers.executeFuture(req) { implicit val cc: CallContext = req.callContext @@ -866,7 +866,7 @@ object Http4s700 { // As v4.0.0's, with two differences: bank_id may be SYS, the system space of Dynamic Entities, where // the Definition and Record Roles live (v4.0.0 refuses it as an unknown bank); and the duplicate check // looks the Scope up by the Consumer's primary key, the key Scopes are stored under. - val addScope: HttpRoutes[IO] = HttpRoutes.of[IO] { + val addScope: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "consumers" / consumerId / "scopes" => EndpointHelpers.withUserAndBodyCreated[code.api.v3_0_0.CreateScopeJson, AnyRef](req) { (user, body, cc) => for { @@ -924,7 +924,7 @@ object Http4s700 { // Anonymous: clients need the policy before they hold credentials, to validate // a proposed password locally during signup or password reset. The /public // prefix marks client-facing config that needs no authentication. - val getPasswordPolicy: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getPasswordPolicy: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "public" / "password-config" => EndpointHelpers.executeAndRespond(req) { _ => Future.successful(JSONFactory700.passwordPoliciesJsonV700) @@ -967,7 +967,7 @@ object Http4s700 { // (links to non-whitelisted hosts stay inert text) before and regardless // of authentication. The /public prefix marks client-facing config that // needs no authentication. - val getChatConfig: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getChatConfig: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "public" / "chat-config" => EndpointHelpers.executeAndRespond(req) { _ => Future(JSONFactory700.ChatConfigJsonV700( @@ -1005,7 +1005,7 @@ object Http4s700 { ) // Route: GET /obp/v7.0.0/api/error-messages - val getErrorMessages: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getErrorMessages: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "api" / "error-messages" => EndpointHelpers.executeAndRespond(req) { _ => Future.successful(ListResult("error_messages", JSONFactory700.errorMessagesCatalog)) @@ -1042,7 +1042,7 @@ object Http4s700 { ) // Route: GET /obp/v7.0.0/api/tags - val getApiTags: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getApiTags: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "api" / "tags" => EndpointHelpers.executeAndRespond(req) { _ => Future.successful(JSONFactory700.createApiTagsJsonV700(allResourceDocs.toList)) @@ -1081,7 +1081,7 @@ object Http4s700 { // ── Phase 1 batch 2 ───────────────────────────────────────────────────── // Route: GET /obp/v7.0.0/users/user-id/USER_ID - val getUserByUserId: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getUserByUserId: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "users" / "user-id" / userId => EndpointHelpers.withUser(req) { (_, cc) => for { @@ -1148,7 +1148,7 @@ object Http4s700 { // v7 signature change over v6: the response carries the user's own mobile phone // fields (number, is_validated flag, validated date) stored on ResourceUser — // distinct from the bank-scoped mobile_phone_number on Customer (KYC data). - val getCurrentUser: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getCurrentUser: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "users" / "current" => EndpointHelpers.withUser(req) { (user, cc) => for { @@ -1220,7 +1220,7 @@ object Http4s700 { private val mobilePhoneNumberRegex = """\+?[0-9\-\s().]{5,50}""" // Route: PUT /obp/v7.0.0/my/user/mobile-phone-number - val updateMyMobilePhoneNumber: HttpRoutes[IO] = HttpRoutes.of[IO] { + val updateMyMobilePhoneNumber: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "my" / "user" / "mobile-phone-number" => EndpointHelpers.withUserAndBody[JSONFactory700.PutMyMobilePhoneNumberJsonV700, JSONFactory700.MyMobilePhoneNumberJsonV700](req) { (user, body, cc) => for { @@ -1278,7 +1278,7 @@ object Http4s700 { // stored on the ResourceUser as unverified (is_validated=false, no validated // date) — verification is a separate flow. Password policy, duplicate-username // check, validation email and default entitlements are shared with v6. - val createUser: HttpRoutes[IO] = HttpRoutes.of[IO] { + val createUser: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "users" => EndpointHelpers.executeFutureCreated(req) { implicit val cc: CallContext = req.callContext @@ -1360,7 +1360,7 @@ object Http4s700 { // Route: GET /obp/v7.0.0/my/metrics // Same fetch path as GET /management/metrics (APIMetrics.getMetricsFromHttpParams) // with the user filter locked to the logged-in user. - val getMyMetrics: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getMyMetrics: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "my" / "metrics" => EndpointHelpers.withUser(req) { (user, cc) => for { @@ -1451,7 +1451,7 @@ object Http4s700 { // ─── getTopUsers ────────────────────────────────────────────────────────────── - val getTopUsers: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getTopUsers: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "management" / "metrics" / "top-users" => EndpointHelpers.withUser(req) { (_, cc) => for { @@ -1529,7 +1529,7 @@ object Http4s700 { // ─── getTopConsumers ────────────────────────────────────────────────────────── - val getTopConsumers: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getTopConsumers: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "management" / "metrics" / "top-consumers" => EndpointHelpers.withUser(req) { (_, cc) => for { @@ -1610,7 +1610,7 @@ object Http4s700 { // ── Trading Endpoints ────────────────────────────────────────────────── // Route: POST /obp/v7.0.0/banks/BANK_ID/accounts/ACCOUNT_ID/views/VIEW_ID/trading/offers - val createTradingOffer: HttpRoutes[IO] = HttpRoutes.of[IO] { + val createTradingOffer: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "banks" / bankId / "accounts" / accountId / "views" / viewId / "trading" / "offers" => EndpointHelpers.withUserAndBodyCreated[JSONFactory700.CreateOfferRequestJson, JSONFactory700.TradingOfferJson](req) { (user, createOfferJson, cc) => for { @@ -1703,7 +1703,7 @@ object Http4s700 { ) // Route: GET /obp/v7.0.0/banks/BANK_ID/accounts/ACCOUNT_ID/views/VIEW_ID/trading/offers/OFFER_ID - val getTradingOffer: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getTradingOffer: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / bankId / "accounts" / accountId / "views" / viewId / "trading" / "offers" / offerId => EndpointHelpers.withUser(req) { (user, cc) => for { @@ -1755,7 +1755,7 @@ object Http4s700 { ) // Route: GET /obp/v7.0.0/banks/BANK_ID/accounts/ACCOUNT_ID/views/VIEW_ID/trading/offers - val getTradingOffers: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getTradingOffers: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / bankId / "accounts" / accountId / "views" / viewId / "trading" / "offers" => EndpointHelpers.withUser(req) { (user, cc) => // Extract query parameters @@ -1831,7 +1831,7 @@ object Http4s700 { ) // Route: DELETE /obp/v7.0.0/banks/BANK_ID/accounts/ACCOUNT_ID/views/VIEW_ID/trading/offers/OFFER_ID - val cancelTradingOffer: HttpRoutes[IO] = HttpRoutes.of[IO] { + val cancelTradingOffer: Http4sRoute = Http4sRoute { case req @ DELETE -> `prefixPath` / "banks" / bankId / "accounts" / accountId / "views" / viewId / "trading" / "offers" / offerId => EndpointHelpers.withUser(req) { (user, cc) => for { @@ -1870,7 +1870,7 @@ object Http4s700 { // ── Market Endpoints (Phase 2) ───────────────────────────────────────── // Route: POST /obp/v7.0.0/banks/BANK_ID/accounts/ACCOUNT_ID/views/VIEW_ID/market/orders - val createMarketOrder: HttpRoutes[IO] = HttpRoutes.of[IO] { + val createMarketOrder: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "banks" / bankId / "accounts" / accountId / "views" / viewId / "market" / "orders" => EndpointHelpers.withUserAndBodyCreated[JSONFactory700.CreateMarketOrderRequestJson, JSONFactory700.MarketOrderJson](req) { (user, createOrderJson, cc) => for { @@ -1951,7 +1951,7 @@ object Http4s700 { ) // Route: GET /obp/v7.0.0/banks/BANK_ID/accounts/ACCOUNT_ID/views/VIEW_ID/market/orders/ORDER_ID - val getMarketOrder: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getMarketOrder: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / bankId / "accounts" / accountId / "views" / viewId / "market" / "orders" / orderId => EndpointHelpers.withUser(req) { (user, cc) => for { @@ -1999,7 +1999,7 @@ object Http4s700 { ) // Route: DELETE /obp/v7.0.0/banks/BANK_ID/accounts/ACCOUNT_ID/views/VIEW_ID/market/orders/ORDER_ID - val cancelMarketOrder: HttpRoutes[IO] = HttpRoutes.of[IO] { + val cancelMarketOrder: Http4sRoute = Http4sRoute { case req @ DELETE -> `prefixPath` / "banks" / bankId / "accounts" / accountId / "views" / viewId / "market" / "orders" / orderId => EndpointHelpers.withUser(req) { (user, cc) => for { @@ -2049,7 +2049,7 @@ object Http4s700 { ) // Route: POST /obp/v7.0.0/banks/BANK_ID/accounts/ACCOUNT_ID/views/VIEW_ID/market/matches - val createMarketMatch: HttpRoutes[IO] = HttpRoutes.of[IO] { + val createMarketMatch: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "banks" / bankId / "accounts" / accountId / "views" / viewId / "market" / "matches" => EndpointHelpers.withUserAndBodyCreated[JSONFactory700.CreateMarketMatchRequestJson, JSONFactory700.MarketMatchJson](req) { (user, createMatchJson, cc) => for { @@ -2119,7 +2119,7 @@ object Http4s700 { ) // Route: GET /obp/v7.0.0/banks/BANK_ID/accounts/ACCOUNT_ID/views/VIEW_ID/market/trades/TRADE_ID - val getMarketTrade: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getMarketTrade: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / bankId / "accounts" / accountId / "views" / viewId / "market" / "trades" / tradeId => EndpointHelpers.withUser(req) { (user, cc) => for { @@ -2166,7 +2166,7 @@ object Http4s700 { ) // Route: POST /obp/v7.0.0/banks/BANK_ID/accounts/ACCOUNT_ID/views/VIEW_ID/market/settlements - val requestSettlement: HttpRoutes[IO] = HttpRoutes.of[IO] { + val requestSettlement: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "banks" / bankId / "accounts" / accountId / "views" / viewId / "market" / "settlements" => EndpointHelpers.withUserAndBodyCreated[JSONFactory700.RequestSettlementJson, JSONFactory700.SettlementJson](req) { (user, requestJson, cc) => for { @@ -2294,7 +2294,7 @@ object Http4s700 { // ) // Route: POST /obp/v7.0.0/banks/BANK_ID/accounts/ACCOUNT_ID/views/VIEW_ID/market/withdrawals - val requestWithdrawal: HttpRoutes[IO] = HttpRoutes.of[IO] { + val requestWithdrawal: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "banks" / bankId / "accounts" / accountId / "views" / viewId / "market" / "withdrawals" => EndpointHelpers.withUserAndBodyCreated[JSONFactory700.RequestWithdrawalJson, JSONFactory700.WithdrawalJson](req) { (user, withdrawalJson, cc) => for { @@ -2632,7 +2632,7 @@ object Http4s700 { message_id: String ) - val createTestEmail: HttpRoutes[IO] = HttpRoutes.of[IO] { + val createTestEmail: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "management" / "self-test-emails" => EndpointHelpers.executeFutureCreated(req) { implicit val cc: CallContext = req.callContext @@ -2827,7 +2827,7 @@ object Http4s700 { } } - val createValidationEmail: HttpRoutes[IO] = HttpRoutes.of[IO] { + val createValidationEmail: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "users" / "validation-emails" => EndpointHelpers.executeFutureCreated(req) { implicit val cc: CallContext = req.callContext @@ -2966,7 +2966,7 @@ object Http4s700 { private val ValidOrganisationVisibilities = Set("public", "unlisted", "private") private val OrganisationIdRegex = "^[a-zA-Z0-9._-]{2,64}$".r - val createOrganisation: HttpRoutes[IO] = HttpRoutes.of[IO] { + val createOrganisation: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "organisations" => EndpointHelpers.withUserAndBodyCreated[JSONFactory700.PostOrganisationJsonV700, JSONFactory700.OrganisationJsonV700](req) { (user, body, cc) => for { @@ -3037,7 +3037,7 @@ object Http4s700 { http4sPartialFunction = Some(createOrganisation) ) - val getOrganisations: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getOrganisations: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "organisations" => EndpointHelpers.withUser(req) { (user, cc) => for { @@ -3080,7 +3080,7 @@ object Http4s700 { http4sPartialFunction = Some(getOrganisations) ) - val getOrganisation: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getOrganisation: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "organisations" / organisationId => EndpointHelpers.withUser(req) { (user, cc) => for { @@ -3122,7 +3122,7 @@ object Http4s700 { http4sPartialFunction = Some(getOrganisation) ) - val updateOrganisation: HttpRoutes[IO] = HttpRoutes.of[IO] { + val updateOrganisation: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "organisations" / organisationId => EndpointHelpers.withUserAndBody[JSONFactory700.PutOrganisationJsonV700, JSONFactory700.OrganisationJsonV700](req) { (_, body, cc) => for { @@ -3178,7 +3178,7 @@ object Http4s700 { http4sPartialFunction = Some(updateOrganisation) ) - val deleteOrganisation: HttpRoutes[IO] = HttpRoutes.of[IO] { + val deleteOrganisation: Http4sRoute = Http4sRoute { case req @ DELETE -> `prefixPath` / "organisations" / organisationId => EndpointHelpers.withUserDelete(req) { (_, cc) => for { @@ -3220,7 +3220,7 @@ object Http4s700 { // matches path segments by '/', not by '.', so "TZ.MSISDN" is a single // segment. - val createRoutingScheme: HttpRoutes[IO] = HttpRoutes.of[IO] { + val createRoutingScheme: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "routing-schemes" => EndpointHelpers.withUserAndBodyCreated[JSONFactory700.PostRoutingSchemeJsonV700, JSONFactory700.RoutingSchemeJsonV700](req) { (user, body, cc) => for { @@ -3315,7 +3315,7 @@ object Http4s700 { http4sPartialFunction = Some(createRoutingScheme) ) - val getRoutingSchemes: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getRoutingSchemes: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "routing-schemes" => EndpointHelpers.executeAndRespond(req) { cc => val q = req.uri.query.params @@ -3366,7 +3366,7 @@ object Http4s700 { http4sPartialFunction = Some(getRoutingSchemes) ) - val getRoutingScheme: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getRoutingScheme: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "routing-schemes" / schemeName => EndpointHelpers.executeAndRespond(req) { cc => for { @@ -3404,7 +3404,7 @@ object Http4s700 { http4sPartialFunction = Some(getRoutingScheme) ) - val updateRoutingScheme: HttpRoutes[IO] = HttpRoutes.of[IO] { + val updateRoutingScheme: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "routing-schemes" / schemeName => EndpointHelpers.withUserAndBody[JSONFactory700.PutRoutingSchemeJsonV700, JSONFactory700.RoutingSchemeJsonV700](req) { (_, body, cc) => for { @@ -3482,7 +3482,7 @@ object Http4s700 { http4sPartialFunction = Some(updateRoutingScheme) ) - val deleteRoutingScheme: HttpRoutes[IO] = HttpRoutes.of[IO] { + val deleteRoutingScheme: Http4sRoute = Http4sRoute { case req @ DELETE -> `prefixPath` / "routing-schemes" / schemeName => EndpointHelpers.withUserDelete(req) { (_, cc) => for { @@ -3518,7 +3518,7 @@ object Http4s700 { // notification webhook was permanent for everyone, by every route: the provider's delete // methods existed but nothing called them. These are that missing half. - val deleteSystemAccountNotificationWebhook: HttpRoutes[IO] = HttpRoutes.of[IO] { + val deleteSystemAccountNotificationWebhook: Http4sRoute = Http4sRoute { case req @ DELETE -> `prefixPath` / "web-hooks" / "account" / "notifications" / "on-create-transaction" / webhookId => EndpointHelpers.withUserDelete(req) { (_, cc) => val provider = code.webhook.SystemAccountNotificationWebhookTrait.systemAccountNotificationWebhook.vend @@ -3553,7 +3553,7 @@ object Http4s700 { http4sPartialFunction = Some(deleteSystemAccountNotificationWebhook) ) - val deleteBankAccountNotificationWebhook: HttpRoutes[IO] = HttpRoutes.of[IO] { + val deleteBankAccountNotificationWebhook: Http4sRoute = Http4sRoute { case req @ DELETE -> `prefixPath` / "banks" / _ / "web-hooks" / "account" / "notifications" / "on-create-transaction" / webhookId => EndpointHelpers.withUserAndBankDelete(req) { (_, bank, cc) => val provider = code.webhook.BankAccountNotificationWebhookTrait.bankAccountNotificationWebhook.vend @@ -3593,7 +3593,7 @@ object Http4s700 { http4sPartialFunction = Some(deleteBankAccountNotificationWebhook) ) - val getBankSupportedRoutingSchemes: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getBankSupportedRoutingSchemes: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "supported-routing-schemes" => EndpointHelpers.withUserAndBank(req) { (_, bank, cc) => for { @@ -3630,7 +3630,7 @@ object Http4s700 { http4sPartialFunction = Some(getBankSupportedRoutingSchemes) ) - val putBankSupportedRoutingScheme: HttpRoutes[IO] = HttpRoutes.of[IO] { + val putBankSupportedRoutingScheme: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "banks" / _ / "supported-routing-schemes" / schemeName => EndpointHelpers.withUserAndBankAndBody[JSONFactory700.PutBankSupportedRoutingSchemeJsonV700, JSONFactory700.BankSupportedRoutingSchemeJsonV700](req) { (_, bank, body, cc) => for { @@ -3741,7 +3741,7 @@ object Http4s700 { } } else Future.unit - val getGlossary: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getGlossary: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "api" / "glossary" => EndpointHelpers.executeAndRespond(req) { cc => val q = req.uri.query.params @@ -3850,7 +3850,7 @@ object Http4s700 { http4sPartialFunction = Some(getGlossary) ) - val getGlossaryItem: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getGlossaryItem: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "api" / "glossary" / titleSegment => EndpointHelpers.executeAndRespond(req) { cc => val expanded = !req.uri.query.params.get("expanded").map(_.trim.toLowerCase).contains("false") @@ -3911,7 +3911,7 @@ object Http4s700 { http4sPartialFunction = Some(getGlossaryItem) ) - val createGlossaryItem: HttpRoutes[IO] = HttpRoutes.of[IO] { + val createGlossaryItem: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "api" / "glossary" => EndpointHelpers.withUserAndBodyCreated[JSONFactory700.PostGlossaryItemJsonV700, JSONFactory700.GlossaryItemJsonV700](req) { (user, body, cc) => // A json null extracts to a null String rather than failing, so guard before trimming. @@ -3983,7 +3983,7 @@ object Http4s700 { http4sPartialFunction = Some(createGlossaryItem) ) - val updateGlossaryItem: HttpRoutes[IO] = HttpRoutes.of[IO] { + val updateGlossaryItem: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "api" / "glossary" / titleSegment => EndpointHelpers.withUserAndBody[JSONFactory700.PutGlossaryItemJsonV700, JSONFactory700.GlossaryItemJsonV700](req) { (_, body, cc) => for { @@ -4038,7 +4038,7 @@ object Http4s700 { http4sPartialFunction = Some(updateGlossaryItem) ) - val deleteGlossaryItem: HttpRoutes[IO] = HttpRoutes.of[IO] { + val deleteGlossaryItem: Http4sRoute = Http4sRoute { case req @ DELETE -> `prefixPath` / "api" / "glossary" / titleSegment => EndpointHelpers.withUserDelete(req) { (_, cc) => for { @@ -4087,7 +4087,7 @@ object Http4s700 { private val PayeeLookupValidCategories: Set[String] = Set("ACCOUNT", "BILL", "UTILITY") private val PayeeLookupTtlSeconds: Long = 600 // 10 minutes - val createPayeeLookup: HttpRoutes[IO] = HttpRoutes.of[IO] { + val createPayeeLookup: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "banks" / _ / "accounts" / _ / _ / "payees" / "lookup" => EndpointHelpers.withViewAndBodyCreated[JSONFactory700.PostPayeeLookupJsonV700, JSONFactory700.PayeeLookupResponseJsonV700](req) { (user, bankAccount, _, body, cc) => for { @@ -4203,7 +4203,7 @@ object Http4s700 { // into the existing v400 payment pipeline so the standard transaction-request // response shape is preserved. - val createTransactionRequestMobileWallet: HttpRoutes[IO] = HttpRoutes.of[IO] { + val createTransactionRequestMobileWallet: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "banks" / _ / "accounts" / _ / _ / "transaction-request-types" / "MOBILE_WALLET" / "transaction-requests" => EndpointHelpers.withViewAndBodyCreated[JSONFactory700.TransactionRequestBodyMobileWalletJsonV700, JSONFactory700.TransactionRequestWithChargeMobileWalletJsonV700](req) { (user, fromAccount, view, body, cc) => val countryCode = body.country_code.getOrElse("TZ") @@ -4342,7 +4342,7 @@ object Http4s700 { // payment. val UtilityValidCategories: Set[String] = Set("UTILITY", "BILL") - val createTransactionRequestUtility: HttpRoutes[IO] = HttpRoutes.of[IO] { + val createTransactionRequestUtility: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "banks" / _ / "accounts" / _ / _ / "transaction-request-types" / "UTILITY" / "transaction-requests" => EndpointHelpers.withViewAndBodyCreated[JSONFactory700.TransactionRequestBodyUtilityJsonV700, JSONFactory700.TransactionRequestWithChargeUtilityJsonV700](req) { (user, fromAccount, view, body, cc) => val callCtx = Some(cc) @@ -4509,7 +4509,7 @@ object Http4s700 { // trusted system actor, gated by canCreateUtilityVendResult. Returns 200. // System path: a flat /utility-payments/UTILITY_TRANSACTION_REQUEST_ID segment avoids // ACCOUNT_ID/VIEW_ID middleware resolution (the rail has no view on the payer's account). - val createUtilityVendResult: HttpRoutes[IO] = HttpRoutes.of[IO] { + val createUtilityVendResult: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "banks" / _ / "utility-payments" / trIdStr / "vend-result" => EndpointHelpers.withUserAndBody[JSONFactory700.PostUtilityVendResultJsonV700, JSONFactory700.UtilityVendResultResponseJsonV700](req) { (_, body, cc) => val callCtx = Some(cc) @@ -4621,7 +4621,7 @@ object Http4s700 { // side-car on the TR row and surfaced on the v7 response. Lives natively at // v7 (rather than bridging to v4) because only v7's response shape carries // the originator block. - val createTransactionRequestOpenCorridor: HttpRoutes[IO] = HttpRoutes.of[IO] { + val createTransactionRequestOpenCorridor: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "banks" / _ / "accounts" / _ / _ / "transaction-request-types" / "OPEN_CORRIDOR_PROMISE" / "transaction-requests" => EndpointHelpers.withViewAndBodyCreated[JSONFactory700.TransactionRequestBodyOpenCorridorJsonV700, JSONFactory700.TransactionRequestWithChargeOpenCorridorJsonV700](req) { (user, fromAccount, view, body, cc) => val callCtx = Some(cc) @@ -4721,7 +4721,7 @@ object Http4s700 { // here. OBP-API stores them as Transaction Request attributes on the PENDING // promise TR and later relays the evidence to the beneficiary bank inside // obp_credit_notification. The evidence is opaque to OBP-API. - val attachOpenCorridorPromise: HttpRoutes[IO] = HttpRoutes.of[IO] { + val attachOpenCorridorPromise: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "banks" / _ / "accounts" / _ / "transaction-requests" / transactionRequestIdStr / "open-corridor" / "promise" if transactionRequestIdStr.nonEmpty => EndpointHelpers.withUserAndBankAndBodyCreated[JSONFactory700.PostOpenCorridorPromiseJsonV700, JSONFactory700.OpenCorridorPromiseJsonV700](req) { (user, bank, body, cc) => for { @@ -4782,7 +4782,7 @@ object Http4s700 { // Read/repair access to the generic transactional outbox. The relay retries // transient failures itself; STICKY rows wait here for a human. - val getMessageOutbox: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getMessageOutbox: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "management" / "message-outbox" => EndpointHelpers.withUser(req) { (_, cc) => scala.concurrent.Future { @@ -4801,7 +4801,7 @@ object Http4s700 { } } - val retryMessageOutboxRow: HttpRoutes[IO] = HttpRoutes.of[IO] { + val retryMessageOutboxRow: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "management" / "message-outbox" / outboxIdStr / "retry" => EndpointHelpers.withUser(req) { (_, cc) => import code.messageoutbox.MessageOutbox @@ -4952,14 +4952,14 @@ object Http4s700 { } yield JSONFactory700.createAccountJsonV700(ownerId, bankAccount, accountAttributes) } - val createAccountV700: HttpRoutes[IO] = HttpRoutes.of[IO] { + val createAccountV700: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "banks" / _ / "accounts" => EndpointHelpers.withUserAndBankAndBodyCreated[JSONFactory700.CreateAccountRequestJsonV700, JSONFactory700.CreateAccountResponseJsonV700](req) { (user, bank, body, cc) => createAccountCommon(user, bank, body, None, cc) } } - val createAccountWithIdV700: HttpRoutes[IO] = HttpRoutes.of[IO] { + val createAccountWithIdV700: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "banks" / _ / "accounts" / accountIdStr => EndpointHelpers.withUserAndBankAndBodyCreated[JSONFactory700.CreateAccountRequestJsonV700, JSONFactory700.CreateAccountResponseJsonV700](req) { (user, bank, body, cc) => createAccountCommon(user, bank, body, Some(accountIdStr), cc) @@ -5048,7 +5048,7 @@ object Http4s700 { // bank's Bank Node consumes on its own vhost, so Interface C publishing needs // the bank's broker coordinates. Passwords are write-only (never echoed). - val setAmqpBankBroker: HttpRoutes[IO] = HttpRoutes.of[IO] { + val setAmqpBankBroker: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "banks" / _ / "amqp-broker" => EndpointHelpers.withUserAndBankAndBody[JSONFactory700.PostAmqpBankBrokerJsonV700, JSONFactory700.AmqpBankBrokerJsonV700](req) { (_, bank, body, cc) => for { @@ -5067,7 +5067,7 @@ object Http4s700 { } } - val getAmqpBankBroker: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getAmqpBankBroker: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "amqp-broker" => EndpointHelpers.withUserAndBank(req) { (_, bank, cc) => scala.concurrent.Future { @@ -5084,7 +5084,7 @@ object Http4s700 { } } - val deleteAmqpBankBroker: HttpRoutes[IO] = HttpRoutes.of[IO] { + val deleteAmqpBankBroker: Http4sRoute = Http4sRoute { case req @ DELETE -> `prefixPath` / "banks" / _ / "amqp-broker" => EndpointHelpers.withUserAndBankDelete(req) { (_, bank, cc) => scala.concurrent.Future { @@ -5175,7 +5175,7 @@ object Http4s700 { // transaction (transactional outbox; the relay publishes them). // The URL bank is one side of the pair; CanSettleOpenCorridor is bank-scoped // and checked there, so a bank can only settle corridors it is party to. - val createOpenCorridorSettlement: HttpRoutes[IO] = HttpRoutes.of[IO] { + val createOpenCorridorSettlement: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "banks" / _ / "open-corridor" / "settlements" => EndpointHelpers.withUserAndBankAndBodyCreated[JSONFactory700.PostOpenCorridorSettlementJsonV700, JSONFactory700.OpenCorridorSettleResultJsonV700](req) { (user, bank, body, cc) => for { @@ -5246,7 +5246,7 @@ object Http4s700 { // covered promise; returns exempt) and enqueues ONE settlement instruction // with purpose=PLATFORM_FEE — creditor is the PLATFORM's settlement // account. The bank's node pays it in ADA like any settlement. - val createOpenCorridorFeeSettlement: HttpRoutes[IO] = HttpRoutes.of[IO] { + val createOpenCorridorFeeSettlement: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "banks" / _ / "open-corridor" / "fee-settlements" => EndpointHelpers.withUserAndBankAndBodyCreated[JSONFactory700.PostOpenCorridorFeeSettlementJsonV700, code.opencorridorfees.OpenCorridorFeeSweepResultJsonV700](req) { (_, bank, body, cc) => for { @@ -5297,7 +5297,7 @@ object Http4s700 { // The settlement resource's read side: ledger fields from the // OPEN_CORRIDOR_SETTLEMENT TR, rail status from the settlement-instruction // outbox row (the node's last reply — redelivery doubles as the poll). - val getOpenCorridorSettlement: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getOpenCorridorSettlement: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "open-corridor" / "settlements" / settlementId => EndpointHelpers.withUserAndBank(req) { (_, bank, cc) => for { @@ -5366,7 +5366,7 @@ object Http4s700 { // do not abort the whole batch — matches how real CBS bulk processing // behaves. See BulkPaymentHandler for the orchestration. - val createTransactionRequestBulk: HttpRoutes[IO] = HttpRoutes.of[IO] { + val createTransactionRequestBulk: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "banks" / _ / "accounts" / _ / _ / "transaction-request-types" / "BULK" / "transaction-requests" => EndpointHelpers.withViewAndBodyCreated[JSONFactory700.TransactionRequestBodyBulkJsonV700, JSONFactory700.BulkTransactionRequestResponseJsonV700](req) { (user, fromAccount, view, body, cc) => val callCtx = Some(cc) @@ -5558,7 +5558,7 @@ object Http4s700 { // Each successful invocation is audit-logged at INFO level with the // calling user_id and the reset view_id; this is a high-impact admin // action and we want a trace of who reset what. - val factoryResetSystemView: HttpRoutes[IO] = HttpRoutes.of[IO] { + val factoryResetSystemView: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "management" / "system-views" / viewIdStr / "factory-reset" => EndpointHelpers.withUser(req) { (user, cc) => val viewId = ViewId(viewIdStr) @@ -5635,7 +5635,7 @@ object Http4s700 { // `metric` and `metricarchive` tables, then runs integrity checks that // surface whether MetricsArchiveScheduler is keeping each table inside its // configured retention window. Intended for use from the API Manager. - val getMetricsDiagnostics: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getMetricsDiagnostics: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "management" / "system" / "diagnostics" / "metrics" => EndpointHelpers.withUser(req) { (_, _) => Future { @@ -5702,7 +5702,7 @@ object Http4s700 { // retention props, and records the run in the `metricsarchiverun` log. // The run executes synchronously and may take a while for large backlogs // (it moves up to `retain_metrics_move_limit` rows). - val triggerMetricsArchiveRun: HttpRoutes[IO] = HttpRoutes.of[IO] { + val triggerMetricsArchiveRun: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "management" / "system" / "diagnostics" / "metrics" / "run" => EndpointHelpers.withUser(req) { (user, _) => Future { @@ -5763,7 +5763,7 @@ object Http4s700 { // when the job finishes — so in healthy operation this is empty. Any row here // is a currently-running job or a stale lock left by a dead JVM; `age_seconds` // tells them apart, and the row can be cleared with the DELETE route below. - val getSchedulerJobs: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getSchedulerJobs: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "management" / "system" / "scheduler" / "job-locks" => EndpointHelpers.withUser(req) { (_, _) => Future { @@ -5808,7 +5808,7 @@ object Http4s700 { // Clear a scheduler lock row by its job id. Use this to release a stale lock // left by a dead JVM so the job (e.g. metrics archiving) can run again. // Idempotent — returns 204 even if the row is already gone. - val deleteSchedulerJob: HttpRoutes[IO] = HttpRoutes.of[IO] { + val deleteSchedulerJob: Http4sRoute = Http4sRoute { case req @ DELETE -> `prefixPath` / "management" / "system" / "scheduler" / "job-locks" / jobId => EndpointHelpers.withUserDelete(req) { (_, _) => Future { code.scheduler.JobScheduler.deleteByJobId(jobId); () } @@ -5851,7 +5851,7 @@ object Http4s700 { // hits Outcome.Errored → rollback. Used by Http4s700TransactionTest to verify // that data written inside a failed request is never committed. if (net.liftweb.util.Props.testMode) { - val testRollbackEndpoint: HttpRoutes[IO] = HttpRoutes.of[IO] { + val testRollbackEndpoint: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "test" / "rollback-check" => val cc = req.callContext cc.user.toOption match { @@ -5885,7 +5885,7 @@ object Http4s700 { // object. Create/update/delete stay on v4. Roles mirror the v4 GET roles. // ═══════════════════════════════════════════════════════════════════════════ - val getDynamicResourceDocsProvenance: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getDynamicResourceDocsProvenance: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "management" / "dynamic-resource-docs" => EndpointHelpers.withUser(req) { (_, cc) => Future(code.dynamicResourceDoc.DynamicResourceDoc.findAll()) @@ -5912,7 +5912,7 @@ object Http4s700 { http4sPartialFunction = Some(getDynamicResourceDocsProvenance) ) - val getDynamicResourceDocProvenance: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getDynamicResourceDocProvenance: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "management" / "dynamic-resource-docs" / dynamicResourceDocId => EndpointHelpers.withUser(req) { (_, cc) => Future(code.dynamicResourceDoc.DynamicResourceDoc.find( @@ -5943,7 +5943,7 @@ object Http4s700 { http4sPartialFunction = Some(getDynamicResourceDocProvenance) ) - val getConnectorMethodsProvenance: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getConnectorMethodsProvenance: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "management" / "connector-methods" => EndpointHelpers.withUser(req) { (_, cc) => Future(code.connectormethod.ConnectorMethod.findAll()) @@ -5970,7 +5970,7 @@ object Http4s700 { http4sPartialFunction = Some(getConnectorMethodsProvenance) ) - val getConnectorMethodProvenance: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getConnectorMethodProvenance: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "management" / "connector-methods" / connectorMethodId => EndpointHelpers.withUser(req) { (_, cc) => Future(code.connectormethod.ConnectorMethod.find( @@ -6001,7 +6001,7 @@ object Http4s700 { http4sPartialFunction = Some(getConnectorMethodProvenance) ) - val getDynamicMessageDocsProvenance: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getDynamicMessageDocsProvenance: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "management" / "dynamic-message-docs" => EndpointHelpers.withUser(req) { (_, cc) => Future(code.dynamicMessageDoc.DynamicMessageDoc.findAll()) @@ -6028,7 +6028,7 @@ object Http4s700 { http4sPartialFunction = Some(getDynamicMessageDocsProvenance) ) - val getDynamicMessageDocProvenance: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getDynamicMessageDocProvenance: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "management" / "dynamic-message-docs" / dynamicMessageDocId => EndpointHelpers.withUser(req) { (_, cc) => Future(code.dynamicMessageDoc.DynamicMessageDoc.find( @@ -6090,7 +6090,7 @@ object Http4s700 { .map(JSONFactory700.createApiProductSubscriptionsJsonV700) // Route: POST /obp/v7.0.0/banks/BANK_ID/api-products/API_PRODUCT_CODE/subscriptions (201) - val createApiProductSubscription: HttpRoutes[IO] = HttpRoutes.of[IO] { + val createApiProductSubscription: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "banks" / _ / "api-products" / apiProductCode / "subscriptions" => EndpointHelpers.executeFutureCreated(req) { implicit val cc: CallContext = req.callContext @@ -6158,7 +6158,7 @@ object Http4s700 { ).disableAutoValidateRoles() // Route: GET /obp/v7.0.0/my/api-product-subscriptions - val getMyApiProductSubscriptions: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getMyApiProductSubscriptions: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "my" / "api-product-subscriptions" => EndpointHelpers.withUser(req) { (user, cc) => for { @@ -6191,7 +6191,7 @@ object Http4s700 { ) // Route: GET /obp/v7.0.0/my/api-product-subscriptions/API_PRODUCT_SUBSCRIPTION_ID - val getMyApiProductSubscription: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getMyApiProductSubscription: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "my" / "api-product-subscriptions" / apiProductSubscriptionId => EndpointHelpers.withUser(req) { (user, cc) => for { @@ -6226,7 +6226,7 @@ object Http4s700 { ) // Route: PUT /obp/v7.0.0/my/api-product-subscriptions/API_PRODUCT_SUBSCRIPTION_ID/status - val updateMyApiProductSubscriptionStatus: HttpRoutes[IO] = HttpRoutes.of[IO] { + val updateMyApiProductSubscriptionStatus: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "my" / "api-product-subscriptions" / apiProductSubscriptionId / "status" => EndpointHelpers.withUser(req) { (user, cc) => val rawBody = cc.httpBody.getOrElse("") @@ -6269,7 +6269,7 @@ object Http4s700 { ) // Route: GET /obp/v7.0.0/banks/BANK_ID/api-products/API_PRODUCT_CODE/subscriptions - val getApiProductSubscriptionsByProduct: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getApiProductSubscriptionsByProduct: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "api-products" / apiProductCode / "subscriptions" => EndpointHelpers.withUserAndBank(req) { (_, bank, cc) => for { @@ -6300,7 +6300,7 @@ object Http4s700 { ) // Route: GET /obp/v7.0.0/management/consumers/CONSUMER_ID/api-product-subscriptions - val getConsumerApiProductSubscriptions: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getConsumerApiProductSubscriptions: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "management" / "consumers" / consumerId / "api-product-subscriptions" => EndpointHelpers.withUser(req) { (user, cc) => val role = ApiRole.canGetApiProductSubscriptionAtOneBank @@ -6353,7 +6353,7 @@ object Http4s700 { ).disableAutoValidateRoles() // Route: GET /obp/v7.0.0/management/api-product-subscriptions/API_PRODUCT_SUBSCRIPTION_ID - val getApiProductSubscription: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getApiProductSubscription: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "management" / "api-product-subscriptions" / apiProductSubscriptionId => EndpointHelpers.withUser(req) { (user, cc) => for { @@ -6387,7 +6387,7 @@ object Http4s700 { // Route: PUT /obp/v7.0.0/management/api-product-subscriptions/API_PRODUCT_SUBSCRIPTION_ID/status // The one write a billing adapter makes. - val updateApiProductSubscriptionStatus: HttpRoutes[IO] = HttpRoutes.of[IO] { + val updateApiProductSubscriptionStatus: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "management" / "api-product-subscriptions" / apiProductSubscriptionId / "status" => EndpointHelpers.withUser(req) { (user, cc) => val rawBody = cc.httpBody.getOrElse("") @@ -6429,7 +6429,7 @@ object Http4s700 { ).disableAutoValidateRoles() // Route: DELETE /obp/v7.0.0/management/api-product-subscriptions/API_PRODUCT_SUBSCRIPTION_ID - val deleteApiProductSubscription: HttpRoutes[IO] = HttpRoutes.of[IO] { + val deleteApiProductSubscription: Http4sRoute = Http4sRoute { case req @ DELETE -> `prefixPath` / "management" / "api-product-subscriptions" / apiProductSubscriptionId => EndpointHelpers.withUserDelete(req) { (user, cc) => for { @@ -6468,7 +6468,7 @@ object Http4s700 { ).disableAutoValidateRoles() // Route: POST /obp/v7.0.0/management/api-product-subscriptions/API_PRODUCT_SUBSCRIPTION_ID/attribute (201) - val createApiProductSubscriptionAttribute: HttpRoutes[IO] = HttpRoutes.of[IO] { + val createApiProductSubscriptionAttribute: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "management" / "api-product-subscriptions" / apiProductSubscriptionId / "attribute" => EndpointHelpers.executeFutureCreated(req) { implicit val cc: CallContext = req.callContext @@ -6507,7 +6507,7 @@ object Http4s700 { ).disableAutoValidateRoles() // Route: PUT /obp/v7.0.0/management/api-product-subscriptions/API_PRODUCT_SUBSCRIPTION_ID/attributes/API_PRODUCT_SUBSCRIPTION_ATTRIBUTE_ID - val updateApiProductSubscriptionAttribute: HttpRoutes[IO] = HttpRoutes.of[IO] { + val updateApiProductSubscriptionAttribute: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "management" / "api-product-subscriptions" / apiProductSubscriptionId / "attributes" / attributeId => EndpointHelpers.withUser(req) { (user, cc) => val rawBody = cc.httpBody.getOrElse("") @@ -6547,7 +6547,7 @@ object Http4s700 { ).disableAutoValidateRoles() // Route: GET /obp/v7.0.0/management/api-product-subscriptions/API_PRODUCT_SUBSCRIPTION_ID/attributes - val getApiProductSubscriptionAttributes: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getApiProductSubscriptionAttributes: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "management" / "api-product-subscriptions" / apiProductSubscriptionId / "attributes" => EndpointHelpers.withUser(req) { (user, cc) => for { @@ -6578,7 +6578,7 @@ object Http4s700 { ).disableAutoValidateRoles() // Route: DELETE /obp/v7.0.0/management/api-product-subscriptions/API_PRODUCT_SUBSCRIPTION_ID/attributes/API_PRODUCT_SUBSCRIPTION_ATTRIBUTE_ID - val deleteApiProductSubscriptionAttribute: HttpRoutes[IO] = HttpRoutes.of[IO] { + val deleteApiProductSubscriptionAttribute: Http4sRoute = Http4sRoute { case req @ DELETE -> `prefixPath` / "management" / "api-product-subscriptions" / apiProductSubscriptionId / "attributes" / attributeId => EndpointHelpers.withUserDelete(req) { (user, cc) => for { @@ -6688,7 +6688,7 @@ object Http4s700 { |Approval is system level. Dynamic code runs in the shared JVM, so a bank-level artefact is approved by the same system-level checker; there are no bank-level change request endpoints. |""".stripMargin - val createDynamicChangeRequest: HttpRoutes[IO] = HttpRoutes.of[IO] { + val createDynamicChangeRequest: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "management" / "dynamic-change-requests" => EndpointHelpers.executeFutureCreated(req) { implicit val cc: CallContext = req.callContext @@ -6746,7 +6746,7 @@ object Http4s700 { http4sPartialFunction = Some(createDynamicChangeRequest) ) - val getDynamicChangeRequests: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getDynamicChangeRequests: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "management" / "dynamic-change-requests" => EndpointHelpers.withUser(req) { (_, cc) => val q = req.uri.query.multiParams @@ -6777,7 +6777,7 @@ object Http4s700 { http4sPartialFunction = Some(getDynamicChangeRequests) ) - val getMyDynamicChangeRequests: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getMyDynamicChangeRequests: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "my" / "dynamic-change-requests" => EndpointHelpers.withUser(req) { (u, cc) => Future(changeRequestProvider.getByRequestorUserId(u.userId).map(MakerChecker.expireIfDue)) @@ -6814,7 +6814,7 @@ object Http4s700 { if (q.size() >= dynamicCompileCallsPerMinute) false else { q.addLast(now); true } } - val compileDynamicResourceDoc: HttpRoutes[IO] = HttpRoutes.of[IO] { + val compileDynamicResourceDoc: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "management" / "dynamic-resource-docs" / "compile" => EndpointHelpers.withUser(req) { (u, cc) => import code.api.v7_0_0.JSONFactory700.{DynamicCompileErrorJsonV700, DynamicCompileResultJsonV700, DynamicResourceDocCompileJsonV700} @@ -6919,7 +6919,7 @@ object Http4s700 { // Route: POST /obp/v7.0.0/management/dynamic-resource-docs/explain // For the author of a Dynamic Query: the statements it would run (SQL with ? for values) and the access // it needs, for the requesting User or for an anonymous caller. Reads no record. - val explainDynamicQuery: HttpRoutes[IO] = HttpRoutes.of[IO] { + val explainDynamicQuery: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "management" / "dynamic-resource-docs" / "explain" => EndpointHelpers.withUser(req) { (u, cc) => import code.api.v7_0_0.JSONFactory700.{DynamicQueryExplainJsonV700, createDynamicQueryExplanationJsonV700} @@ -6985,7 +6985,7 @@ object Http4s700 { // Route: GET /obp/v7.0.0/management/dynamic-code-approval-config // Lets a client (the API Manager create/edit pages) tell the maker up front whether a write will be // applied or queued for approval. Authenticated, no role: any user who can create an artefact needs this. - val getDynamicCodeApprovalConfig: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getDynamicCodeApprovalConfig: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "management" / "dynamic-code-approval-config" => EndpointHelpers.withUser(req) { (_, _) => Future.successful(JSONFactory700.DynamicCodeApprovalConfigJsonV700( @@ -7023,7 +7023,7 @@ object Http4s700 { http4sPartialFunction = Some(getDynamicCodeApprovalConfig) ) - val getDynamicChangeRequest: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getDynamicChangeRequest: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "management" / "dynamic-change-requests" / changeRequestId if changeRequestId.nonEmpty => EndpointHelpers.withUser(req) { (_, cc) => loadChangeRequest(changeRequestId, cc).map(JSONFactory700.createDynamicChangeRequestJsonV700) @@ -7048,7 +7048,7 @@ object Http4s700 { http4sPartialFunction = Some(getDynamicChangeRequest) ) - val approveDynamicChangeRequest: HttpRoutes[IO] = HttpRoutes.of[IO] { + val approveDynamicChangeRequest: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "management" / "dynamic-change-requests" / changeRequestId / "approval" if changeRequestId.nonEmpty => EndpointHelpers.withUser(req) { (u, cc) => implicit val c: CallContext = cc @@ -7082,7 +7082,7 @@ object Http4s700 { http4sPartialFunction = Some(approveDynamicChangeRequest) ) - val rejectDynamicChangeRequest: HttpRoutes[IO] = HttpRoutes.of[IO] { + val rejectDynamicChangeRequest: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "management" / "dynamic-change-requests" / changeRequestId / "rejection" if changeRequestId.nonEmpty => EndpointHelpers.withUser(req) { (u, cc) => implicit val c: CallContext = cc @@ -7115,7 +7115,7 @@ object Http4s700 { http4sPartialFunction = Some(rejectDynamicChangeRequest) ) - val withdrawDynamicChangeRequest: HttpRoutes[IO] = HttpRoutes.of[IO] { + val withdrawDynamicChangeRequest: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "management" / "dynamic-change-requests" / changeRequestId / "withdrawal" if changeRequestId.nonEmpty => EndpointHelpers.withUser(req) { (u, cc) => implicit val c: CallContext = cc @@ -7172,7 +7172,7 @@ object Http4s700 { private val deactivationExample = changeRequestExample.copy(operation = "DEACTIVATE", status = "APPROVED", target_id = "0d1c9e3c-6c2b-4c1e-9a53-2d5b2f0d7f22", request_path = "", proposed_payload = Extraction.decompose(Map("is_active" -> false)), checker_user_id = code.api.util.ExampleValue.userIdExample.value, actioned_at = APIUtil.DateWithMsExampleString) - val deactivateDynamicResourceDoc: HttpRoutes[IO] = HttpRoutes.of[IO] { + val deactivateDynamicResourceDoc: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "management" / "dynamic-resource-docs" / dynamicResourceDocId / "deactivation" if dynamicResourceDocId.nonEmpty => deactivateArtefact(req, DYNAMIC_RESOURCE_DOC, dynamicResourceDocId) } @@ -7191,7 +7191,7 @@ object Http4s700 { http4sPartialFunction = Some(deactivateDynamicResourceDoc) ) - val deactivateConnectorMethod: HttpRoutes[IO] = HttpRoutes.of[IO] { + val deactivateConnectorMethod: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "management" / "connector-methods" / connectorMethodId / "deactivation" if connectorMethodId.nonEmpty => deactivateArtefact(req, CONNECTOR_METHOD, connectorMethodId) } @@ -7210,7 +7210,7 @@ object Http4s700 { http4sPartialFunction = Some(deactivateConnectorMethod) ) - val deactivateDynamicMessageDoc: HttpRoutes[IO] = HttpRoutes.of[IO] { + val deactivateDynamicMessageDoc: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "management" / "dynamic-message-docs" / dynamicMessageDocId / "deactivation" if dynamicMessageDocId.nonEmpty => deactivateArtefact(req, DYNAMIC_MESSAGE_DOC, dynamicMessageDocId) } @@ -7229,7 +7229,7 @@ object Http4s700 { http4sPartialFunction = Some(deactivateDynamicMessageDoc) ) - val deactivateAbacRule: HttpRoutes[IO] = HttpRoutes.of[IO] { + val deactivateAbacRule: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "management" / "abac-rules" / abacRuleId / "deactivation" if abacRuleId.nonEmpty => deactivateArtefact(req, ABAC_RULE, abacRuleId) } @@ -7266,7 +7266,7 @@ object Http4s700 { // ─── getConsumerRateLimits ───────────────────────────────────────────── // Every per-consumer rate limit row on the instance, so an operator can see what overrides the // consumer limiter's defaults without opening each consumer. Per-consumer reads stay in v5.1.0 / v6.0.0. - lazy val getConsumerRateLimits: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getConsumerRateLimits: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "management" / "rate-limits" => EndpointHelpers.withUser(req) { (_, _) => for { @@ -7310,7 +7310,7 @@ object Http4s700 { ) // ─── getRateLimiterConfig ────────────────────────────────────────────── - lazy val getRateLimiterConfig: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getRateLimiterConfig: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "management" / "rate-limiter-config" => EndpointHelpers.withUser(req) { (_, _) => Future(JSONFactory700.createRateLimitersJsonV700()) @@ -7374,7 +7374,7 @@ object Http4s700 { code.users.Users.users.vend .attributedUserId(userId, code.users.UserReference.UserCustomerLink_UserId).openOr(userId) - val getMyCustomersAtBank: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getMyCustomersAtBank: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "my" / "customers" => EndpointHelpers.withUserAndBank(req) { (user, bank, cc) => for { @@ -7411,7 +7411,7 @@ object Http4s700 { http4sPartialFunction = Some(getMyCustomersAtBank) ) - val getMyCustomers: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getMyCustomers: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "my" / "customers" => EndpointHelpers.withUser(req) { (user, cc) => val callerIsConsentUser = user.isConsentUser @@ -7482,9 +7482,14 @@ object Http4s700 { // The asset registry: the currencies, metals, accounting units, crypto assets and bank-issued assets amounts are held in. resourceDocs ++= Http4s700Assets.resourceDocs + // The order in which the routes are tried. ResourceDocMiddleware selects the doc of the first + // route that serves a request, so it is given this same list: the doc that validates a request + // is the doc of the route that runs it. + val orderedResourceDocs: ArrayBuffer[ResourceDoc] = + resourceDocs.sortBy(rd => -rd.requestUrl.split("/").count(_.nonEmpty)) + val allRoutes: HttpRoutes[IO] = { - val sorted = resourceDocs - .sortBy(rd => -rd.requestUrl.split("/").count(_.nonEmpty)) + val sorted = orderedResourceDocs .flatMap(_.http4sPartialFunction) .map(_.routes) sorted.foldLeft(HttpRoutes.empty[IO]) { (acc, route) => @@ -7497,23 +7502,20 @@ object Http4s700 { // before the idempotency scope key is computed; on a cache hit the inner // routes (and any DB transaction) are skipped. val allRoutesWithMiddleware: HttpRoutes[IO] = - ResourceDocMiddleware.apply(resourceDocs)(IdempotencyMiddleware(allRoutes)) + ResourceDocMiddleware.apply(orderedResourceDocs, routes => IdempotencyMiddleware(routes))(IdempotencyMiddleware(allRoutes)) } // ─── path-rewriting bridge: /obp/v7.0.0/… → /obp/v6.0.0/… ───────────── - // Catches v7.0.0 paths with NO matching v7 ResourceDoc and forwards them to - // Http4s600 (which has all 243 v6.0.0 endpoints). Paths that DO have a v7 - // ResourceDoc are intentionally excluded: if the middleware returned + // Catches v7.0.0 paths that NO v7 route serves and forwards them to + // Http4s600 (which has all 243 v6.0.0 endpoints). Paths that a v7 route DOES + // serve are intentionally excluded: if the middleware returned // OptionT.none for such a path (e.g. api_disabled_endpoints), the bridge must - // not silently re-serve them from v6. The index is built lazily from the same - // resourceDocs buffer that the middleware uses, so it stays in sync. - private lazy val v7ResourceDocIndex: ResourceDocMatcher.ResourceDocIndex = - ResourceDocMatcher.buildIndex(resourceDocs) - + // not silently re-serve them from v6. "Served by a v7 route" is asked of the + // routes themselves, through the same ordered docs the middleware selects from. private val v700ToV600Bridge: HttpRoutes[IO] = Kleisli[HttpF, Request[IO], Response[IO]] { req => val rawPath = req.uri.path.renderString if (rawPath.startsWith("/obp/v7.0.0/") && - ResourceDocMatcher.findResourceDoc(req.method.name, req.uri.path, v7ResourceDocIndex).isEmpty) { + ResourceDocMatcher.selectByRoute(req, Implementations7_0_0.orderedResourceDocs).isEmpty) { val rewritten = rawPath.replaceFirst("/obp/v7\\.0\\.0/", "/obp/v6.0.0/") val newUri = req.uri.withPath(Uri.Path.unsafeFromString(rewritten)) code.api.v6_0_0.Http4s600.wrappedRoutesV600Services.run(req.withUri(newUri)) diff --git a/obp-api/src/main/scala/code/api/v7_0_0/Http4s700Assets.scala b/obp-api/src/main/scala/code/api/v7_0_0/Http4s700Assets.scala index 818bd46f18..bc9fa24b00 100644 --- a/obp-api/src/main/scala/code/api/v7_0_0/Http4s700Assets.scala +++ b/obp-api/src/main/scala/code/api/v7_0_0/Http4s700Assets.scala @@ -28,7 +28,7 @@ package code.api.v7_0_0 import cats.effect.IO import code.api.Constant.ApiPathZero -import code.api.util.APIUtil.{EmptyBody, ResourceDoc, unboxFullOrFail} +import code.api.util.APIUtil.{EmptyBody, ResourceDoc, unboxFullOrFail, Http4sRoute} import code.api.util.ApiTag._ import code.api.util.ErrorMessages._ import code.api.util.CustomJsonFormats @@ -69,7 +69,7 @@ object Http4s700Assets { val MaxPageSize = 500 // Route: GET /obp/v7.0.0/assets - lazy val getAssets: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getAssets: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "assets" => EndpointHelpers.executeAndRespond(req) { cc => val parameters = req.uri.query.params @@ -137,7 +137,7 @@ object Http4s700Assets { ) // Route: GET /obp/v7.0.0/assets/ASSET_CODE - lazy val getAsset: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getAsset: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "assets" / assetCode if assetCode.nonEmpty => EndpointHelpers.executeAndRespond(req) { cc => Future(Assets.getAsset(assetCode)) @@ -167,7 +167,7 @@ object Http4s700Assets { ) // Route: GET /obp/v7.0.0/assets/chain/CHAIN_SCHEME/CHAIN_ASSET_ID - lazy val getAssetByChainIdentity: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getAssetByChainIdentity: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "assets" / "chain" / chainScheme / chainAssetId if chainScheme.nonEmpty && chainAssetId.nonEmpty => EndpointHelpers.executeAndRespond(req) { cc => Future(Assets.getAssetByChainIdentity(chainScheme.toUpperCase, chainAssetId)) diff --git a/obp-api/src/main/scala/code/api/v7_0_0/Http4s700DeploymentChecks.scala b/obp-api/src/main/scala/code/api/v7_0_0/Http4s700DeploymentChecks.scala index a4213bee3f..0692421dc2 100644 --- a/obp-api/src/main/scala/code/api/v7_0_0/Http4s700DeploymentChecks.scala +++ b/obp-api/src/main/scala/code/api/v7_0_0/Http4s700DeploymentChecks.scala @@ -28,7 +28,7 @@ package code.api.v7_0_0 import cats.effect.IO import code.api.Constant.ApiPathZero -import code.api.util.APIUtil.{EmptyBody, ResourceDoc} +import code.api.util.APIUtil.{EmptyBody, ResourceDoc, Http4sRoute} import code.api.util.ApiRole._ import code.api.util.ApiTag._ import code.api.util.ErrorMessages._ @@ -61,7 +61,7 @@ object Http4s700DeploymentChecks { val resourceDocs = ArrayBuffer[ResourceDoc]() // Route: GET /obp/v7.0.0/management/system/diagnostics/deployment - lazy val getDeploymentChecks: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getDeploymentChecks: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "management" / "system" / "diagnostics" / "deployment" => EndpointHelpers.withUser(req) { (_, _) => Future(JSONFactory700Operations.createDeploymentChecksJson(DeploymentChecks.run())) diff --git a/obp-api/src/main/scala/code/api/v7_0_0/Http4s700DomainApis.scala b/obp-api/src/main/scala/code/api/v7_0_0/Http4s700DomainApis.scala index 0a3cf45bfb..76640ff2b2 100644 --- a/obp-api/src/main/scala/code/api/v7_0_0/Http4s700DomainApis.scala +++ b/obp-api/src/main/scala/code/api/v7_0_0/Http4s700DomainApis.scala @@ -30,7 +30,7 @@ import cats.effect.IO import code.api.Constant.ApiPathZero import code.api.dynamic.domainapi.DomainApiPaths import code.api.dynamic.entity.helper.DynamicEntitySpace -import code.api.util.APIUtil.{EmptyBody, ResourceDoc} +import code.api.util.APIUtil.{EmptyBody, ResourceDoc, Http4sRoute} import code.api.util.ApiRole._ import code.api.util.ApiTag._ import code.api.util.ErrorMessages._ @@ -156,7 +156,7 @@ object Http4s700DomainApis { DomainApiPathClash, UnknownError) // Route: POST /obp/v7.0.0/management/banks/BANK_ID/domain-apis (201) - lazy val createDomainApi: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val createDomainApi: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "management" / "banks" / bankIdInUrl / "domain-apis" => EndpointHelpers.withUserAndBodyCreated[PostDomainApiJsonV700, DomainApiJsonV700](req) { (user, body, cc) => val spaceId = space(bankIdInUrl) @@ -186,7 +186,7 @@ object Http4s700DomainApis { ).allowSystemSpace() // Route: GET /obp/v7.0.0/management/banks/BANK_ID/domain-apis - lazy val getDomainApis: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getDomainApis: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "management" / "banks" / bankIdInUrl / "domain-apis" => EndpointHelpers.withUser(req) { (_, cc) => Future(provider.getAll(space(bankIdInUrl))).map(box => @@ -212,7 +212,7 @@ object Http4s700DomainApis { ).allowSystemSpace() // Route: GET /obp/v7.0.0/management/banks/BANK_ID/domain-apis/DOMAIN_API_ID - lazy val getDomainApi: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getDomainApi: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "management" / "banks" / bankIdInUrl / "domain-apis" / domainApiId => EndpointHelpers.withUser(req) { (_, cc) => found(space(bankIdInUrl), domainApiId, cc).map(JSONFactory700DomainApis.createDomainApiJson) @@ -237,7 +237,7 @@ object Http4s700DomainApis { ).allowSystemSpace() // Route: PUT /obp/v7.0.0/management/banks/BANK_ID/domain-apis/DOMAIN_API_ID - lazy val updateDomainApi: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val updateDomainApi: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "management" / "banks" / bankIdInUrl / "domain-apis" / domainApiId => EndpointHelpers.withUserAndBody[PostDomainApiJsonV700, DomainApiJsonV700](req) { (_, body, cc) => val spaceId = space(bankIdInUrl) @@ -270,7 +270,7 @@ object Http4s700DomainApis { ).allowSystemSpace() // Route: DELETE /obp/v7.0.0/management/banks/BANK_ID/domain-apis/DOMAIN_API_ID (204) - lazy val deleteDomainApi: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val deleteDomainApi: Http4sRoute = Http4sRoute { case req @ DELETE -> `prefixPath` / "management" / "banks" / bankIdInUrl / "domain-apis" / domainApiId => EndpointHelpers.withUserDelete(req) { (_, cc) => val spaceId = space(bankIdInUrl) diff --git a/obp-api/src/main/scala/code/api/v7_0_0/Http4s700DynamicEntityDefinitions.scala b/obp-api/src/main/scala/code/api/v7_0_0/Http4s700DynamicEntityDefinitions.scala index 119d4b8388..6dfb6cdc40 100644 --- a/obp-api/src/main/scala/code/api/v7_0_0/Http4s700DynamicEntityDefinitions.scala +++ b/obp-api/src/main/scala/code/api/v7_0_0/Http4s700DynamicEntityDefinitions.scala @@ -30,7 +30,7 @@ import cats.effect.IO import code.DynamicData.DynamicData import code.api.dynamic.entity.helper.DynamicEntitySpace import code.api.Constant.ApiPathZero -import code.api.util.APIUtil.{EmptyBody, ResourceDoc, UserOrApplication} +import code.api.util.APIUtil.{EmptyBody, ResourceDoc, UserOrApplication, Http4sRoute} import code.api.util.ApiRole._ import code.api.util.ApiTag._ import code.api.util.ErrorMessages._ @@ -112,7 +112,7 @@ object Http4s700DynamicEntityDefinitions { |Every response carries `bank_id`, `SYS` included.""".stripMargin // Route: GET /obp/v7.0.0/management/banks/BANK_ID/dynamic-entities - lazy val getDynamicEntityDefinitions: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getDynamicEntityDefinitions: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "management" / "banks" / bankIdInUrl / "dynamic-entities" => EndpointHelpers.executeAndRespond(req) { _ => val bankId = DynamicEntitySpace.bankIdOrNoneForSystem(bankIdInUrl) @@ -166,7 +166,7 @@ object Http4s700DynamicEntityDefinitions { ).allowSystemSpace() // Route: POST /obp/v7.0.0/management/banks/BANK_ID/dynamic-entities (201) - lazy val createDynamicEntityDefinition: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val createDynamicEntityDefinition: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "management" / "banks" / bankIdInUrl / "dynamic-entities" => EndpointHelpers.executeFutureCreated(req) { implicit val cc: CallContext = req.callContext @@ -217,7 +217,7 @@ object Http4s700DynamicEntityDefinitions { ).allowSystemSpace() // Route: PUT /obp/v7.0.0/management/banks/BANK_ID/dynamic-entities/DYNAMIC_ENTITY_ID (200) - lazy val updateDynamicEntityDefinition: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val updateDynamicEntityDefinition: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "management" / "banks" / bankIdInUrl / "dynamic-entities" / dynamicEntityId => EndpointHelpers.executeAndRespond(req) { implicit cc => val rawBody = cc.httpBody.getOrElse("") @@ -260,7 +260,7 @@ object Http4s700DynamicEntityDefinitions { ).allowSystemSpace() // Route: DELETE /obp/v7.0.0/management/banks/BANK_ID/dynamic-entities/DYNAMIC_ENTITY_ID (204) - lazy val deleteDynamicEntityDefinition: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val deleteDynamicEntityDefinition: Http4sRoute = Http4sRoute { case req @ DELETE -> `prefixPath` / "management" / "banks" / bankIdInUrl / "dynamic-entities" / dynamicEntityId => EndpointHelpers.executeDelete(req) { cc => Implementations4_0_0.deleteDynamicEntityImpl(DynamicEntitySpace.bankIdOrNoneForSystem(bankIdInUrl), dynamicEntityId, cc) @@ -289,7 +289,7 @@ object Http4s700DynamicEntityDefinitions { ).allowSystemSpace() // Route: POST /obp/v7.0.0/management/banks/BANK_ID/dynamic-entities/DYNAMIC_ENTITY_ID/backup (201) - lazy val backupDynamicEntityDefinition: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val backupDynamicEntityDefinition: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "management" / "banks" / bankIdInUrl / "dynamic-entities" / dynamicEntityId / "backup" => EndpointHelpers.executeFutureCreated(req) { implicit val cc: CallContext = req.callContext @@ -322,7 +322,7 @@ object Http4s700DynamicEntityDefinitions { ).allowSystemSpace() // Route: DELETE /obp/v7.0.0/management/banks/BANK_ID/dynamic-entities/cascade/DYNAMIC_ENTITY_ID (204) - lazy val deleteDynamicEntityDefinitionCascade: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val deleteDynamicEntityDefinitionCascade: Http4sRoute = Http4sRoute { case req @ DELETE -> `prefixPath` / "management" / "banks" / bankIdInUrl / "dynamic-entities" / "cascade" / dynamicEntityId => EndpointHelpers.executeDelete(req) { cc => Implementations6_0_0.deleteDynamicEntityCascadeFut(DynamicEntitySpace.bankIdOrNoneForSystem(bankIdInUrl), dynamicEntityId, cc) diff --git a/obp-api/src/main/scala/code/api/v7_0_0/Http4s700Groups.scala b/obp-api/src/main/scala/code/api/v7_0_0/Http4s700Groups.scala index 2e88e8647e..884481a6ee 100644 --- a/obp-api/src/main/scala/code/api/v7_0_0/Http4s700Groups.scala +++ b/obp-api/src/main/scala/code/api/v7_0_0/Http4s700Groups.scala @@ -29,7 +29,7 @@ package code.api.v7_0_0 import cats.effect.IO import code.api.Constant import code.api.Constant.ApiPathZero -import code.api.util.APIUtil.{EmptyBody, ResourceDoc} +import code.api.util.APIUtil.{EmptyBody, ResourceDoc, Http4sRoute} import code.api.util.ApiRole._ import code.api.util.ApiTag._ import code.api.util.ErrorMessages._ @@ -138,7 +138,7 @@ object Http4s700Groups { } // Route: POST /obp/v7.0.0/management/groups/GROUP_ID/sync-members - lazy val syncGroupMembers: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val syncGroupMembers: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "management" / "groups" / groupId / "sync-members" => EndpointHelpers.withUser(req) { (user, cc) => val dryRun = isDryRun(req) @@ -227,7 +227,7 @@ object Http4s700Groups { } // Route: POST /obp/v7.0.0/management/groups/GROUP_ID/users/USER_ID/sync - lazy val syncGroupMember: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val syncGroupMember: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "management" / "groups" / groupId / "users" / userId / "sync" => EndpointHelpers.withUser(req) { (user, cc) => val dryRun = isDryRun(req) @@ -293,7 +293,7 @@ object Http4s700Groups { ) // Route: POST /obp/v7.0.0/management/users/USER_ID/sync-groups - lazy val syncUserGroups: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val syncUserGroups: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "management" / "users" / userId / "sync-groups" => EndpointHelpers.withUser(req) { (user, cc) => val dryRun = isDryRun(req) diff --git a/obp-api/src/main/scala/code/api/v7_0_0/Http4s700IpPenalties.scala b/obp-api/src/main/scala/code/api/v7_0_0/Http4s700IpPenalties.scala index bbf2440820..9e7f847217 100644 --- a/obp-api/src/main/scala/code/api/v7_0_0/Http4s700IpPenalties.scala +++ b/obp-api/src/main/scala/code/api/v7_0_0/Http4s700IpPenalties.scala @@ -28,7 +28,7 @@ package code.api.v7_0_0 import cats.effect.IO import code.api.Constant.ApiPathZero -import code.api.util.APIUtil.{EmptyBody, ResourceDoc} +import code.api.util.APIUtil.{EmptyBody, ResourceDoc, Http4sRoute} import code.api.util.ApiRole._ import code.api.util.ApiTag._ import code.api.util.ErrorMessages._ @@ -73,7 +73,7 @@ object Http4s700IpPenalties { |everyone behind the proxy.""".stripMargin // Route: POST /obp/v7.0.0/management/ip-penalties - lazy val createIpPenalty: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val createIpPenalty: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "management" / "ip-penalties" => EndpointHelpers.withUserAndBodyCreated[PostIpPenaltyJsonV700, IpPenaltyJsonV700](req) { (user, body, cc) => for { @@ -118,7 +118,7 @@ object Http4s700IpPenalties { ) // Route: GET /obp/v7.0.0/management/ip-penalties - lazy val getIpPenalties: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getIpPenalties: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "management" / "ip-penalties" => EndpointHelpers.withUser(req) { (_, _) => Future(IpPenaltiesJsonV700(IpPenalties.listAll().map(JSONFactory700Operations.createIpPenaltyJson))) @@ -144,7 +144,7 @@ object Http4s700IpPenalties { ) // Route: DELETE /obp/v7.0.0/management/ip-penalties/IP_ADDRESS - lazy val deleteIpPenalty: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val deleteIpPenalty: Http4sRoute = Http4sRoute { case req @ DELETE -> `prefixPath` / "management" / "ip-penalties" / ipAddress => EndpointHelpers.withUserDelete(req) { (_, cc) => for { diff --git a/obp-api/src/main/scala/code/api/v7_0_0/Http4s700PlatformApps.scala b/obp-api/src/main/scala/code/api/v7_0_0/Http4s700PlatformApps.scala index bf34621ba9..d325914cca 100644 --- a/obp-api/src/main/scala/code/api/v7_0_0/Http4s700PlatformApps.scala +++ b/obp-api/src/main/scala/code/api/v7_0_0/Http4s700PlatformApps.scala @@ -28,7 +28,7 @@ package code.api.v7_0_0 import cats.effect.IO import code.api.Constant.ApiPathZero -import code.api.util.APIUtil.{EmptyBody, ResourceDoc, UserOrApplication} +import code.api.util.APIUtil.{EmptyBody, ResourceDoc, UserOrApplication, Http4sRoute} import code.api.util.ApiRole._ import code.api.util.ApiTag._ import code.api.util.ErrorMessages._ @@ -103,7 +103,7 @@ object Http4s700PlatformApps { |For more information see ${Glossary.getGlossaryItemLink("Platform Apps")}""".stripMargin // Route: POST /obp/v7.0.0/management/platform-apps (201) - lazy val createPlatformApp: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val createPlatformApp: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "management" / "platform-apps" => EndpointHelpers.withUserAndBodyCreated[PostPlatformAppJsonV700, PlatformAppJsonV700](req) { (user, body, cc) => val label = Option(body.label).map(_.trim).getOrElse("") @@ -144,7 +144,7 @@ object Http4s700PlatformApps { ) // Route: GET /obp/v7.0.0/management/platform-apps - lazy val getPlatformApps: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getPlatformApps: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "management" / "platform-apps" => EndpointHelpers.withUser(req) { (_, cc) => Future(provider.getPlatformApps()) map { x => @@ -175,7 +175,7 @@ object Http4s700PlatformApps { ) // Route: DELETE /obp/v7.0.0/management/platform-apps/CONSUMER_ID (204) - lazy val deletePlatformApp: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val deletePlatformApp: Http4sRoute = Http4sRoute { case req @ DELETE -> `prefixPath` / "management" / "platform-apps" / consumerId => EndpointHelpers.withUserDelete(req) { (_, cc) => for { @@ -209,7 +209,7 @@ object Http4s700PlatformApps { // Route: PUT /obp/v7.0.0/consumers/current/platform-app // No Role: an app may always say what it needs. It is refused for a Consumer an administrator has not // marked, so only the apps an administrator chose appear on the list. - lazy val updateCurrentConsumerPlatformApp: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val updateCurrentConsumerPlatformApp: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "consumers" / "current" / "platform-app" => EndpointHelpers.executeFutureWithBody[PutPlatformAppDeclarationJsonV700, PlatformAppJsonV700](req) { (body, cc) => for { diff --git a/obp-api/src/main/scala/code/api/v7_0_0/Http4s700Telemetry.scala b/obp-api/src/main/scala/code/api/v7_0_0/Http4s700Telemetry.scala index 78613576f0..72abd401e9 100644 --- a/obp-api/src/main/scala/code/api/v7_0_0/Http4s700Telemetry.scala +++ b/obp-api/src/main/scala/code/api/v7_0_0/Http4s700Telemetry.scala @@ -28,7 +28,7 @@ package code.api.v7_0_0 import cats.effect.IO import code.api.Constant.ApiPathZero -import code.api.util.APIUtil.{EmptyBody, ResourceDoc, UserOrApplication} +import code.api.util.APIUtil.{EmptyBody, ResourceDoc, UserOrApplication, Http4sRoute} import code.api.util.ApiRole._ import code.api.util.ApiTag._ import code.api.util.ErrorMessages._ @@ -62,7 +62,7 @@ object Http4s700Telemetry { val resourceDocs = ArrayBuffer[ResourceDoc]() // Route: GET /obp/v7.0.0/management/telemetry - lazy val getTelemetry: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getTelemetry: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "management" / "telemetry" => EndpointHelpers.executeFuture(req) { val namePrefix = req.uri.query.params.get("name_prefix").filter(_.nonEmpty) diff --git a/obp-api/src/main/scala/code/api/v7_0_0/Http4s700TrafficSources.scala b/obp-api/src/main/scala/code/api/v7_0_0/Http4s700TrafficSources.scala index c939d4169d..a4984d2d87 100644 --- a/obp-api/src/main/scala/code/api/v7_0_0/Http4s700TrafficSources.scala +++ b/obp-api/src/main/scala/code/api/v7_0_0/Http4s700TrafficSources.scala @@ -28,7 +28,7 @@ package code.api.v7_0_0 import cats.effect.IO import code.api.Constant.ApiPathZero -import code.api.util.APIUtil.{EmptyBody, ResourceDoc} +import code.api.util.APIUtil.{EmptyBody, ResourceDoc, Http4sRoute} import code.api.util.ApiRole._ import code.api.util.ApiTag._ import code.api.util.ErrorMessages._ @@ -65,7 +65,7 @@ object Http4s700TrafficSources { private val AllowedWindows = Set(1, 5, 15) // Route: GET /obp/v7.0.0/management/traffic/top-callers - lazy val getTrafficSources: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getTrafficSources: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "management" / "traffic" / "top-callers" => EndpointHelpers.withUser(req) { (_, cc) => val window = req.uri.query.params.get("window").map(_.trim).getOrElse("5") diff --git a/obp-api/src/test/scala/code/api/util/http4s/ResourceDocRouteBindingTest.scala b/obp-api/src/test/scala/code/api/util/http4s/ResourceDocRouteBindingTest.scala new file mode 100644 index 0000000000..cb7c6a3ee0 --- /dev/null +++ b/obp-api/src/test/scala/code/api/util/http4s/ResourceDocRouteBindingTest.scala @@ -0,0 +1,101 @@ +/** +Open Bank Project - API +Copyright (C) 2011-2026, TESOBE GmbH. + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . + +Email: contact@tesobe.com +TESOBE GmbH. +Osloer Strasse 16/17 +Berlin 13359, Germany + +This product includes software developed at +TESOBE (http://www.tesobe.com/) + + */ + +package code.api.util.http4s + +import cats.effect.IO +import code.api.util.APIUtil.ResourceDoc +import code.setup.ServerSetup +import org.http4s.{Method, Request, Uri} +import org.scalatest.Tag + +import scala.collection.mutable.ArrayBuffer + +/** + * Every ResourceDoc of a converted catalog is bound to the route that serves it. + * + * For each doc the test sends the doc's own template as the request path. A placeholder in a route + * pattern accepts any string, so the template text stands in for a value, and a fixed word in the + * pattern needs exactly the word the template carries. The doc's own route must serve that + * request, and selecting by route must give back that same doc. + * + * A doc whose template has drifted away from its route fails here, which nothing else catches: + * the route still serves its real URLs, so its own tests pass. + */ +class ResourceDocRouteBindingTest extends ServerSetup { + + object ResourceDocRouteBindingTag extends Tag("ResourceDocRouteBinding") + + /** Each converted version's `resourceDocs` buffer. Naming the Implementations object fills it. */ + private def loaded(implementations: Any, docs: ArrayBuffer[ResourceDoc]): ArrayBuffer[ResourceDoc] = docs + + private def convertedCatalogs: List[(String, ArrayBuffer[ResourceDoc])] = List( + "v7.0.0" -> loaded(code.api.v7_0_0.Http4s700.Implementations7_0_0, code.api.v7_0_0.Http4s700.resourceDocs) + ) + + private def describe(doc: ResourceDoc): String = + s"${doc.partialFunctionName} (${doc.requestVerb} ${doc.requestUrl})" + + private def requestFor(doc: ResourceDoc): Request[IO] = { + val version = doc.implementedInApiVersion + Request[IO]( + Method.fromString(doc.requestVerb.toUpperCase).fold(throw _, identity), + Uri.unsafeFromString(s"/${version.urlPrefix}/$version${doc.requestUrl}") + ) + } + + /** One line per doc that is not bound to a route that serves its own URL. */ + private def unbound(docs: ArrayBuffer[ResourceDoc]): List[String] = { + val sameVerbAndUrlCount = docs.groupBy(doc => (doc.requestVerb.toUpperCase, doc.requestUrl)).view.mapValues(_.size).toMap + docs.toList.flatMap { doc => + doc.http4sPartialFunction.flatMap(_.route) match { + case None => Some(s"${describe(doc)} carries no Http4sRoute") + case Some(route) => + val req = requestFor(doc) + if (!route.isDefinedAt(req)) Some(s"${describe(doc)}: its own route does not serve its own URL") + else if (sameVerbAndUrlCount((doc.requestVerb.toUpperCase, doc.requestUrl)) > 1) None + else ResourceDocMatcher.selectByRoute(req, docs) match { + case Some(selected) if selected eq doc => None + case Some(selected) => Some(s"${describe(doc)} selects ${describe(selected)}") + case None => Some(s"${describe(doc)} selects no doc") + } + } + } + } + + feature("Every ResourceDoc of a converted catalog is bound to the route that serves it") { + convertedCatalogs.foreach { case (label, docs) => + scenario(s"$label: each doc's own route serves its own URL, and selection returns that doc", ResourceDocRouteBindingTag) { + docs should not be empty + val problems = unbound(docs) + withClue(s"\n${problems.mkString("\n")}\n") { + problems shouldBe empty + } + } + } + } +} From 99b47f55a88b6e39957d1052970954e2af4a885b Mon Sep 17 00:00:00 2001 From: Hongwei Date: Wed, 7 Oct 2026 09:20:41 +0000 Subject: [PATCH 04/29] docs: describe binding a ResourceDoc to its route Explain how an endpoint is declared as an Http4sRoute so the middleware selects its ResourceDoc by route, and how that differs from the template matching that versions not yet converted still use. --- CLAUDE.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index 3ae2cd705d..38be32eee3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -245,6 +245,8 @@ Note: `executeFutureCreated` returns 201; pair it with `cc.user.openOrThrowExcep **Reserved ALL_CAPS literals — don't use them as placeholders**: `ResourceDocMatcher` in `Http4sSupport.scala` treats every value of the `TransactionRequestTypes` and `StrongCustomerAuthentication` enums (plus `NOT_EMAIL_NEITHER_SMS`) as a **literal** URL segment: `literalAllCapsSegments` is built from those enums, so adding a transaction-request type to the enum is all it takes. Any other ALL_CAPS segment is a wildcard. Until 2026-10 the set was kept by hand, and `MOBILE_WALLET`, `UTILITY`, `BULK`, `HOLD`, `CARDANO` and the `ETH_*` types were never added: the v7 MOBILE_WALLET template then claimed every transaction-request type (UTILITY and BULK were validated under the MOBILE_WALLET doc; HOLD/CARDANO/ETH on v7 got an empty 404 instead of falling through to v6). When several templates match, the one with the most literal segments wins, whatever the registration order. `ResourceDocSelfResolveTest` checks that every registered doc's own URL resolves to that doc. If you migrate an endpoint whose URL template uses an enum value 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). +**A ResourceDoc is bound to its route (v7.0.0 so far)**: declare an endpoint as `val x: Http4sRoute = Http4sRoute { case req @ GET -> ... => ... }` (not `HttpRoutes.of[IO]`) and put it in the doc's `http4sPartialFunction = Some(x)`. `ResourceDocMiddleware` then picks the doc by asking the routes whether they serve the request (`ResourceDocMatcher.selectByRoute`, docs tried in the order the version passes them, the same order its `allRoutes` uses) and runs only that route, so the doc that validates a request is always the doc of the handler that serves it. For such docs the ALL_CAPS literal-vs-placeholder rule above is not consulted: the template is documentation (resource-docs, Swagger) and the route pattern decides. `ResourceDocRouteBindingTest` checks that each converted doc's own URL is served by its own route. Versions not yet converted still use the template matching described above; convert one version file at a time, and add the version to `ResourceDocRouteBindingTest.convertedCatalogs`. + **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. **Bridge-cascade hijack**: when a new version (e.g. v4.0.0) *overrides* an endpoint from an earlier version with the same URL + verb (e.g. v4's `POST /banks` adds entitlement-granting that v2.2.0's `POST /banks` doesn't have), the v4 override **must** be migrated to `Http4s400`'s own-routes **before** wiring `Http4s400` into the chain. Otherwise the path-rewriting bridge cascade silently sends the request to the older handler: From 058ebbde58295c39401a240ddd2b4eecc50d879f Mon Sep 17 00:00:00 2001 From: Hongwei Date: Wed, 7 Oct 2026 10:25:39 +0000 Subject: [PATCH 05/29] test: start TrafficSourcesEndpointTest from an empty traffic record GET /obp/v7.0.0/management/traffic/top-callers lists only the 50 busiest caller and endpoint pairs of the window, and the window is the current minute of the JVM. Each pair the test looks for has a single request, so when other suites had made more than 50 pairs in the same minute the pair was cut off and the test failed. It passed alone and failed after AuthSweepTest in one JVM. Clear the record at the start of the scenario so it only sees its own requests. --- .../scala/code/api/v7_0_0/TrafficSourcesEndpointTest.scala | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/obp-api/src/test/scala/code/api/v7_0_0/TrafficSourcesEndpointTest.scala b/obp-api/src/test/scala/code/api/v7_0_0/TrafficSourcesEndpointTest.scala index 12bdca1f3c..20b13486f7 100644 --- a/obp-api/src/test/scala/code/api/v7_0_0/TrafficSourcesEndpointTest.scala +++ b/obp-api/src/test/scala/code/api/v7_0_0/TrafficSourcesEndpointTest.scala @@ -5,6 +5,7 @@ import code.api.util.ApiRole.CanGetTrafficSources import code.api.util.ErrorMessages.{AuthenticatedUserIsRequired, UserHasMissingRoles} import code.api.v6_0_0.V600ServerSetup import code.entitlement.Entitlement +import code.telemetry.TrafficSources import com.openbankproject.commons.model.ErrorMessage import com.openbankproject.commons.util.ApiVersion import org.scalatest.Tag @@ -40,6 +41,10 @@ class TrafficSourcesEndpointTest extends V600ServerSetup { } scenario("requests appear under their Consumer, their address, and their endpoint", ApiEndpoint, VersionOfApi) { + // The endpoint lists only the 50 busiest caller and endpoint pairs of the window, and the window + // is the current minute of this JVM. Pairs that other suites made in the same minute can crowd + // out the single request of each pair below, so start from an empty record. + TrafficSources.clear() makeGetRequest((v7_0_0_Request / "banks").GET <@ (user1)).code should equal(200) makeGetRequest((v7_0_0_Request / "banks").GET).code should equal(200) makeGetRequest((v7_0_0_Request / "no-such-endpoint-traffic-probe").GET).code should equal(404) From a498ab918aebd8284c1f244ee137ead754f8eb8d Mon Sep 17 00:00:00 2001 From: Hongwei Date: Wed, 7 Oct 2026 12:52:13 +0000 Subject: [PATCH 06/29] fix: keep an empty path segment when reading the path parameters of a route-selected doc A route can serve a path with an empty segment, such as /banks/B/api-products//subscriptions, and the doc selected for it is the route's. extractPathParams dropped empty segments before comparing the path with the template, so the lengths differed and it returned no parameters. BANK_ID, ACCOUNT_ID and VIEW_ID were then never validated, and the role check asked for the role at bank "": a caller holding the role at bank B was refused with 403, and a handler that needs the bank or account answered 500. Read the parameters by position with the empty segment kept when that matches the template, and fall back to the previous reading otherwise, so docs that are still matched by template behave as before. --- .../code/api/util/http4s/Http4sSupport.scala | 11 +++++- .../util/http4s/ResourceDocMatcherTest.scala | 37 +++++++++++++++++++ .../EmptySegmentRoleValidationTest.scala | 19 +++++++++- 3 files changed, 64 insertions(+), 3 deletions(-) diff --git a/obp-api/src/main/scala/code/api/util/http4s/Http4sSupport.scala b/obp-api/src/main/scala/code/api/util/http4s/Http4sSupport.scala index 8bbcc56ad1..acd7a5bccf 100644 --- a/obp-api/src/main/scala/code/api/util/http4s/Http4sSupport.scala +++ b/obp-api/src/main/scala/code/api/util/http4s/Http4sSupport.scala @@ -972,9 +972,16 @@ object ResourceDocMatcher extends code.util.Helper.MdcLoggable { val pathString = path.renderString // Strip the API prefix (/obp/vX.X.X) from the path for matching val strippedPath = apiPrefixPattern.replaceFirstIn(pathString, "") - val pathSegments = strippedPath.split("/").filter(_.nonEmpty) val templateSegments = resourceDoc.requestUrl.split("/").filter(_.nonEmpty) - + // A route can serve a path with an empty segment (`/banks/B/api-products//subscriptions`), and the + // doc selected for it is the route's. Read the parameters by position with the empty segment kept, + // so BANK_ID and the other checks see the value the request carries (an empty one) instead of finding + // no parameters at all and skipping validation. + val positionalSegments = strippedPath.split("/", -1).drop(1) + val pathSegments = + if (positionalSegments.length == templateSegments.length) positionalSegments + else strippedPath.split("/").filter(_.nonEmpty) + if (pathSegments.length != templateSegments.length) { Map.empty } else { diff --git a/obp-api/src/test/scala/code/api/util/http4s/ResourceDocMatcherTest.scala b/obp-api/src/test/scala/code/api/util/http4s/ResourceDocMatcherTest.scala index 32bdea6ee9..d9b87477bd 100644 --- a/obp-api/src/test/scala/code/api/util/http4s/ResourceDocMatcherTest.scala +++ b/obp-api/src/test/scala/code/api/util/http4s/ResourceDocMatcherTest.scala @@ -635,4 +635,41 @@ class ResourceDocMatcherTest extends FeatureSpec with Matchers with GivenWhenThe enumValues.filterNot(ResourceDocMatcher.literalAllCapsSegments.contains) shouldBe empty } } + + feature("ResourceDocMatcher - path parameters of a path with an empty segment") { + + scenario("An empty segment is read by position, so the parameters of the other segments keep their place", ResourceDocMatcherTag) { + Given("a doc with BANK_ID before a placeholder, and a request that leaves the placeholder empty") + val resourceDoc = createResourceDoc("GET", "/banks/BANK_ID/api-products/API_PRODUCT_CODE/subscriptions", "getSubscriptions") + val path = Uri.Path.unsafeFromString(s"$base/banks/gh.29.uk/api-products//subscriptions") + + When("the path parameters are extracted") + val params = ResourceDocMatcher.extractPathParams(path, resourceDoc) + + Then("BANK_ID is the bank of the request, and the empty segment is an empty value") + params.get("BANK_ID") should be(Some("gh.29.uk")) + params.get("API_PRODUCT_CODE") should be(Some("")) + } + + scenario("An empty BANK_ID segment is an empty BANK_ID, not a missing one", ResourceDocMatcherTag) { + val resourceDoc = createResourceDoc("GET", "/banks/BANK_ID/accounts", "getAccounts") + val path = Uri.Path.unsafeFromString(s"$base/banks//accounts") + + ResourceDocMatcher.extractPathParams(path, resourceDoc).get("BANK_ID") should be(Some("")) + } + + scenario("A path whose segment count differs from the template still yields nothing", ResourceDocMatcherTag) { + val resourceDoc = createResourceDoc("GET", "/banks/BANK_ID/accounts", "getAccounts") + val path = Uri.Path.unsafeFromString(s"$base/banks/gh.29.uk/accounts/extra") + + ResourceDocMatcher.extractPathParams(path, resourceDoc) should be(Map.empty) + } + + scenario("A trailing slash does not change the parameters of the doc it was matched with", ResourceDocMatcherTag) { + val resourceDoc = createResourceDoc("GET", "/banks/BANK_ID", "getBank") + val path = Uri.Path.unsafeFromString(s"$base/banks/gh.29.uk/") + + ResourceDocMatcher.extractPathParams(path, resourceDoc).get("BANK_ID") should be(Some("gh.29.uk")) + } + } } diff --git a/obp-api/src/test/scala/code/api/v7_0_0/EmptySegmentRoleValidationTest.scala b/obp-api/src/test/scala/code/api/v7_0_0/EmptySegmentRoleValidationTest.scala index 5396bd49b4..f209b1314f 100644 --- a/obp-api/src/test/scala/code/api/v7_0_0/EmptySegmentRoleValidationTest.scala +++ b/obp-api/src/test/scala/code/api/v7_0_0/EmptySegmentRoleValidationTest.scala @@ -29,7 +29,9 @@ package code.api.v7_0_0 import cats.effect.unsafe.IORuntime import code.api.sweep.SweepFixtures +import code.api.util.ApiRole import code.api.util.ErrorMessages.UserHasMissingRoles +import code.entitlement.Entitlement import code.setup.{DefaultUsers, ServerSetupWithTestData} import org.json4s.native.JsonMethods.{compact, render} import org.scalatest.Tag @@ -44,7 +46,9 @@ import org.scalatest.Tag * matches, but the middleware counts segments, finds no doc and runs the handler without any * validation, so the caller gets the handler's own answer instead of the role check. * - * The caller here is authenticated and holds no role at all. + * The first two scenarios use a caller that is authenticated and holds no role at all. The third uses + * a caller that holds the role at the bank: the empty segment must not make the middleware check the + * role against a bank called "" or skip the bank, so the answer is the handler's own, not a 403 or a 500. */ class EmptySegmentRoleValidationTest extends ServerSetupWithTestData with DefaultUsers with SweepFixtures { @@ -73,5 +77,18 @@ class EmptySegmentRoleValidationTest extends ServerSetupWithTestData with Defaul compact(render(json)) should include(UserHasMissingRoles.take(10)) } } + + scenario("A caller with the role at the bank is not refused by the role check when a path parameter is empty", EmptySegmentRoleValidationTag) { + val entitlement = Entitlement.entitlement.vend.addEntitlement( + bank, resourceUser1.userId, ApiRole.canGetApiProductSubscriptionAtOneBank.toString) + try { + val (status, json) = callApi("GET", s"/obp/v7.0.0/banks/$bank/api-products//subscriptions", callerWithoutRoles) + withClue(s"body: ${compact(render(json))}\n") { + status should not be 403 + status should not be 500 + compact(render(json)) should not include UserHasMissingRoles.take(10) + } + } finally Entitlement.entitlement.vend.deleteEntitlement(entitlement) + } } } From 082ddb4c385b56d6f996c1f95928812e55dec6d3 Mon Sep 17 00:00:00 2001 From: Hongwei Date: Wed, 7 Oct 2026 12:53:27 +0000 Subject: [PATCH 07/29] test: check the doc binding in the order the middleware selects from ResourceDocRouteBindingTest sent each doc's URL through selectByRoute with the docs in registration order, but the v7.0.0 middleware selects from the docs sorted by segment count, the order its routes are tried in. A doc shadowed by another one in that order would have passed the test and been validated and run as the wrong endpoint in production. Use the ordered list. --- .../api/util/http4s/ResourceDocRouteBindingTest.scala | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/obp-api/src/test/scala/code/api/util/http4s/ResourceDocRouteBindingTest.scala b/obp-api/src/test/scala/code/api/util/http4s/ResourceDocRouteBindingTest.scala index cb7c6a3ee0..1d8e3d512d 100644 --- a/obp-api/src/test/scala/code/api/util/http4s/ResourceDocRouteBindingTest.scala +++ b/obp-api/src/test/scala/code/api/util/http4s/ResourceDocRouteBindingTest.scala @@ -50,11 +50,13 @@ class ResourceDocRouteBindingTest extends ServerSetup { object ResourceDocRouteBindingTag extends Tag("ResourceDocRouteBinding") - /** Each converted version's `resourceDocs` buffer. Naming the Implementations object fills it. */ - private def loaded(implementations: Any, docs: ArrayBuffer[ResourceDoc]): ArrayBuffer[ResourceDoc] = docs - + /** + * Each converted version's docs in the order its middleware selects from, which is the order its routes + * are tried in. Selection takes the first doc whose route serves a request, so a test that used the + * registration order could pass while production picks another doc. + */ private def convertedCatalogs: List[(String, ArrayBuffer[ResourceDoc])] = List( - "v7.0.0" -> loaded(code.api.v7_0_0.Http4s700.Implementations7_0_0, code.api.v7_0_0.Http4s700.resourceDocs) + "v7.0.0" -> code.api.v7_0_0.Http4s700.Implementations7_0_0.orderedResourceDocs ) private def describe(doc: ResourceDoc): String = From 464109a61d8c5583215f284d496f15718ad09ebb Mon Sep 17 00:00:00 2001 From: Hongwei Date: Wed, 7 Oct 2026 13:07:54 +0000 Subject: [PATCH 08/29] perf: select the doc by route from an index and pass on requests no route serves selectByRoute asked every route-bound doc on every request. The v7.0.0 middleware sits ahead of the older versions in the chain, so a v4 or v3 request paid for about 120 pattern matches in v7.0.0 first, and the docs that share a route were looked up again with a pass over all docs each time. Build an index of the route-bound docs by verb and API version when the middleware is built, with the docs that share a route worked out once, and ask only the routes of the request's verb and version. The v7.0.0 to v6.0.0 bridge uses the same index. When every doc of a group carries its route, a request no route serves is passed on at once, before the CallContext is built: nothing else in the group could serve it, so resolving the caller and running all the routes again could only find nothing. Also report a route that is null (an endpoint val declared after the resourceDocs += line that uses it) by the endpoint's name when the middleware is built, and say why a doc that shares a route with others falls back to the first of them when no template matches. --- .../code/api/util/http4s/Http4sSupport.scala | 74 +++++-- .../util/http4s/ResourceDocMiddleware.scala | 207 ++++++++++-------- .../scala/code/api/v7_0_0/Http4s700.scala | 5 +- .../ResourceDocRouteSelectionTest.scala | 64 ++++++ 4 files changed, 237 insertions(+), 113 deletions(-) diff --git a/obp-api/src/main/scala/code/api/util/http4s/Http4sSupport.scala b/obp-api/src/main/scala/code/api/util/http4s/Http4sSupport.scala index acd7a5bccf..94c209e8d4 100644 --- a/obp-api/src/main/scala/code/api/util/http4s/Http4sSupport.scala +++ b/obp-api/src/main/scala/code/api/util/http4s/Http4sSupport.scala @@ -871,6 +871,41 @@ object ResourceDocMatcher extends code.util.Helper.MdcLoggable { ): Option[ResourceDoc] = findResourceDoc(verb, path, buildIndex(resourceDocs)) + /** A doc that carries its route, with the docs of the same bucket that share that route. */ + final case class RouteBoundDoc( + doc: ResourceDoc, + route: code.api.util.APIUtil.Http4sRoutePF, + sharingTheRoute: List[ResourceDoc] + ) + + /** + * The docs that carry their route, by (VERB, apiVersion) and in the order given. A request is only + * put to the routes of its own verb and version, and which docs share a route is worked out once + * here instead of on every request. + */ + final case class RouteIndex(byVerbAndVersion: Map[(String, String), List[RouteBoundDoc]]) + + private def routeOf(doc: ResourceDoc): Option[code.api.util.APIUtil.Http4sRoutePF] = + doc.http4sPartialFunction.flatMap(handler => Option(handler)).flatMap(_.route) + + /** The API version a request path is for: the segment after the API prefix, as in [[findResourceDoc]]. */ + private def apiVersionOf(pathString: String): String = + pathString.split("/").filter(_.nonEmpty).drop(1).headOption.getOrElse("") + + /** + * Build the index [[selectByRoute]] uses. Call this once, at middleware construction; docs without a + * route are left out. + */ + def buildRouteIndex(docs: Iterable[ResourceDoc]): RouteIndex = { + val bound = docs.toList.flatMap(doc => routeOf(doc).map(doc -> _)) + val buckets = bound.groupBy { case (doc, _) => (doc.requestVerb.toUpperCase, doc.implementedInApiVersion.toString) } + RouteIndex(buckets.map { case (key, inBucket) => + key -> inBucket.map { case (doc, route) => + RouteBoundDoc(doc, route, inBucket.collect { case (other, otherRoute) if otherRoute eq route => other }) + } + }) + } + /** * Select the ResourceDoc of the route that serves `req`. * @@ -878,34 +913,37 @@ object ResourceDocMatcher extends code.util.Helper.MdcLoggable { * the pattern match of its handler, so the request is put to that match instead of being compared * with the doc's URL template. The doc that comes back is the doc of the route that will run, * whatever the template says, and nothing about a segment has to be guessed to be a placeholder - * or a fixed word. Docs without a route are ignored; they are found by [[findResourceDoc]]. + * or a fixed word. Docs without a route are not in the index; they are found by [[findResourceDoc]]. * - * `docs` are tried in the order given, which must be the order the routes are tried in when a - * request could be served by more than one. Several docs may share one route (a handler that - * serves every transaction-request type has a doc per type); the template then tells those docs - * apart, the one with the most fixed segments winning, and the first of them when none matches. + * Only the routes of the request's verb and API version are asked, in the order the docs were given, + * which must be the order the routes are tried in when a request could be served by more than one. + * Several docs may share one route (a handler that serves every transaction-request type has a doc + * per type); the template then tells those docs apart, the one with the most fixed segments winning. + * When none of their templates matches (an empty segment, a type that has no doc of its own) the + * first of them is used: the request is served by that handler either way, so it is still validated + * under the roles and errors of the endpoint that serves it rather than run with no validation. */ - def selectByRoute(req: Request[IO], docs: Iterable[ResourceDoc]): Option[ResourceDoc] = { - def routeOf(doc: ResourceDoc): Option[code.api.util.APIUtil.Http4sRoutePF] = - doc.http4sPartialFunction.flatMap(_.route) - - docs.iterator - .flatMap(doc => routeOf(doc).map(doc -> _)) - .find { case (_, route) => route.isDefinedAt(req) } - .map { case (first, route) => - val sharingTheRoute = docs.filter(doc => routeOf(doc).exists(_ eq route)).toList - if (sharingTheRoute.size <= 1) first + def selectByRoute(req: Request[IO], index: RouteIndex): Option[ResourceDoc] = { + val key = (req.method.name.toUpperCase, apiVersionOf(req.uri.path.renderString)) + index.byVerbAndVersion.getOrElse(key, Nil) + .find(_.route.isDefinedAt(req)) + .map { first => + if (first.sharingTheRoute.size <= 1) first.doc else { val strippedPath = apiPrefixPattern.replaceFirstIn(req.uri.path.renderString, "") - sharingTheRoute - .filter(doc => doc.requestVerb.equalsIgnoreCase(req.method.name) && matchesUrlTemplate(strippedPath, doc.requestUrl)) + first.sharingTheRoute + .filter(doc => matchesUrlTemplate(strippedPath, doc.requestUrl)) .sortBy(doc => -literalSegmentCount(doc.requestUrl)) .headOption - .getOrElse(first) + .getOrElse(first.doc) } } } + /** Select by route from docs that have not been indexed; builds the index on every call, so use it for tests. */ + def selectByRoute(req: Request[IO], docs: Iterable[ResourceDoc]): Option[ResourceDoc] = + selectByRoute(req, buildRouteIndex(docs)) + /** * Check if a path matches a URL template * Template segments in uppercase are treated as variables diff --git a/obp-api/src/main/scala/code/api/util/http4s/ResourceDocMiddleware.scala b/obp-api/src/main/scala/code/api/util/http4s/ResourceDocMiddleware.scala index 0e1efd6b29..200833e2a9 100644 --- a/obp-api/src/main/scala/code/api/util/http4s/ResourceDocMiddleware.scala +++ b/obp-api/src/main/scala/code/api/util/http4s/ResourceDocMiddleware.scala @@ -152,105 +152,124 @@ object ResourceDocMiddleware extends MdcLoggable { resourceDocs: ArrayBuffer[ResourceDoc], wrap: HttpRoutes[IO] => HttpRoutes[IO] = identity ): HttpRoutes[IO] => HttpRoutes[IO] = { routes => + // A route declared after the `resourceDocs +=` line that refers to it is still null when the doc is + // built (Rule 5 in CLAUDE.md). Say which endpoint it is now, rather than failing with an NPE here or + // on the first request. + resourceDocs.foreach { doc => + if (doc.http4sPartialFunction.exists(_ == null)) + throw new IllegalStateException( + s"The route of ${doc.partialFunctionName} (${doc.requestVerb} ${doc.requestUrl}) is null: declare " + + "each endpoint val before the resourceDocs += line that uses it.") + } // Build the lookup structures once per middleware instance (at startup), not per request. - val routeBoundDocs = resourceDocs.filter(_.http4sPartialFunction.exists(_.route.isDefined)).toList - val resourceDocIndex = ResourceDocMatcher.buildIndex( - resourceDocs.filterNot(doc => doc.http4sPartialFunction.exists(_.route.isDefined))) + val (routeBound, unbound) = resourceDocs.toList.partition(_.http4sPartialFunction.exists(_.route.isDefined)) + val routeIndex = ResourceDocMatcher.buildRouteIndex(routeBound) + val resourceDocIndex = ResourceDocMatcher.buildIndex(unbound) + // When every doc carries its route, a request no route serves is not this group's: nothing else here + // could serve it, so it can go straight to the next link of the chain. + val everyDocIsRouteBound = routeBound.nonEmpty && unbound.isEmpty // A ResourceDoc is a case class holding JSON bodies: look it up by identity, not by equality. val routeOfDoc = new java.util.IdentityHashMap[ResourceDoc, HttpRoutes[IO]]() - routeBoundDocs.foreach(doc => routeOfDoc.put(doc, wrap(doc.http4sPartialFunction.get.routes))) + routeBound.foreach(doc => routeOfDoc.put(doc, wrap(doc.http4sPartialFunction.get.routes))) Kleisli[HttpF, Request[IO], Response[IO]] { req: Request[IO] => - // Read enable/disable Props per request so runtime changes (e.g. `setPropsValues` in - // tests or live config reloads) take effect immediately. Cost is a few Lift Props - // lookups — negligible per request, but lets disabled endpoints be toggled without - // restarting the server. A disabled endpoint yields OptionT.none so the request - // falls through to the next handler in the chain (typically the Lift bridge). - // - // Version-level enable/disable is NOT re-checked here — that's enforced once at - // startup by `Http4sApp.gate` for the URL prefix the request arrives at, so that - // disabling vX.Y.Z retires `/obp/vX.Y.Z/...` but leaves the same endpoints - // reachable via newer enabled prefixes through the cascade. See - // `isEndpointEnabled`'s docstring for the rationale. - val disabledOperationIds = APIUtil.getDisabledEndpointOperationIds().toSet - val enabledOperationIds = APIUtil.getEnabledEndpointOperationIds().toSet - def endpointIsEnabled(rd: ResourceDoc): Boolean = - isEndpointEnabled(rd, disabledOperationIds, enabledOperationIds) - val apiVersionFromPath = req.uri.path.segments.map(_.encoded).toList match { - case apiPathZero :: version :: _ if apiPathZero == APIUtil.getPropsValue("apiPathZero", "obp") => version - case _ => ApiShortVersions.`v7.0.0`.toString - } - // Build initial CallContext from request - OptionT.liftF(Http4sCallContextBuilder.fromRequest(req, apiVersionFromPath)).flatMap { cc => - // Cache the body so bridge-cascade hops (v400→v310→v300→…) don't re-read the now-empty stream. - // First read won the body in fromRequest; we replay it from cc.httpBody onwards. - val reqWithOriginalBody = req.withAttribute(Http4sRequestAttributes.cachedBodyKey, cc.httpBody) - val selected: Option[(ResourceDoc, HttpRoutes[IO])] = - ResourceDocMatcher.selectByRoute(req, routeBoundDocs).map(doc => doc -> routeOfDoc.get(doc)) - .orElse(ResourceDocMatcher.findResourceDoc(req.method.name, req.uri.path, resourceDocIndex).map(_ -> routes)) - selected match { - case Some((resourceDoc, _)) if !endpointIsEnabled(resourceDoc) => - // Disabled by api_disabled_endpoints / api_enabled_endpoints / api_disabled_versions / - // api_enabled_versions. Fall through so the Lift bridge can serve or 404. - OptionT.none[IO, Response[IO]] - case Some((resourceDoc, routesToRun)) => - Http4sRequestAttributes.trafficNote(req).foreach { note => - note.operationId = Some(resourceDoc.operationId) - note.apiVersion = Some(resourceDoc.implementedInApiVersion.apiShortVersion) - } - val (reqWithCachedBody, ccForDoc) = withCurrencyCodesUpperCased(reqWithOriginalBody, cc, resourceDoc) - val ccWithDoc = ResourceDocMatcher.attachToCallContext(ccForDoc, resourceDoc) - val pathParams = ResourceDocMatcher.extractPathParams(req.uri.path, resourceDoc) - // Validate first (read-only, outside any transaction), then run business logic. - // GET/HEAD are safe methods — no writes, no transaction needed; they run on - // auto-commit vendor connections (same as validation). All other methods - // (POST/PUT/DELETE/PATCH) wrap routes.run in withBusinessDBTransaction. - val work: IO[Option[Response[IO]]] = - validateOnly(reqWithCachedBody, resourceDoc, pathParams, ccWithDoc).flatMap { - case Left(errorResponse) => - IO.pure(Option(errorResponse)) - case Right(enrichedReq) => - // The caller is known now: record its Consumer for TrafficSources. - for { - note <- Http4sRequestAttributes.trafficNote(req) - consumer <- enrichedReq.attributes.lookup(Http4sRequestAttributes.callContextKey).flatMap(_.consumer.toOption) - } { - note.consumerId = Some(consumer.consumerId.get) - note.consumerName = Some(consumer.name.get) - } - for { - note <- Http4sRequestAttributes.trafficNote(req) - user <- enrichedReq.attributes.lookup(Http4sRequestAttributes.callContextKey).flatMap(_.user.toOption) - } note.userId = Some(user.userId) - val routeIO = - routesToRun.run(enrichedReq) - .map(ensureJsonContentType) - .getOrElseF(IO.pure(ensureJsonContentType(Response[IO](org.http4s.Status.NotFound)))) - val executed = - if (req.method == Method.GET || req.method == Method.HEAD) routeIO - else RequestScopeConnection.withBusinessDBTransaction(routeIO) - executed.map(Option(_)) - } - val startNanos = System.nanoTime() - OptionT(work.timeoutTo(endpointTimeoutMs.millis, endpointTimeoutResponse(req)).flatTap(recordTelemetry(resourceDoc, startNanos))) - - case None => - // This group has no ResourceDoc for the request. Almost always the request is simply - // not ours: `routes.run` yields None and the request moves to the next link of the - // version fallthrough chain (v7 -> v6 -> v5.1 -> bridges -> ... ). No transaction scope - // is opened. The cached body is carried forward for the later hops. - // - // The one case where the inner routes DO serve such a request is a malformed URL with - // an empty path segment (e.g. `/banks//accounts`): the matcher counts segments and finds - // no doc, but the http4s pattern still matches with an empty id, and its handler needs - // the caller in the CallContext to answer 403/404 rather than a misleading 401. That is - // why the caller is resolved here at all. It is resolved WITHOUT rate limiting and at - // most ONCE per request (see resolveCallerOnce): rate limiting belongs to the hop that - // serves the request, and re-validating the credentials on every hop was pure waste. - OptionT.liftF( - resolveCallerOnce(req, cc).map { resolvedCc => - reqWithOriginalBody.withAttribute(Http4sRequestAttributes.callContextKey, resolvedCc) + val selected: Option[(ResourceDoc, HttpRoutes[IO])] = + ResourceDocMatcher.selectByRoute(req, routeIndex).map(doc => doc -> routeOfDoc.get(doc)) + .orElse(ResourceDocMatcher.findResourceDoc(req.method.name, req.uri.path, resourceDocIndex).map(_ -> routes)) + if (selected.isEmpty && everyDocIsRouteBound) OptionT.none[IO, Response[IO]] + else { + // Read enable/disable Props per request so runtime changes (e.g. `setPropsValues` in + // tests or live config reloads) take effect immediately. Cost is a few Lift Props + // lookups — negligible per request, but lets disabled endpoints be toggled without + // restarting the server. A disabled endpoint yields OptionT.none so the request + // falls through to the next handler in the chain (typically the Lift bridge). + // + // Version-level enable/disable is NOT re-checked here — that's enforced once at + // startup by `Http4sApp.gate` for the URL prefix the request arrives at, so that + // disabling vX.Y.Z retires `/obp/vX.Y.Z/...` but leaves the same endpoints + // reachable via newer enabled prefixes through the cascade. See + // `isEndpointEnabled`'s docstring for the rationale. + val disabledOperationIds = APIUtil.getDisabledEndpointOperationIds().toSet + val enabledOperationIds = APIUtil.getEnabledEndpointOperationIds().toSet + def endpointIsEnabled(rd: ResourceDoc): Boolean = + isEndpointEnabled(rd, disabledOperationIds, enabledOperationIds) + val apiVersionFromPath = req.uri.path.segments.map(_.encoded).toList match { + case apiPathZero :: version :: _ if apiPathZero == APIUtil.getPropsValue("apiPathZero", "obp") => version + case _ => ApiShortVersions.`v7.0.0`.toString + } + // Build initial CallContext from request + OptionT.liftF(Http4sCallContextBuilder.fromRequest(req, apiVersionFromPath)).flatMap { cc => + // Cache the body so bridge-cascade hops (v400→v310→v300→…) don't re-read the now-empty stream. + // First read won the body in fromRequest; we replay it from cc.httpBody onwards. + val reqWithOriginalBody = req.withAttribute(Http4sRequestAttributes.cachedBodyKey, cc.httpBody) + selected match { + case Some((resourceDoc, _)) if !endpointIsEnabled(resourceDoc) => + // Disabled by api_disabled_endpoints / api_enabled_endpoints / api_disabled_versions / + // api_enabled_versions. Fall through so the Lift bridge can serve or 404. + OptionT.none[IO, Response[IO]] + case Some((resourceDoc, routesToRun)) => + Http4sRequestAttributes.trafficNote(req).foreach { note => + note.operationId = Some(resourceDoc.operationId) + note.apiVersion = Some(resourceDoc.implementedInApiVersion.apiShortVersion) } - ).flatMap(routes.run) + val (reqWithCachedBody, ccForDoc) = withCurrencyCodesUpperCased(reqWithOriginalBody, cc, resourceDoc) + val ccWithDoc = ResourceDocMatcher.attachToCallContext(ccForDoc, resourceDoc) + val pathParams = ResourceDocMatcher.extractPathParams(req.uri.path, resourceDoc) + // Validate first (read-only, outside any transaction), then run business logic. + // GET/HEAD are safe methods — no writes, no transaction needed; they run on + // auto-commit vendor connections (same as validation). All other methods + // (POST/PUT/DELETE/PATCH) wrap routes.run in withBusinessDBTransaction. + val work: IO[Option[Response[IO]]] = + validateOnly(reqWithCachedBody, resourceDoc, pathParams, ccWithDoc).flatMap { + case Left(errorResponse) => + IO.pure(Option(errorResponse)) + case Right(enrichedReq) => + // The caller is known now: record its Consumer for TrafficSources. + for { + note <- Http4sRequestAttributes.trafficNote(req) + consumer <- enrichedReq.attributes.lookup(Http4sRequestAttributes.callContextKey).flatMap(_.consumer.toOption) + } { + note.consumerId = Some(consumer.consumerId.get) + note.consumerName = Some(consumer.name.get) + } + for { + note <- Http4sRequestAttributes.trafficNote(req) + user <- enrichedReq.attributes.lookup(Http4sRequestAttributes.callContextKey).flatMap(_.user.toOption) + } note.userId = Some(user.userId) + val routeIO = + routesToRun.run(enrichedReq) + .map(ensureJsonContentType) + .getOrElseF(IO.pure(ensureJsonContentType(Response[IO](org.http4s.Status.NotFound)))) + val executed = + if (req.method == Method.GET || req.method == Method.HEAD) routeIO + else RequestScopeConnection.withBusinessDBTransaction(routeIO) + executed.map(Option(_)) + } + val startNanos = System.nanoTime() + OptionT(work.timeoutTo(endpointTimeoutMs.millis, endpointTimeoutResponse(req)).flatTap(recordTelemetry(resourceDoc, startNanos))) + + case None => + // This group has no ResourceDoc for the request. Almost always the request is simply + // not ours: `routes.run` yields None and the request moves to the next link of the + // version fallthrough chain (v7 -> v6 -> v5.1 -> bridges -> ... ). No transaction scope + // is opened. The cached body is carried forward for the later hops. + // + // The one case where the inner routes DO serve such a request is a malformed URL with + // an empty path segment (e.g. `/banks//accounts`) in a version whose docs are matched + // by template: the matcher counts segments and finds no doc, but the http4s pattern + // still matches with an empty id, and its handler needs the caller in the CallContext + // to answer 403/404 rather than a misleading 401. That is why the caller is resolved + // here at all. It is resolved WITHOUT rate limiting and at most ONCE per request (see + // resolveCallerOnce): rate limiting belongs to the hop that serves the request, and + // re-validating the credentials on every hop was pure waste. + // + // A group whose docs all carry their route never gets here: a request one of its routes + // serves has its doc, empty segment or not, and any other request is passed on at once. + OptionT.liftF( + resolveCallerOnce(req, cc).map { resolvedCc => + reqWithOriginalBody.withAttribute(Http4sRequestAttributes.callContextKey, resolvedCc) + } + ).flatMap(routes.run) + } } } } diff --git a/obp-api/src/main/scala/code/api/v7_0_0/Http4s700.scala b/obp-api/src/main/scala/code/api/v7_0_0/Http4s700.scala index 030528c69a..77355090b5 100644 --- a/obp-api/src/main/scala/code/api/v7_0_0/Http4s700.scala +++ b/obp-api/src/main/scala/code/api/v7_0_0/Http4s700.scala @@ -7512,10 +7512,13 @@ object Http4s700 { // OptionT.none for such a path (e.g. api_disabled_endpoints), the bridge must // not silently re-serve them from v6. "Served by a v7 route" is asked of the // routes themselves, through the same ordered docs the middleware selects from. + private lazy val v7RouteIndex: ResourceDocMatcher.RouteIndex = + ResourceDocMatcher.buildRouteIndex(Implementations7_0_0.orderedResourceDocs) + private val v700ToV600Bridge: HttpRoutes[IO] = Kleisli[HttpF, Request[IO], Response[IO]] { req => val rawPath = req.uri.path.renderString if (rawPath.startsWith("/obp/v7.0.0/") && - ResourceDocMatcher.selectByRoute(req, Implementations7_0_0.orderedResourceDocs).isEmpty) { + ResourceDocMatcher.selectByRoute(req, v7RouteIndex).isEmpty) { val rewritten = rawPath.replaceFirst("/obp/v7\\.0\\.0/", "/obp/v6.0.0/") val newUri = req.uri.withPath(Uri.Path.unsafeFromString(rewritten)) code.api.v6_0_0.Http4s600.wrappedRoutesV600Services.run(req.withUri(newUri)) diff --git a/obp-api/src/test/scala/code/api/util/http4s/ResourceDocRouteSelectionTest.scala b/obp-api/src/test/scala/code/api/util/http4s/ResourceDocRouteSelectionTest.scala index 100eec8599..29536527db 100644 --- a/obp-api/src/test/scala/code/api/util/http4s/ResourceDocRouteSelectionTest.scala +++ b/obp-api/src/test/scala/code/api/util/http4s/ResourceDocRouteSelectionTest.scala @@ -28,6 +28,7 @@ TESOBE (http://www.tesobe.com/) package code.api.util.http4s import cats.effect.IO +import cats.effect.unsafe.implicits.global import code.api.util.APIUtil.{Http4sRoute, ResourceDoc} import code.api.util.ApiTag.ResourceDocTag import com.openbankproject.commons.util.ApiVersion @@ -36,6 +37,8 @@ import org.http4s.dsl.io._ import org.json4s.JsonAST.JObject import org.scalatest.{FeatureSpec, GivenWhenThen, Matchers, Tag} +import scala.collection.mutable.ArrayBuffer + /** * Unit tests for ResourceDocMatcher.selectByRoute. * @@ -141,4 +144,65 @@ class ResourceDocRouteSelectionTest extends FeatureSpec with Matchers with Given ResourceDocMatcher.selectByRoute(request(Method.GET, "/obp/v7.0.0/legacy/l1"), List(legacy)) shouldBe None } } + + feature("ResourceDocMatcher.selectByRoute - only the routes of the request's verb and version are asked") { + + /** A route that counts how often it is asked whether it serves a request. */ + class CountingRoute extends PartialFunction[Request[IO], IO[Response[IO]]] { + var asked = 0 + def isDefinedAt(req: Request[IO]): Boolean = { asked += 1; false } + def apply(req: Request[IO]): IO[Response[IO]] = ok + } + + scenario("A route of another verb or another API version is not asked", ResourceDocRouteSelectionTag) { + val counting = new CountingRoute + val docs = List(doc("postThing", "POST", "/things", Some(Http4sRoute(counting)))) + val index = ResourceDocMatcher.buildRouteIndex(docs) + + ResourceDocMatcher.selectByRoute(request(Method.GET, "/obp/v7.0.0/things"), index) shouldBe None + ResourceDocMatcher.selectByRoute(request(Method.POST, "/obp/v4.0.0/things"), index) shouldBe None + counting.asked shouldBe 0 + + ResourceDocMatcher.selectByRoute(request(Method.POST, "/obp/v7.0.0/things"), index) shouldBe None + counting.asked shouldBe 1 + } + + scenario("The docs that share a route are found when the index is built, in the order given", ResourceDocRouteSelectionTag) { + val shared = Http4sRoute { case POST -> Root / "obp" / "v7.0.0" / "types" / _ / "requests" => ok } + val sepa = doc("createSepa", "POST", "/types/SEPA/requests", Some(shared)) + val generic = doc("createAnyType", "POST", "/types/TRANSACTION_REQUEST_TYPE/requests", Some(shared)) + val index = ResourceDocMatcher.buildRouteIndex(List(sepa, generic)) + + ResourceDocMatcher.selectByRoute(request(Method.POST, "/obp/v7.0.0/types/SEPA/requests"), index) shouldBe Some(sepa) + ResourceDocMatcher.selectByRoute(request(Method.POST, "/obp/v7.0.0/types/CARDANO/requests"), index) shouldBe Some(generic) + } + + scenario("When no template of the docs that share a route matches, the first of them is used", ResourceDocRouteSelectionTag) { + val shared = Http4sRoute { case POST -> Root / "obp" / "v7.0.0" / "types" / _ / "requests" => ok } + val sepa = doc("createSepa", "POST", "/types/SEPA/requests", Some(shared)) + val generic = doc("createAnyType", "POST", "/types/TRANSACTION_REQUEST_TYPE/requests", Some(shared)) + + ResourceDocMatcher.selectByRoute(request(Method.POST, "/obp/v7.0.0/types//requests"), List(sepa, generic)) shouldBe Some(sepa) + } + } + + feature("ResourceDocMiddleware - a group whose docs all carry their route") { + + scenario("A route that is null is reported by name when the middleware is built", ResourceDocRouteSelectionTag) { + val declaredTooLate = doc("getDeclaredTooLate", "GET", "/late", Some(null.asInstanceOf[Http4sRoute])) + + val thrown = the[IllegalStateException] thrownBy ResourceDocMiddleware.apply(ArrayBuffer(declaredTooLate))(HttpRoutes.empty[IO]) + thrown.getMessage should include("getDeclaredTooLate") + } + + scenario("A request no route serves is passed on without running anything of this group", ResourceDocRouteSelectionTag) { + var ranTheRoutes = false + val routes = HttpRoutes.of[IO] { case _ => IO { ranTheRoutes = true }.as(Response[IO]()) } + val thing = doc("getThing", "GET", "/things", Some(Http4sRoute { case GET -> Root / "obp" / "v7.0.0" / "things" => ok })) + val middleware = ResourceDocMiddleware.apply(ArrayBuffer(thing))(routes) + + middleware.run(request(Method.GET, "/obp/v7.0.0/no-such-thing")).value.unsafeRunSync() shouldBe None + ranTheRoutes shouldBe false + } + } } From b112b04c548ac8e9435fefdea25d188313946885 Mon Sep 17 00:00:00 2001 From: Hongwei Date: Wed, 7 Oct 2026 17:29:57 +0200 Subject: [PATCH 09/29] refactor: derive the order docs are selected in from the route chain Add Http4sRoute.chain, which tries handlers in the order given, and ResourceDocMatcher.orderByRoutes, which puts a version's docs in the order its routes are tried. Docs that share a route stay together and an alias of a route counts as the same route. A doc whose route is missing from the chain is an error naming the doc, unless the route is listed as deliberately run outside the middleware; a doc with no route documents an endpoint served elsewhere and is left out. The route index is bucketed by apiShortVersion so Berlin Group and UK Open Banking versions are found as well. --- .../main/scala/code/api/util/APIUtil.scala | 6 ++++ .../code/api/util/http4s/Http4sSupport.scala | 34 ++++++++++++++++++- 2 files changed, 39 insertions(+), 1 deletion(-) 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 46fd323c86..8c540e515c 100644 --- a/obp-api/src/main/scala/code/api/util/APIUtil.scala +++ b/obp-api/src/main/scala/code/api/util/APIUtil.scala @@ -2826,6 +2826,12 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{ object Http4sRoute { def apply(pf: Http4sRoutePF): Http4sRoute = new Http4sRoute(pf) + + /** Try the handlers in the order given; the first that serves a request answers it. */ + def chain(handlers: Seq[Http4sHandler]): HttpRoutes[IO] = + handlers.map(_.routes).foldLeft(HttpRoutes.empty[IO]) { (acc, next) => + HttpRoutes[IO](req => acc.run(req).orElse(next.run(req))) + } } type Http4sEndpoint = Option[Http4sHandler] diff --git a/obp-api/src/main/scala/code/api/util/http4s/Http4sSupport.scala b/obp-api/src/main/scala/code/api/util/http4s/Http4sSupport.scala index 94c209e8d4..d233e7b535 100644 --- a/obp-api/src/main/scala/code/api/util/http4s/Http4sSupport.scala +++ b/obp-api/src/main/scala/code/api/util/http4s/Http4sSupport.scala @@ -888,6 +888,38 @@ object ResourceDocMatcher extends code.util.Helper.MdcLoggable { private def routeOf(doc: ResourceDoc): Option[code.api.util.APIUtil.Http4sRoutePF] = doc.http4sPartialFunction.flatMap(handler => Option(handler)).flatMap(_.route) + /** + * The docs in the order their routes are tried: the order the middleware must select from, so the doc + * that validates a request is the doc of the route that runs it. Docs that share a route stay together, + * in registration order. + * + * A doc whose route is not in `routesInOrder` is an error (a route left out of the chain would never be + * served, or served without its doc's checks), unless it is named in `outsideTheChain`: routes that are + * deliberately run before the middleware, and so have no place in the order it selects from. A doc + * with no route at all documents an endpoint served somewhere else (the message-docs Swagger is served + * by Http4sResourceDocs); it is left out, because there is nothing for the middleware to validate it against. + */ + def orderByRoutes( + docs: Iterable[ResourceDoc], + routesInOrder: Iterable[code.api.util.APIUtil.Http4sHandler], + outsideTheChain: Iterable[code.api.util.APIUtil.Http4sHandler] = Nil + ): ArrayBuffer[ResourceDoc] = { + // Routes are compared by identity: an alias of a route (`val b = a`) is the same route. + def keyOf(h: code.api.util.APIUtil.Http4sHandler): AnyRef = h.routes + val position = new java.util.IdentityHashMap[AnyRef, Integer]() + routesInOrder.zipWithIndex.foreach { case (h, i) => if (!position.containsKey(keyOf(h))) position.put(keyOf(h), i) } + val outside = new java.util.IdentityHashMap[AnyRef, Boolean]() + outsideTheChain.foreach(h => outside.put(keyOf(h), true)) + val withRoute = docs.toSeq.filter(_.http4sPartialFunction.exists(_ != null)) + val kept = withRoute.filterNot(doc => outside.containsKey(keyOf(doc.http4sPartialFunction.get))) + val missing = kept.filterNot(doc => position.containsKey(keyOf(doc.http4sPartialFunction.get))) + if (missing.nonEmpty) + throw new IllegalStateException( + "ResourceDocs whose route is not in the route chain: " + + missing.map(d => s"${d.partialFunctionName} (${d.requestVerb} ${d.requestUrl})").mkString(", ")) + ArrayBuffer(kept.sortBy(doc => position.get(keyOf(doc.http4sPartialFunction.get)).intValue): _*) + } + /** The API version a request path is for: the segment after the API prefix, as in [[findResourceDoc]]. */ private def apiVersionOf(pathString: String): String = pathString.split("/").filter(_.nonEmpty).drop(1).headOption.getOrElse("") @@ -898,7 +930,7 @@ object ResourceDocMatcher extends code.util.Helper.MdcLoggable { */ def buildRouteIndex(docs: Iterable[ResourceDoc]): RouteIndex = { val bound = docs.toList.flatMap(doc => routeOf(doc).map(doc -> _)) - val buckets = bound.groupBy { case (doc, _) => (doc.requestVerb.toUpperCase, doc.implementedInApiVersion.toString) } + val buckets = bound.groupBy { case (doc, _) => (doc.requestVerb.toUpperCase, doc.implementedInApiVersion.apiShortVersion) } RouteIndex(buckets.map { case (key, inBucket) => key -> inBucket.map { case (doc, route) => RouteBoundDoc(doc, route, inBucket.collect { case (other, otherRoute) if otherRoute eq route => other }) From 98bc7318764ce4fcf51ee4292d556e69acc4b68b Mon Sep 17 00:00:00 2001 From: Hongwei Date: Wed, 7 Oct 2026 17:29:57 +0200 Subject: [PATCH 10/29] refactor: bind every v6.0.0 ResourceDoc to its route Declare each endpoint as an Http4sRoute and derive the route chain from one list, so the middleware selects the doc of the route that serves a request and runs only that route. The path-rewriting bridge asks the routes whether the version serves a path instead of matching the URL against doc templates. --- .../scala/code/api/v6_0_0/Http4s600.scala | 1007 +++++++++-------- 1 file changed, 506 insertions(+), 501 deletions(-) diff --git a/obp-api/src/main/scala/code/api/v6_0_0/Http4s600.scala b/obp-api/src/main/scala/code/api/v6_0_0/Http4s600.scala index bc3aee14d3..a52f954e0a 100644 --- a/obp-api/src/main/scala/code/api/v6_0_0/Http4s600.scala +++ b/obp-api/src/main/scala/code/api/v6_0_0/Http4s600.scala @@ -37,7 +37,7 @@ import cats.data.{Kleisli, OptionT} import cats.effect._ import code.api.Constant._ import code.api.ResourceDocs1_4_0.SwaggerDefinitionsJSON._ -import code.api.util.APIUtil.{ +import code.api.util.APIUtil.{Http4sHandler, Http4sRoute, DateWithMsExampleString, DefaultToDateString, EmptyBody, @@ -169,7 +169,7 @@ object Http4s600 { // Mirrors v6 Lift root — both bare prefix and /root return the same // info JSON. Reuses JSONFactory510.getApiInfoJSON because v6's API-info // shape is unchanged from v5.1. - lazy val root: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val root: Http4sRoute = Http4sRoute { case GET -> `prefixPath` => Ok(convertAnyToJsonString( JSONFactory510.getApiInfoJSON(implementedInApiVersion, versionStatus) @@ -184,7 +184,7 @@ object Http4s600 { // Route: GET /obp/v6.0.0/api/versions // Returns the list of scanned API versions with `is_active` reflecting // current `api_disabled_versions`/`api_enabled_versions` props. - lazy val getScannedApiVersions: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getScannedApiVersions: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "api" / "versions" => EndpointHelpers.executeAndRespond(req) { _ => Future { @@ -214,7 +214,7 @@ object Http4s600 { // Auth-only. Returns the logged-in user enriched with entitlements, // virtual roles (super_admin / oidc_operator), permissions, and the // optional on-behalf-of user when the request runs under a consent. - lazy val getCurrentUser: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getCurrentUser: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "users" / "current" => EndpointHelpers.withUser(req) { (user, cc) => for { @@ -259,7 +259,7 @@ object Http4s600 { // Route: GET /obp/v6.0.0/banks - lazy val getBanks: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getBanks: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" => EndpointHelpers.executeAndRespond(req) { implicit cc => for { @@ -270,7 +270,7 @@ object Http4s600 { // Route: GET /obp/v6.0.0/banks/BANK_ID - lazy val getBank: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getBank: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ => EndpointHelpers.withBank(req) { (bank, cc) => for { @@ -281,7 +281,7 @@ object Http4s600 { // Route: GET /obp/v6.0.0/banks/BANK_ID/customers - lazy val getCustomersAtOneBank: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getCustomersAtOneBank: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / bankIdStr / "customers" => EndpointHelpers.withUserAndBank(req) { (_, _, cc) => val bankId = BankId(bankIdStr) @@ -298,7 +298,7 @@ object Http4s600 { // Route: GET /obp/v6.0.0/banks/BANK_ID/customers/CUSTOMER_ID - lazy val getCustomerByCustomerId: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getCustomerByCustomerId: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "customers" / customerId => EndpointHelpers.withUserAndBank(req) { (_, bank, cc) => for { @@ -312,7 +312,7 @@ object Http4s600 { // Route: GET /obp/v6.0.0/my/banks/BANK_ID/accounts/ACCOUNT_ID/account - lazy val getCoreAccountByIdV600: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getCoreAccountByIdV600: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "my" / "banks" / _ / "accounts" / _ / "account" => EndpointHelpers.withBankAccount(req) { (user, account, cc) => for { @@ -329,7 +329,7 @@ object Http4s600 { // Route: GET /obp/v6.0.0/my/dynamic-entities - lazy val getMyDynamicEntities: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getMyDynamicEntities: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "my" / "dynamic-entities" => EndpointHelpers.withUser(req) { (user, _) => for { @@ -343,7 +343,7 @@ object Http4s600 { // Route: GET /obp/v6.0.0/management/system-dynamic-entities - lazy val getSystemDynamicEntities: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getSystemDynamicEntities: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "management" / "system-dynamic-entities" => EndpointHelpers.withUser(req) { (_, cc) => for { @@ -367,7 +367,7 @@ object Http4s600 { // Route: GET /obp/v6.0.0/management/banks/BANK_ID/dynamic-entities - lazy val getBankLevelDynamicEntities: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getBankLevelDynamicEntities: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "management" / "banks" / bankIdStr / "dynamic-entities" => EndpointHelpers.withUserAndBank(req) { (_, _, _) => for { @@ -389,7 +389,7 @@ object Http4s600 { // Route: GET /obp/v6.0.0/management/consumers/CONSUMER_ID - lazy val getConsumer: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getConsumer: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "management" / "consumers" / consumerId => EndpointHelpers.withUser(req) { (_, cc) => for { @@ -407,7 +407,7 @@ object Http4s600 { // Route: GET /obp/v6.0.0/customers - lazy val getCustomersAtAllBanks: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getCustomersAtAllBanks: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "customers" => EndpointHelpers.withUser(req) { (_, cc) => for { @@ -423,7 +423,7 @@ object Http4s600 { // Route: GET /obp/v6.0.0/users/USER_ID/attributes - lazy val getUserAttributes: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getUserAttributes: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "users" / userIdStr / "attributes" => EndpointHelpers.withUser(req) { (_, cc) => for { @@ -435,7 +435,7 @@ object Http4s600 { // Route: GET /obp/v6.0.0/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/account - lazy val getPrivateAccountByIdFull: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getPrivateAccountByIdFull: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "accounts" / _ / viewIdStr / "account" => EndpointHelpers.withView(req) { (user, account, view, cc) => for { @@ -457,7 +457,7 @@ object Http4s600 { // POST that GETs (returns 200) — used to fetch a customer by their customer_number. // Body is parsed manually so we preserve v6 Lift's "The Json body should be the …" // wording verbatim, which the test suites assert on. - lazy val getCustomerByCustomerNumber: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getCustomerByCustomerNumber: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "banks" / _ / "customers" / "customer-number" => EndpointHelpers.withUserAndBank(req) { (_, bank, cc) => val rawBody = cc.httpBody.getOrElse("") @@ -478,7 +478,7 @@ object Http4s600 { // Route: POST /obp/v6.0.0/banks/BANK_ID/customers/legal-name // POST that GETs (returns 200) — fetch customers by legal name. - lazy val getCustomersByLegalName: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getCustomersByLegalName: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "banks" / _ / "customers" / "legal-name" => EndpointHelpers.withUserAndBank(req) { (_, bank, cc) => val rawBody = cc.httpBody.getOrElse("") @@ -570,7 +570,7 @@ object Http4s600 { } // Route: POST /obp/v6.0.0/management/system-dynamic-entities (201) - lazy val createSystemDynamicEntity: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val createSystemDynamicEntity: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "management" / "system-dynamic-entities" => EndpointHelpers.executeFutureCreated(req) { implicit val cc: CallContext = req.callContext @@ -591,7 +591,7 @@ object Http4s600 { // Route: POST /obp/v6.0.0/management/banks/BANK_ID/dynamic-entities (201) - lazy val createBankLevelDynamicEntity: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val createBankLevelDynamicEntity: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "management" / "banks" / bankIdStr / "dynamic-entities" => EndpointHelpers.executeFutureCreated(req) { implicit val cc: CallContext = req.callContext @@ -611,7 +611,7 @@ object Http4s600 { // Route: PUT /obp/v6.0.0/management/system-dynamic-entities/DYNAMIC_ENTITY_ID (200) - lazy val updateSystemDynamicEntity: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val updateSystemDynamicEntity: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "management" / "system-dynamic-entities" / dynamicEntityId => EndpointHelpers.executeAndRespond(req) { implicit cc => val rawBody = cc.httpBody.getOrElse("") @@ -630,7 +630,7 @@ object Http4s600 { // Route: PUT /obp/v6.0.0/management/banks/BANK_ID/dynamic-entities/DYNAMIC_ENTITY_ID (200) - lazy val updateBankLevelDynamicEntity: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val updateBankLevelDynamicEntity: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "management" / "banks" / bankIdStr / "dynamic-entities" / dynamicEntityId => EndpointHelpers.executeAndRespond(req) { implicit cc => val rawBody = cc.httpBody.getOrElse("") @@ -648,7 +648,7 @@ object Http4s600 { // Route: PUT /obp/v6.0.0/my/dynamic-entities/DYNAMIC_ENTITY_ID (200) - lazy val updateMyDynamicEntity: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val updateMyDynamicEntity: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "my" / "dynamic-entities" / dynamicEntityId => EndpointHelpers.executeAndRespond(req) { implicit cc => val rawBody = cc.httpBody.getOrElse("") @@ -674,7 +674,7 @@ object Http4s600 { // Route: PUT /obp/v6.0.0/system-views/UPD_VIEW_ID (200) // Uses UPD_VIEW_ID (non-standard ALL_CAPS) so middleware skips view validation; // system views aren't in the regular view tables that VIEW_ID resolution checks. - lazy val updateSystemView: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val updateSystemView: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "system-views" / viewIdStr if viewIdStr.nonEmpty => EndpointHelpers.executeAndRespond(req) { implicit cc => val rawBody = cc.httpBody.getOrElse("") @@ -698,7 +698,7 @@ object Http4s600 { // Route: GET /obp/v6.0.0/management/metrics - lazy val getMetrics: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getMetrics: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "management" / "metrics" => EndpointHelpers.withUser(req) { (_, cc) => for { @@ -710,7 +710,7 @@ object Http4s600 { // Route: GET /obp/v6.0.0/management/aggregate-metrics - lazy val getAggregateMetrics: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getAggregateMetrics: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "management" / "aggregate-metrics" => EndpointHelpers.executeAndRespond(req) { cc => for { @@ -738,7 +738,7 @@ object Http4s600 { // Route: GET /obp/v6.0.0/management/metrics/top-apis - lazy val getTopAPIs: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getTopAPIs: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "management" / "metrics" / "top-apis" => EndpointHelpers.withUser(req) { (_, cc) => for { @@ -769,7 +769,7 @@ object Http4s600 { // Route: GET /obp/v6.0.0/webui-props - lazy val getWebUiProps: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getWebUiProps: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "webui-props" => EndpointHelpers.executeAndRespond(req) { implicit cc => val what = req.uri.query.params.getOrElse("what", "active") @@ -800,7 +800,7 @@ object Http4s600 { // Route: GET /obp/v6.0.0/banks/BANK_ID/accounts - lazy val getAccountsAtBank: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getAccountsAtBank: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "accounts" => EndpointHelpers.withUserAndBank(req) { (user, bank, cc) => val filteredParams: Map[String, List[String]] = req.uri.query.multiParams @@ -834,7 +834,7 @@ object Http4s600 { // Route: GET /obp/v6.0.0/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/transactions - lazy val getTransactionsForBankAccount: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getTransactionsForBankAccount: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "accounts" / _ / _ / "transactions" => EndpointHelpers.withView(req) { (user, bankAccount, view, cc) => for { @@ -855,7 +855,7 @@ object Http4s600 { // Route: GET /obp/v6.0.0/banks/BANK_ID/products // Simplified port — skips the Redis cache layer (perf optimization only). - lazy val getProductsV600: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getProductsV600: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "products" => EndpointHelpers.withBank(req) { (bank, cc) => val params = req.uri.query.multiParams.toList.map { case (k, vs) => GetProductsParam(k, vs.toList) } @@ -867,7 +867,7 @@ object Http4s600 { // Route: GET /obp/v6.0.0/users - lazy val getUsers: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getUsers: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "users" => EndpointHelpers.withUser(req) { (_, cc) => for { @@ -890,7 +890,7 @@ object Http4s600 { // Route: POST /obp/v6.0.0/banks (201) - lazy val createBank: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val createBank: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "banks" => EndpointHelpers.executeFutureCreated(req) { implicit val cc: CallContext = req.callContext @@ -945,7 +945,7 @@ object Http4s600 { // Route: POST /obp/v6.0.0/banks/BANK_ID/customers (201) - lazy val createCustomer: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val createCustomer: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "banks" / bankIdStr / "customers" => EndpointHelpers.executeFutureCreated(req) { implicit val cc: CallContext = req.callContext @@ -1079,7 +1079,7 @@ object Http4s600 { } // Route: POST /obp/v6.0.0/users (201) - lazy val createUser: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val createUser: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "users" => EndpointHelpers.executeFutureCreated(req) { implicit val cc: CallContext = req.callContext @@ -1127,7 +1127,7 @@ object Http4s600 { } // Route: POST /obp/v6.0.0/management/user/reset-password-url (201) - lazy val resetPasswordUrl: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val resetPasswordUrl: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "management" / "user" / "reset-password-url" => EndpointHelpers.executeFutureCreated(req) { implicit val cc: CallContext = req.callContext @@ -1197,7 +1197,7 @@ object Http4s600 { // ─── Phase 2: system bucket (8 GETs) — wholly new in v6, no override risk ──── // Route: GET /obp/v6.0.0/system/connectors - lazy val getConnectors: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getConnectors: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "system" / "connectors" => EndpointHelpers.executeAndRespond(req) { _ => Future.successful { @@ -1214,7 +1214,7 @@ object Http4s600 { // Route: GET /obp/v6.0.0/system/cache/config - lazy val getCacheConfig: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getCacheConfig: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "system" / "cache" / "config" => EndpointHelpers.withUser(req) { (_, _) => Future.successful(JSONFactory600.createCacheConfigJsonV600()) @@ -1223,7 +1223,7 @@ object Http4s600 { // Route: GET /obp/v6.0.0/system/cache/info - lazy val getCacheInfo: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getCacheInfo: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "system" / "cache" / "info" => EndpointHelpers.withUser(req) { (_, _) => Future.successful(JSONFactory600.createCacheInfoJsonV600()) @@ -1232,7 +1232,7 @@ object Http4s600 { // Route: GET /obp/v6.0.0/system/cache/namespaces - lazy val getCacheNamespaces: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getCacheNamespaces: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "system" / "cache" / "namespaces" => EndpointHelpers.withUser(req) { (_, _) => Future { @@ -1262,7 +1262,7 @@ object Http4s600 { // Route: GET /obp/v6.0.0/system/database/pool - lazy val getDatabasePoolInfo: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getDatabasePoolInfo: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "system" / "database" / "pool" => EndpointHelpers.withUser(req) { (_, _) => Future.successful(JSONFactory600.createDatabasePoolInfoJsonV600()) @@ -1271,7 +1271,7 @@ object Http4s600 { // Route: GET /obp/v6.0.0/system/migrations - lazy val getMigrations: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getMigrations: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "system" / "migrations" => EndpointHelpers.withUser(req) { (_, _) => Future { @@ -1283,7 +1283,7 @@ object Http4s600 { // Route: GET /obp/v6.0.0/system/connectors/stored_procedure_vDec2019/health - lazy val getStoredProcedureConnectorHealth: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getStoredProcedureConnectorHealth: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "system" / "connectors" / "stored_procedure_vDec2019" / "health" => EndpointHelpers.withUser(req) { (_, _) => Future { @@ -1302,7 +1302,7 @@ object Http4s600 { // Route: GET /obp/v6.0.0/system/connector-method-names // Simplified port — skips the Redis cache wrapper (perf only). - lazy val getConnectorMethodNames: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getConnectorMethodNames: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "system" / "connector-method-names" => EndpointHelpers.withUser(req) { (_, _) => Future { @@ -1318,7 +1318,7 @@ object Http4s600 { // `my_resources` (the granting User's own resources the consent user may act on). Owned, not granted, // so no Role is checked for the block: Consent.validateMyResources checks kind, shape and existence. // Lives in v6 because v5.1.0 is next in line to be frozen. ideas/CONSENT_MY_RESOURCES.md - lazy val createConsent: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val createConsent: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "my" / "consents" / scaMethod if scaMethod == "EMAIL" || scaMethod == "SMS" || scaMethod == "IMPLICIT" => EndpointHelpers.executeFutureCreated(req) { @@ -1510,262 +1510,267 @@ object Http4s600 { http4sPartialFunction = Some(createConsent) ) - val allRoutes: HttpRoutes[IO] = - Kleisli[HttpF, Request[IO], Response[IO]] { req: Request[IO] => - root(req) - .orElse(getScannedApiVersions(req)) - .orElse(getCurrentUser(req)) - .orElse(createConsent(req)) - .orElse(getBanks(req)) - .orElse(getBank(req)) - .orElse(getCustomersAtOneBank(req)) - .orElse(getCustomerByCustomerId(req)) - .orElse(getCoreAccountByIdV600(req)) - .orElse(getMyDynamicEntities(req)) - .orElse(getSystemDynamicEntities(req)) - .orElse(getBankLevelDynamicEntities(req)) - .orElse(getConsumer(req)) - .orElse(getCustomersAtAllBanks(req)) - .orElse(getUserAttributes(req)) - .orElse(getPrivateAccountByIdFull(req)) - .orElse(getCustomerByCustomerNumber(req)) - .orElse(getCustomersByLegalName(req)) - .orElse(createSystemDynamicEntity(req)) - .orElse(createBankLevelDynamicEntity(req)) - .orElse(updateSystemDynamicEntity(req)) - .orElse(updateBankLevelDynamicEntity(req)) - .orElse(updateMyDynamicEntity(req)) - .orElse(updateSystemView(req)) - .orElse(getMetrics(req)) - .orElse(getAggregateMetrics(req)) - .orElse(getTopAPIs(req)) - .orElse(getWebUiProps(req)) - .orElse(getAccountsAtBank(req)) - .orElse(getTransactionsForBankAccount(req)) - .orElse(getProductsV600(req)) - .orElse(getUsers(req)) - .orElse(createBank(req)) - .orElse(createCustomer(req)) - .orElse(createUser(req)) - .orElse(resetPasswordUrl(req)) - .orElse(getConnectors(req)) - .orElse(getCacheConfig(req)) - .orElse(getCacheInfo(req)) - .orElse(getCacheNamespaces(req)) - .orElse(getDatabasePoolInfo(req)) - .orElse(getMigrations(req)) - .orElse(getStoredProcedureConnectorHealth(req)) - .orElse(getConnectorMethodNames(req)) - .orElse(createMandate(req)) - .orElse(getMandates(req)) - .orElse(getMandate(req)) - .orElse(updateMandate(req)) - .orElse(deleteMandate(req)) - .orElse(createMandateProvision(req)) - .orElse(getMandateProvisions(req)) - .orElse(getMandateProvision(req)) - .orElse(updateMandateProvision(req)) - .orElse(deleteMandateProvision(req)) - .orElse(createApiProduct(req)) - .orElse(createOrUpdateApiProduct(req)) - .orElse(getApiProduct(req)) - .orElse(getApiProducts(req)) - .orElse(deleteApiProduct(req)) - .orElse(createApiProductAttribute(req)) - .orElse(updateApiProductAttribute(req)) - .orElse(getApiProductAttribute(req)) - .orElse(deleteApiProductAttribute(req)) - .orElse(createFeaturedApiCollection(req)) - .orElse(getFeaturedApiCollectionsAdmin(req)) - .orElse(updateFeaturedApiCollection(req)) - .orElse(deleteFeaturedApiCollection(req)) - .orElse(createPersonalDataField(req)) - .orElse(getPersonalDataFields(req)) - .orElse(getPersonalDataFieldById(req)) - .orElse(updatePersonalDataField(req)) - .orElse(deletePersonalDataField(req)) - .orElse(getConsumerCallCounters(req)) - .orElse(createCallLimits(req)) - .orElse(updateRateLimits(req)) - .orElse(deleteCallLimits(req)) - .orElse(getActiveRateLimitsNow(req)) - .orElse(getActiveRateLimitsAtDate(req)) - .orElse(createGroup(req)) - .orElse(getGroup(req)) - .orElse(getGroups(req)) - .orElse(updateGroup(req)) - .orElse(deleteGroup(req)) - .orElse(getGroupEntitlements(req)) - .orElse(createAbacRule(req)) - .orElse(getAbacRule(req)) - .orElse(getAbacRules(req)) - .orElse(getAbacRulesByPolicy(req)) - .orElse(updateAbacRule(req)) - .orElse(deleteAbacRule(req)) - .orElse(getFeatures(req)) - .orElse(getProviders(req)) - .orElse(getCurrentConsumer(req)) - .orElse(getPopularApis(req)) - .orElse(getAccountDirectory(req)) - .orElse(getConfigProps(req)) - .orElse(getAppDirectory(req)) - .orElse(getCustomViews(req)) - .orElse(getRolesWithEntitlementCountsAtAllBanks(req)) - .orElse(getCustomViewById(req)) - .orElse(invalidateCacheNamespace(req)) - .orElse(createCustomerLink(req)) - .orElse(getCustomerLinksByBankId(req)) - .orElse(getCustomerLinkById(req)) - .orElse(updateCustomerLink(req)) - .orElse(deleteCustomerLink(req)) - .orElse(getCorporateCustomersAtOneBank(req)) - .orElse(getCorporateCustomerByCustomerId(req)) - .orElse(getCorporateCustomerSubsidiaries(req)) - .orElse(getRetailCustomersAtOneBank(req)) - .orElse(getRetailCustomerByCustomerId(req)) - .orElse(getCustomerChildren(req)) - .orElse(getCustomerLinksByCustomerId(req)) - .orElse(getCustomerInvestigationReport(req)) - .orElse(getSystemViews(req)) - .orElse(getSystemViewById(req)) - .orElse(getAbacPolicies(req)) - .orElse(getConnectorCallCounts(req)) - .orElse(getConnectorTraces(req)) - .orElse(getDynamicEntityDiagnostics(req)) - .orElse(cleanupOrphanedDynamicEntityRecords(req)) - .orElse(createWebUiProps(req)) - .orElse(createOrUpdateWebUiProps(req)) - .orElse(deleteWebUiProps(req)) - .orElse(createCustomViewManagement(req)) - .orElse(getProductTagsV600(req)) - .orElse(updateProductTagsV600(req)) - .orElse(getOidcClient(req)) - .orElse(verifyOidcClient(req)) - .orElse(getUserAttributeById(req)) - .orElse(createUserAttribute(req)) - .orElse(updateUserAttribute(req)) - .orElse(deleteUserAttribute(req)) - .orElse(addUserToGroup(req)) - .orElse(removeUserFromGroup(req)) - .orElse(deleteEntitlement(req)) - .orElse(getAvailablePersonalDynamicEntities(req)) - .orElse(getReferenceTypes(req)) - .orElse(joinSystemChatRoom(req)) - .orElse(createCounterpartyAttribute(req)) - .orElse(deleteCounterpartyAttribute(req)) - .orElse(getCounterpartyAttributeById(req)) - .orElse(getAllCounterpartyAttributes(req)) - .orElse(updateCounterpartyAttribute(req)) - .orElse(hasAccountAccess(req)) - .orElse(getMyAccountAccessRequests(req)) - .orElse(getWebUiProp(req)) - .orElse(getMessageDocsJsonSchema(req)) - .orElse(verifyUserCredentials(req)) - .orElse(getViewPermissions(req)) - .orElse(getAllApiProductsV600(req)) - .orElse(getAllProductsV600(req)) - .orElse(getAccountAccessRequestsForAccount(req)) - .orElse(getAccountAccessRequestById(req)) - .orElse(getHoldingAccountByReleaser(req)) - .orElse(createAccountAccessRequest(req)) - .orElse(approveAccountAccessRequest(req)) - .orElse(rejectAccountAccessRequest(req)) - .orElse(getSignalChannels(req)) - .orElse(getSignalChannelInfo(req)) - .orElse(getSignalStats(req)) - .orElse(publishSignalMessage(req)) - .orElse(getSignalMessages(req)) - .orElse(deleteSignalChannel(req)) - .orElse(getBankChatRooms(req)) - .orElse(getSystemChatRooms(req)) - .orElse(getBankChatRoom(req)) - .orElse(getSystemChatRoom(req)) - .orElse(getMyChatRooms(req)) - .orElse(getMyUnreadCounts(req)) - .orElse(markChatRoomRead(req)) - .orElse(getMyMentions(req)) - .orElse(searchChatRooms(req)) - .orElse(getBulkReactions(req)) - .orElse(archiveBankChatRoom(req)) - .orElse(archiveSystemChatRoom(req)) - .orElse(joinBankChatRoom(req)) - .orElse(refreshBankJoiningKey(req)) - .orElse(refreshSystemJoiningKey(req)) - .orElse(createBankChatRoom(req)) - .orElse(createSystemChatRoom(req)) - .orElse(updateBankChatRoom(req)) - .orElse(updateSystemChatRoom(req)) - .orElse(deleteBankChatRoom(req)) - .orElse(deleteSystemChatRoom(req)) - .orElse(setBankChatRoomOpenRoom(req)) - .orElse(setSystemChatRoomOpenRoom(req)) - .orElse(addBankChatRoomParticipant(req)) - .orElse(addSystemChatRoomParticipant(req)) - .orElse(getBankChatRoomParticipants(req)) - .orElse(getSystemChatRoomParticipants(req)) - .orElse(updateBankParticipantPermissions(req)) - .orElse(updateSystemParticipantPermissions(req)) - .orElse(removeBankChatRoomParticipant(req)) - .orElse(removeSystemChatRoomParticipant(req)) - .orElse(sendBankChatMessage(req)) - .orElse(sendSystemChatMessage(req)) - .orElse(getBankChatMessages(req)) - .orElse(getSystemChatMessages(req)) - .orElse(getBankChatMessage(req)) - .orElse(getSystemChatMessage(req)) - .orElse(editBankChatMessage(req)) - .orElse(editSystemChatMessage(req)) - .orElse(deleteBankChatMessage(req)) - .orElse(deleteSystemChatMessage(req)) - .orElse(getBankThreadReplies(req)) - .orElse(getSystemThreadReplies(req)) - .orElse(replyInBankThread(req)) - .orElse(replyInSystemThread(req)) - .orElse(addBankReaction(req)) - .orElse(addSystemReaction(req)) - .orElse(removeBankReaction(req)) - .orElse(removeSystemReaction(req)) - .orElse(getBankReactions(req)) - .orElse(getSystemReactions(req)) - .orElse(signalBankTyping(req)) - .orElse(signalSystemTyping(req)) - .orElse(getBankTypingUsers(req)) - .orElse(getSystemTypingUsers(req)) - .orElse(createSignatoryPanel(req)) - .orElse(getSignatoryPanels(req)) - .orElse(getSignatoryPanel(req)) - .orElse(updateSignatoryPanel(req)) - .orElse(deleteSignatoryPanel(req)) - .orElse(validateUserEmail(req)) - .orElse(resetPasswordComplete(req)) - .orElse(resetPasswordUrlAnonymous(req)) - .orElse(validateDynamicResourceDoc(req)) - .orElse(createTransactionRequestHold(req)) - .orElse(createTransactionRequestCardano(req)) - .orElse(createTransactionRequestEthereumeSendTransaction(req)) - .orElse(createTransactionRequestEthSendRawTransaction(req)) - .orElse(getUserGroupMemberships(req)) - .orElse(getUsersWithAccountAccess(req)) - .orElse(createRetailCustomer(req)) - .orElse(createCorporateCustomer(req)) - .orElse(getUserByUserId(req)) - .orElse(directLoginEndpoint(req)) - .orElse(validateAbacRule(req)) - .orElse(executeAbacRule(req)) - .orElse(executeAbacPolicy(req)) - .orElse(getAbacRuleSchema(req)) - .orElse(backupSystemDynamicEntity(req)) - .orElse(backupBankLevelDynamicEntity(req)) - .orElse(deleteSystemDynamicEntityCascade(req)) - // createCorporateCustomer + createRetailCustomer deferred — share - // the 60-line date-parsing/customer-number generation logic of - // createCustomer (already migrated); will batch as a focused pass. - } + // The routes in the order they are tried. ResourceDocMiddleware selects the doc of the first + // route that serves a request, so it is given the docs in this same order. + lazy val routesInOrder: List[Http4sHandler] = List( + root, + getScannedApiVersions, + getCurrentUser, + createConsent, + getBanks, + getBank, + getCustomersAtOneBank, + getCustomerByCustomerId, + getCoreAccountByIdV600, + getMyDynamicEntities, + getSystemDynamicEntities, + getBankLevelDynamicEntities, + getConsumer, + getCustomersAtAllBanks, + getUserAttributes, + getPrivateAccountByIdFull, + getCustomerByCustomerNumber, + getCustomersByLegalName, + createSystemDynamicEntity, + createBankLevelDynamicEntity, + updateSystemDynamicEntity, + updateBankLevelDynamicEntity, + updateMyDynamicEntity, + updateSystemView, + getMetrics, + getAggregateMetrics, + getTopAPIs, + getWebUiProps, + getAccountsAtBank, + getTransactionsForBankAccount, + getProductsV600, + getUsers, + createBank, + createCustomer, + createUser, + resetPasswordUrl, + getConnectors, + getCacheConfig, + getCacheInfo, + getCacheNamespaces, + getDatabasePoolInfo, + getMigrations, + getStoredProcedureConnectorHealth, + getConnectorMethodNames, + createMandate, + getMandates, + getMandate, + updateMandate, + deleteMandate, + createMandateProvision, + getMandateProvisions, + getMandateProvision, + updateMandateProvision, + deleteMandateProvision, + createApiProduct, + createOrUpdateApiProduct, + getApiProduct, + getApiProducts, + deleteApiProduct, + createApiProductAttribute, + updateApiProductAttribute, + getApiProductAttribute, + deleteApiProductAttribute, + createFeaturedApiCollection, + getFeaturedApiCollectionsAdmin, + updateFeaturedApiCollection, + deleteFeaturedApiCollection, + createPersonalDataField, + getPersonalDataFields, + getPersonalDataFieldById, + updatePersonalDataField, + deletePersonalDataField, + getConsumerCallCounters, + createCallLimits, + updateRateLimits, + deleteCallLimits, + getActiveRateLimitsNow, + getActiveRateLimitsAtDate, + createGroup, + getGroup, + getGroups, + updateGroup, + deleteGroup, + getGroupEntitlements, + createAbacRule, + getAbacRule, + getAbacRules, + getAbacRulesByPolicy, + updateAbacRule, + deleteAbacRule, + getFeatures, + getProviders, + getCurrentConsumer, + getPopularApis, + getAccountDirectory, + getConfigProps, + getAppDirectory, + getCustomViews, + getRolesWithEntitlementCountsAtAllBanks, + getCustomViewById, + invalidateCacheNamespace, + createCustomerLink, + getCustomerLinksByBankId, + getCustomerLinkById, + updateCustomerLink, + deleteCustomerLink, + getCorporateCustomersAtOneBank, + getCorporateCustomerByCustomerId, + getCorporateCustomerSubsidiaries, + getRetailCustomersAtOneBank, + getRetailCustomerByCustomerId, + getCustomerChildren, + getCustomerLinksByCustomerId, + getCustomerInvestigationReport, + getSystemViews, + getSystemViewById, + getAbacPolicies, + getConnectorCallCounts, + getConnectorTraces, + getDynamicEntityDiagnostics, + cleanupOrphanedDynamicEntityRecords, + createWebUiProps, + createOrUpdateWebUiProps, + deleteWebUiProps, + createCustomViewManagement, + getProductTagsV600, + updateProductTagsV600, + getOidcClient, + verifyOidcClient, + getUserAttributeById, + createUserAttribute, + updateUserAttribute, + deleteUserAttribute, + addUserToGroup, + removeUserFromGroup, + deleteEntitlement, + getAvailablePersonalDynamicEntities, + getReferenceTypes, + joinSystemChatRoom, + createCounterpartyAttribute, + deleteCounterpartyAttribute, + getCounterpartyAttributeById, + getAllCounterpartyAttributes, + updateCounterpartyAttribute, + hasAccountAccess, + getMyAccountAccessRequests, + getWebUiProp, + getMessageDocsJsonSchema, + verifyUserCredentials, + getViewPermissions, + getAllApiProductsV600, + getAllProductsV600, + getAccountAccessRequestsForAccount, + getAccountAccessRequestById, + getHoldingAccountByReleaser, + createAccountAccessRequest, + approveAccountAccessRequest, + rejectAccountAccessRequest, + getSignalChannels, + getSignalChannelInfo, + getSignalStats, + publishSignalMessage, + getSignalMessages, + deleteSignalChannel, + getBankChatRooms, + getSystemChatRooms, + getBankChatRoom, + getSystemChatRoom, + getMyChatRooms, + getMyUnreadCounts, + markChatRoomRead, + getMyMentions, + searchChatRooms, + getBulkReactions, + archiveBankChatRoom, + archiveSystemChatRoom, + joinBankChatRoom, + refreshBankJoiningKey, + refreshSystemJoiningKey, + createBankChatRoom, + createSystemChatRoom, + updateBankChatRoom, + updateSystemChatRoom, + deleteBankChatRoom, + deleteSystemChatRoom, + setBankChatRoomOpenRoom, + setSystemChatRoomOpenRoom, + addBankChatRoomParticipant, + addSystemChatRoomParticipant, + getBankChatRoomParticipants, + getSystemChatRoomParticipants, + updateBankParticipantPermissions, + updateSystemParticipantPermissions, + removeBankChatRoomParticipant, + removeSystemChatRoomParticipant, + sendBankChatMessage, + sendSystemChatMessage, + getBankChatMessages, + getSystemChatMessages, + getBankChatMessage, + getSystemChatMessage, + editBankChatMessage, + editSystemChatMessage, + deleteBankChatMessage, + deleteSystemChatMessage, + getBankThreadReplies, + getSystemThreadReplies, + replyInBankThread, + replyInSystemThread, + addBankReaction, + addSystemReaction, + removeBankReaction, + removeSystemReaction, + getBankReactions, + getSystemReactions, + signalBankTyping, + signalSystemTyping, + getBankTypingUsers, + getSystemTypingUsers, + createSignatoryPanel, + getSignatoryPanels, + getSignatoryPanel, + updateSignatoryPanel, + deleteSignatoryPanel, + validateUserEmail, + resetPasswordComplete, + resetPasswordUrlAnonymous, + validateDynamicResourceDoc, + createTransactionRequestHold, + createTransactionRequestCardano, + createTransactionRequestEthereumeSendTransaction, + createTransactionRequestEthSendRawTransaction, + getUserGroupMemberships, + getUsersWithAccountAccess, + createRetailCustomer, + createCorporateCustomer, + getUserByUserId, + directLoginEndpoint, + validateAbacRule, + executeAbacRule, + executeAbacPolicy, + getAbacRuleSchema, + backupSystemDynamicEntity, + backupBankLevelDynamicEntity, + deleteSystemDynamicEntityCascade + // createCorporateCustomer + createRetailCustomer deferred — share + // the 60-line date-parsing/customer-number generation logic of + // createCustomer (already migrated); will batch as a focused pass. + ) + + lazy val orderedResourceDocs: ArrayBuffer[ResourceDoc] = ResourceDocMatcher.orderByRoutes(resourceDocs, routesInOrder) + + lazy val allRoutes: HttpRoutes[IO] = Http4sRoute.chain(routesInOrder) // ─── Phase 2: corporate-customers + retail-customers + banks/customers/* (8) ─── // Route: GET /obp/v6.0.0/banks/BANK_ID/corporate-customers - lazy val getCorporateCustomersAtOneBank: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getCorporateCustomersAtOneBank: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "corporate-customers" => EndpointHelpers.withUserAndBank(req) { (_, bank, cc) => for { @@ -1779,7 +1784,7 @@ object Http4s600 { // Route: GET /obp/v6.0.0/banks/BANK_ID/corporate-customers/CUSTOMER_ID - lazy val getCorporateCustomerByCustomerId: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getCorporateCustomerByCustomerId: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "corporate-customers" / customerId => EndpointHelpers.withUserAndBank(req) { (_, bank, cc) => for { @@ -1794,7 +1799,7 @@ object Http4s600 { // Route: GET /obp/v6.0.0/banks/BANK_ID/corporate-customers/CUSTOMER_ID/subsidiaries - lazy val getCorporateCustomerSubsidiaries: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getCorporateCustomerSubsidiaries: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "corporate-customers" / customerId / "subsidiaries" => EndpointHelpers.withUserAndBank(req) { (_, bank, cc) => for { @@ -1809,7 +1814,7 @@ object Http4s600 { // Route: GET /obp/v6.0.0/banks/BANK_ID/retail-customers - lazy val getRetailCustomersAtOneBank: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getRetailCustomersAtOneBank: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "retail-customers" => EndpointHelpers.withUserAndBank(req) { (_, bank, cc) => for { @@ -1823,7 +1828,7 @@ object Http4s600 { // Route: GET /obp/v6.0.0/banks/BANK_ID/retail-customers/CUSTOMER_ID - lazy val getRetailCustomerByCustomerId: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getRetailCustomerByCustomerId: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "retail-customers" / customerId => EndpointHelpers.withUserAndBank(req) { (_, bank, cc) => for { @@ -1838,7 +1843,7 @@ object Http4s600 { // Route: GET /obp/v6.0.0/banks/BANK_ID/customers/CUSTOMER_ID/children - lazy val getCustomerChildren: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getCustomerChildren: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "customers" / customerId / "children" => EndpointHelpers.withUserAndBank(req) { (_, bank, cc) => for { @@ -1850,7 +1855,7 @@ object Http4s600 { // Route: GET /obp/v6.0.0/banks/BANK_ID/customers/CUSTOMER_ID/customer-links - lazy val getCustomerLinksByCustomerId: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getCustomerLinksByCustomerId: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "customers" / customerId / "customer-links" => EndpointHelpers.withUserAndBank(req) { (_, _, cc) => for { @@ -1867,7 +1872,7 @@ object Http4s600 { // deleteSystemDynamicEntityCascade (private deleteDynamicEntityCascadeMethod). // GET /obp/v6.0.0/management/system-views - lazy val getSystemViews: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getSystemViews: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "management" / "system-views" => EndpointHelpers.withUser(req) { (_, _) => Views.views.vend.getSystemViews().map(JSONFactory600.createViewsJsonV600) @@ -1875,7 +1880,7 @@ object Http4s600 { } // GET /obp/v6.0.0/management/system-views/SYS_VIEW_ID (non-standard var so middleware skips view validation) - lazy val getSystemViewById: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getSystemViewById: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "management" / "system-views" / viewIdStr => EndpointHelpers.withUser(req) { (_, cc) => ViewNewStyle.systemView(ViewId(viewIdStr), Some(cc)).map(JSONFactory600.createViewJsonV600) @@ -1883,7 +1888,7 @@ object Http4s600 { } // GET /obp/v6.0.0/management/abac-policies - lazy val getAbacPolicies: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getAbacPolicies: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "management" / "abac-policies" => EndpointHelpers.withUser(req) { (_, _) => Future { @@ -1897,7 +1902,7 @@ object Http4s600 { } // GET /obp/v6.0.0/management/connector/metrics/counts - lazy val getConnectorCallCounts: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getConnectorCallCounts: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "management" / "connector" / "metrics" / "counts" => EndpointHelpers.withUser(req) { (_, _) => Future { @@ -1915,7 +1920,7 @@ object Http4s600 { } // GET /obp/v6.0.0/management/connector/traces - lazy val getConnectorTraces: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getConnectorTraces: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "management" / "connector" / "traces" => EndpointHelpers.withUser(req) { (_, cc) => for { @@ -1927,7 +1932,7 @@ object Http4s600 { } // GET /obp/v6.0.0/management/diagnostics/dynamic-entities - lazy val getDynamicEntityDiagnostics: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getDynamicEntityDiagnostics: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "management" / "diagnostics" / "dynamic-entities" => EndpointHelpers.withUser(req) { (_, _) => Future { @@ -1943,7 +1948,7 @@ object Http4s600 { } // DELETE /obp/v6.0.0/management/diagnostics/dynamic-entities/orphaned-records - lazy val cleanupOrphanedDynamicEntityRecords: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val cleanupOrphanedDynamicEntityRecords: Http4sRoute = Http4sRoute { case req @ DELETE -> `prefixPath` / "management" / "diagnostics" / "dynamic-entities" / "orphaned-records" => EndpointHelpers.withUser(req) { (_, _) => Future { @@ -1962,7 +1967,7 @@ object Http4s600 { } // POST /obp/v6.0.0/management/webui_props - lazy val createWebUiProps: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val createWebUiProps: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "management" / "webui_props" => EndpointHelpers.withUserAndBodyCreated[WebUiPropsCommons, Any](req) { (user, postedData, cc) => for { @@ -1979,7 +1984,7 @@ object Http4s600 { // PUT /obp/v6.0.0/management/webui_props/WEBUI_PROP_NAME - lazy val createOrUpdateWebUiProps: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val createOrUpdateWebUiProps: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "management" / "webui_props" / webUiPropName => implicit val cc: CallContext = req.callContext implicit val formats: Formats = code.api.util.CustomJsonFormats.formats @@ -2013,7 +2018,7 @@ object Http4s600 { } // DELETE /obp/v6.0.0/management/webui_props/WEBUI_PROP_NAME - lazy val deleteWebUiProps: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val deleteWebUiProps: Http4sRoute = Http4sRoute { case req @ DELETE -> `prefixPath` / "management" / "webui_props" / webUiPropName => EndpointHelpers.executeDelete(req) { cc => val nameLower = webUiPropName.toLowerCase @@ -2039,7 +2044,7 @@ object Http4s600 { // ─── Phase 2: 3 small mixed buckets (5 endpoints) ───────────────────── // POST /obp/v6.0.0/management/banks/BANK_ID/accounts/ACCOUNT_ID/views (201) - lazy val createCustomViewManagement: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val createCustomViewManagement: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "management" / "banks" / bankIdStr / "accounts" / accountIdStr / "views" => EndpointHelpers.executeFutureCreated(req) { implicit val cc: CallContext = req.callContext @@ -2060,7 +2065,7 @@ object Http4s600 { } // GET /obp/v6.0.0/banks/BANK_ID/products/PRODUCT_CODE/tags - lazy val getProductTagsV600: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getProductTagsV600: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "products" / productCodeStr / "tags" => EndpointHelpers.withUserAndBank(req) { (_, bank, cc) => val productCode = com.openbankproject.commons.model.ProductCode(productCodeStr) @@ -2072,7 +2077,7 @@ object Http4s600 { } // PUT /obp/v6.0.0/banks/BANK_ID/products/PRODUCT_CODE/tags - lazy val updateProductTagsV600: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val updateProductTagsV600: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "banks" / _ / "products" / productCodeStr / "tags" => EndpointHelpers.executeAndRespond(req) { implicit cc => val rawBody = cc.httpBody.getOrElse("") @@ -2097,7 +2102,7 @@ object Http4s600 { } // GET /obp/v6.0.0/oidc/clients/CLIENT_ID - lazy val getOidcClient: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getOidcClient: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "oidc" / "clients" / clientId => EndpointHelpers.withUser(req) { (_, cc) => for { @@ -2120,7 +2125,7 @@ object Http4s600 { } // POST /obp/v6.0.0/oidc/clients/verify - lazy val verifyOidcClient: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val verifyOidcClient: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "oidc" / "clients" / "verify" => EndpointHelpers.executeAndRespond(req) { implicit cc => val rawBody = cc.httpBody.getOrElse("") @@ -2148,7 +2153,7 @@ object Http4s600 { // ─── Phase 2: users bucket (6 of 16; chat-room + special-purpose deferred) ─── // GET /obp/v6.0.0/users/USER_ID/attributes/USER_ATTRIBUTE_ID - lazy val getUserAttributeById: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getUserAttributeById: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "users" / userIdStr / "attributes" / userAttributeId => EndpointHelpers.withUser(req) { (_, cc) => for { @@ -2161,7 +2166,7 @@ object Http4s600 { } // POST /obp/v6.0.0/users/USER_ID/attributes (201) - lazy val createUserAttribute: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val createUserAttribute: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "users" / userIdStr / "attributes" => EndpointHelpers.executeFutureCreated(req) { implicit val cc: CallContext = req.callContext @@ -2181,7 +2186,7 @@ object Http4s600 { } // PUT /obp/v6.0.0/users/USER_ID/attributes/USER_ATTRIBUTE_ID - lazy val updateUserAttribute: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val updateUserAttribute: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "users" / userIdStr / "attributes" / userAttributeId => EndpointHelpers.executeAndRespond(req) { implicit cc => val rawBody = cc.httpBody.getOrElse("") @@ -2203,7 +2208,7 @@ object Http4s600 { } // DELETE /obp/v6.0.0/users/USER_ID/attributes/USER_ATTRIBUTE_ID - lazy val deleteUserAttribute: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val deleteUserAttribute: Http4sRoute = Http4sRoute { case req @ DELETE -> `prefixPath` / "users" / userIdStr / "attributes" / userAttributeId => EndpointHelpers.executeAndRespond(req) { implicit cc => for { @@ -2217,7 +2222,7 @@ object Http4s600 { } // POST /obp/v6.0.0/users/USER_ID/group-entitlements (201) - lazy val addUserToGroup: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val addUserToGroup: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "users" / userIdStr / "group-entitlements" => EndpointHelpers.executeFutureCreated(req) { implicit val cc: CallContext = req.callContext @@ -2270,7 +2275,7 @@ object Http4s600 { } // DELETE /obp/v6.0.0/users/USER_ID/group-entitlements/GROUP_ID - lazy val removeUserFromGroup: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val removeUserFromGroup: Http4sRoute = Http4sRoute { case req @ DELETE -> `prefixPath` / "users" / userIdStr / "group-entitlements" / groupId => EndpointHelpers.executeAndRespond(req) { implicit cc => val user = cc.user.openOrThrowException(AuthenticatedUserIsRequired) @@ -2300,7 +2305,7 @@ object Http4s600 { // ─── Phase 2: 4 more single-endpoint buckets ────────────────────────── // DELETE /obp/v6.0.0/entitlements/ENTITLEMENT_ID - lazy val deleteEntitlement: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val deleteEntitlement: Http4sRoute = Http4sRoute { case req @ DELETE -> `prefixPath` / "entitlements" / entitlementId => EndpointHelpers.executeAndRespond(req) { implicit cc => for { @@ -2314,7 +2319,7 @@ object Http4s600 { } // GET /obp/v6.0.0/personal-dynamic-entities/available - lazy val getAvailablePersonalDynamicEntities: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getAvailablePersonalDynamicEntities: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "personal-dynamic-entities" / "available" => EndpointHelpers.withUser(req) { (_, _) => Future(NewStyle.function.getDynamicEntities(None, true)) @@ -2323,7 +2328,7 @@ object Http4s600 { } // GET /obp/v6.0.0/management/dynamic-entities/reference-types - lazy val getReferenceTypes: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getReferenceTypes: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "management" / "dynamic-entities" / "reference-types" => EndpointHelpers.withUser(req) { (_, _) => Future { @@ -2366,7 +2371,7 @@ object Http4s600 { } // POST /obp/v6.0.0/chat-room-participants (201) — join a system chat room by joining_key - lazy val joinSystemChatRoom: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val joinSystemChatRoom: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "chat-room-participants" => EndpointHelpers.executeFutureCreated(req) { implicit val cc: CallContext = req.callContext @@ -2394,7 +2399,7 @@ object Http4s600 { AttributeTypeDocs.typesWithExamples // POST /obp/v6.0.0/banks/BANK_ID/accounts/ACCOUNT_ID/counterparties/COUNTERPARTY_ID/attributes (201) - lazy val createCounterpartyAttribute: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val createCounterpartyAttribute: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "banks" / _ / "accounts" / _ / "counterparties" / counterpartyId / "attributes" => EndpointHelpers.executeFutureCreated(req) { implicit val cc: CallContext = req.callContext @@ -2419,7 +2424,7 @@ object Http4s600 { } // DELETE /obp/v6.0.0/banks/BANK_ID/accounts/ACCOUNT_ID/counterparties/COUNTERPARTY_ID/attributes/COUNTERPARTY_ATTRIBUTE_ID - lazy val deleteCounterpartyAttribute: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val deleteCounterpartyAttribute: Http4sRoute = Http4sRoute { case req @ DELETE -> `prefixPath` / "banks" / _ / "accounts" / _ / "counterparties" / _ / "attributes" / attributeId => EndpointHelpers.executeDelete(req) { cc => code.api.util.newstyle.CounterpartyAttributeNewStyle.deleteCounterpartyAttribute(attributeId, Some(cc)) @@ -2427,7 +2432,7 @@ object Http4s600 { } // GET /obp/v6.0.0/banks/BANK_ID/accounts/ACCOUNT_ID/counterparties/COUNTERPARTY_ID/attributes/COUNTERPARTY_ATTRIBUTE_ID - lazy val getCounterpartyAttributeById: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getCounterpartyAttributeById: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "accounts" / _ / "counterparties" / _ / "attributes" / attributeId => EndpointHelpers.withUser(req) { (_, cc) => for { @@ -2437,7 +2442,7 @@ object Http4s600 { } // GET /obp/v6.0.0/banks/BANK_ID/accounts/ACCOUNT_ID/counterparties/COUNTERPARTY_ID/attributes - lazy val getAllCounterpartyAttributes: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getAllCounterpartyAttributes: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "accounts" / _ / "counterparties" / counterpartyId / "attributes" => EndpointHelpers.withUser(req) { (_, cc) => for { @@ -2448,7 +2453,7 @@ object Http4s600 { } // PUT /obp/v6.0.0/banks/BANK_ID/accounts/ACCOUNT_ID/counterparties/COUNTERPARTY_ID/attributes/COUNTERPARTY_ATTRIBUTE_ID - lazy val updateCounterpartyAttribute: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val updateCounterpartyAttribute: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "banks" / _ / "accounts" / _ / "counterparties" / counterpartyId / "attributes" / attributeId => EndpointHelpers.executeAndRespond(req) { implicit cc => val rawBody = cc.httpBody.getOrElse("") @@ -2472,7 +2477,7 @@ object Http4s600 { } // GET /obp/v6.0.0/banks/BANK_ID/accounts/ACCOUNT_ID/views/VIEW_ID/has-account-access - lazy val hasAccountAccess: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val hasAccountAccess: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / bankIdStr / "accounts" / accountIdStr / "views" / viewIdStr / "has-account-access" => EndpointHelpers.withUser(req) { (user, cc) => val bankId = BankId(bankIdStr) @@ -2503,7 +2508,7 @@ object Http4s600 { } // GET /obp/v6.0.0/my/account-access-requests - lazy val getMyAccountAccessRequests: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getMyAccountAccessRequests: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "my" / "account-access-requests" => EndpointHelpers.withUser(req) { (user, cc) => for { @@ -2517,7 +2522,7 @@ object Http4s600 { // ─── Phase 2: 3 anonymous/UserOrApplication endpoints ───────────────── // GET /obp/v6.0.0/webui-props/WEBUI_PROP_NAME - lazy val getWebUiProp: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getWebUiProp: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "webui-props" / webUiPropName => EndpointHelpers.executeAndRespond(req) { implicit cc => val active = req.uri.query.params.getOrElse("active", "false") @@ -2553,7 +2558,7 @@ object Http4s600 { } // GET /obp/v6.0.0/message-docs/CONNECTOR/json-schema - lazy val getMessageDocsJsonSchema: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getMessageDocsJsonSchema: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "message-docs" / connector / "json-schema" => EndpointHelpers.executeAndRespond(req) { implicit cc => val cacheKey = s"message-docs-json-schema-$connector" @@ -2582,7 +2587,7 @@ object Http4s600 { } // POST /obp/v6.0.0/users/verify-credentials - lazy val verifyUserCredentials: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val verifyUserCredentials: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "users" / "verify-credentials" => EndpointHelpers.executeAndRespond(req) { implicit cc => val rawBody = cc.httpBody.getOrElse("") @@ -2612,7 +2617,7 @@ object Http4s600 { } // GET /obp/v6.0.0/management/view-permissions - lazy val getViewPermissions: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getViewPermissions: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "management" / "view-permissions" => EndpointHelpers.withUser(req) { (_, cc) => Future { @@ -2640,7 +2645,7 @@ object Http4s600 { } // GET /obp/v6.0.0/api-products (all banks; auth-required; cached) - lazy val getAllApiProductsV600: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getAllApiProductsV600: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "api-products" => EndpointHelpers.withUser(req) { (_, cc) => val tagFilter = req.uri.query.params.get("tag").map(_.trim).filter(_.nonEmpty) @@ -2668,7 +2673,7 @@ object Http4s600 { } // GET /obp/v6.0.0/products (all banks; auth-required; cached) - lazy val getAllProductsV600: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getAllProductsV600: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "products" => EndpointHelpers.withUser(req) { (_, cc) => val params = req.uri.query.multiParams.toList.map { case (k, vs) => GetProductsParam(k, vs.toList) } @@ -2702,7 +2707,7 @@ object Http4s600 { // ─── Phase 2: account-access-requests + holding-accounts (3 endpoints) ─ // GET /obp/v6.0.0/banks/BANK_ID/accounts/ACCOUNT_ID/account-access-requests - lazy val getAccountAccessRequestsForAccount: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getAccountAccessRequestsForAccount: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / bankIdStr / "accounts" / accountIdStr / "account-access-requests" => EndpointHelpers.withBankAccount(req) { (_, _, cc) => val status = req.uri.query.params.get("status") @@ -2724,7 +2729,7 @@ object Http4s600 { } // GET /obp/v6.0.0/banks/BANK_ID/accounts/ACCOUNT_ID/account-access-requests/ACCOUNT_ACCESS_REQUEST_ID - lazy val getAccountAccessRequestById: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getAccountAccessRequestById: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / bankIdStr / "accounts" / accountIdStr / "account-access-requests" / requestId => EndpointHelpers.withBankAccount(req) { (_, _, cc) => for { @@ -2740,7 +2745,7 @@ object Http4s600 { } // GET /obp/v6.0.0/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/holding-accounts - lazy val getHoldingAccountByReleaser: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getHoldingAccountByReleaser: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / bankIdStr / "accounts" / accountIdStr / viewIdStr / "holding-accounts" => EndpointHelpers.withView(req) { (user, _, view, cc) => val bankId = BankId(bankIdStr) @@ -2774,7 +2779,7 @@ object Http4s600 { // ─── Phase 2: account-access-request lifecycle (3 endpoints) ───────── // POST /obp/v6.0.0/banks/BANK_ID/accounts/ACCOUNT_ID/account-access-requests (201) - lazy val createAccountAccessRequest: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val createAccountAccessRequest: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "banks" / bankIdStr / "accounts" / accountIdStr / "account-access-requests" => EndpointHelpers.executeFutureCreated(req) { implicit val cc: CallContext = req.callContext @@ -2819,7 +2824,7 @@ object Http4s600 { } // POST /obp/v6.0.0/banks/BANK_ID/accounts/ACCOUNT_ID/account-access-requests/.../approval (201) - lazy val approveAccountAccessRequest: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val approveAccountAccessRequest: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "banks" / bankIdStr / "accounts" / accountIdStr / "account-access-requests" / requestIdStr / "approval" => EndpointHelpers.executeFutureCreated(req) { implicit val cc: CallContext = req.callContext @@ -2878,7 +2883,7 @@ object Http4s600 { } // POST /obp/v6.0.0/banks/BANK_ID/accounts/ACCOUNT_ID/account-access-requests/.../rejection (201) - lazy val rejectAccountAccessRequest: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val rejectAccountAccessRequest: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "banks" / bankIdStr / "accounts" / accountIdStr / "account-access-requests" / requestIdStr / "rejection" => EndpointHelpers.executeFutureCreated(req) { implicit val cc: CallContext = req.callContext @@ -2919,7 +2924,7 @@ object Http4s600 { // ─── Phase 2: Signal bucket (6 endpoints) ──────────────────────────── // GET /obp/v6.0.0/signal-channels - lazy val getSignalChannels: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getSignalChannels: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "signal-channels" => EndpointHelpers.withUser(req) { (_, cc) => // Shared with the gRPC SignalChannelsService.ListChannels, see code.signal.SignalChannels @@ -2928,7 +2933,7 @@ object Http4s600 { } // GET /obp/v6.0.0/signal-channels/CHANNEL_NAME/info - lazy val getSignalChannelInfo: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getSignalChannelInfo: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "signal-channels" / channelName / "info" => EndpointHelpers.withUser(req) { (_, cc) => for { @@ -2951,7 +2956,7 @@ object Http4s600 { } // GET /obp/v6.0.0/signal-channels/stats - lazy val getSignalStats: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getSignalStats: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "signal-channels" / "stats" => EndpointHelpers.withUser(req) { (_, cc) => Future { @@ -2970,7 +2975,7 @@ object Http4s600 { } // POST /obp/v6.0.0/signal-channels/CHANNEL_NAME/messages (201) - lazy val publishSignalMessage: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val publishSignalMessage: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "signal-channels" / channelName / "messages" => EndpointHelpers.executeFutureCreated(req) { implicit val cc: CallContext = req.callContext @@ -3004,7 +3009,7 @@ object Http4s600 { } // GET /obp/v6.0.0/signal-channels/CHANNEL_NAME/messages - lazy val getSignalMessages: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getSignalMessages: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "signal-channels" / channelName / "messages" => EndpointHelpers.withUser(req) { (user, cc) => for { @@ -3031,7 +3036,7 @@ object Http4s600 { } // DELETE /obp/v6.0.0/signal-channels/CHANNEL_NAME (200 with body — not 204) - lazy val deleteSignalChannel: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val deleteSignalChannel: Http4sRoute = Http4sRoute { case req @ DELETE -> `prefixPath` / "signal-channels" / channelName => EndpointHelpers.executeAndRespond(req) { implicit cc => for { @@ -3065,7 +3070,7 @@ object Http4s600 { }.toMap // GET /obp/v6.0.0/banks/BANK_ID/chat-rooms - lazy val getBankChatRooms: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getBankChatRooms: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / bankIdStr / "chat-rooms" => EndpointHelpers.withUserAndBank(req) { (user, _, cc) => for { @@ -3080,7 +3085,7 @@ object Http4s600 { } // GET /obp/v6.0.0/chat-rooms - lazy val getSystemChatRooms: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getSystemChatRooms: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "chat-rooms" => EndpointHelpers.withUser(req) { (user, cc) => for { @@ -3095,7 +3100,7 @@ object Http4s600 { } // GET /obp/v6.0.0/banks/BANK_ID/chat-rooms/CHAT_ROOM_ID - lazy val getBankChatRoom: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getBankChatRoom: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "chat-rooms" / chatRoomId => EndpointHelpers.withUserAndBank(req) { (user, _, cc) => for { @@ -3108,7 +3113,7 @@ object Http4s600 { } // GET /obp/v6.0.0/chat-rooms/CHAT_ROOM_ID - lazy val getSystemChatRoom: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getSystemChatRoom: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "chat-rooms" / chatRoomId => EndpointHelpers.withUser(req) { (user, cc) => for { @@ -3123,7 +3128,7 @@ object Http4s600 { // ─── Phase 2: Chat-room my-views (6 endpoints) ──────────────────────── // GET /obp/v6.0.0/users/current/chat-rooms - lazy val getMyChatRooms: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getMyChatRooms: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "users" / "current" / "chat-rooms" => EndpointHelpers.withUser(req) { (user, cc) => for { @@ -3152,7 +3157,7 @@ object Http4s600 { } // GET /obp/v6.0.0/users/current/chat-rooms/unread - lazy val getMyUnreadCounts: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getMyUnreadCounts: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "users" / "current" / "chat-rooms" / "unread" => EndpointHelpers.withUser(req) { (user, cc) => for { @@ -3176,7 +3181,7 @@ object Http4s600 { } // PUT /obp/v6.0.0/users/current/chat-rooms/CHAT_ROOM_ID/read-marker - lazy val markChatRoomRead: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val markChatRoomRead: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "users" / "current" / "chat-rooms" / chatRoomId / "read-marker" => EndpointHelpers.executeAndRespond(req) { implicit cc => val user = cc.user.openOrThrowException("User not found in CallContext") @@ -3193,7 +3198,7 @@ object Http4s600 { } // GET /obp/v6.0.0/users/current/mentions - lazy val getMyMentions: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getMyMentions: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "users" / "current" / "mentions" => EndpointHelpers.withUser(req) { (user, cc) => val qp = req.uri.query.params @@ -3215,7 +3220,7 @@ object Http4s600 { } // POST /obp/v6.0.0/chat-rooms/search (200, NOT 201) - lazy val searchChatRooms: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val searchChatRooms: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "chat-rooms" / "search" => EndpointHelpers.executeAndRespond(req) { implicit cc => val rawBody = cc.httpBody.getOrElse("") @@ -3237,7 +3242,7 @@ object Http4s600 { } // GET /obp/v6.0.0/chat-rooms/CHAT_ROOM_ID/messages/reactions - lazy val getBulkReactions: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getBulkReactions: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "chat-rooms" / chatRoomId / "messages" / "reactions" => EndpointHelpers.withUser(req) { (user, cc) => for { @@ -3258,7 +3263,7 @@ object Http4s600 { // ─── Phase 2: Chat-room admin (5 endpoints) ─────────────────────────── // PUT /obp/v6.0.0/banks/BANK_ID/chat-rooms/CHAT_ROOM_ID/archive-status - lazy val archiveBankChatRoom: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val archiveBankChatRoom: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "banks" / _ / "chat-rooms" / chatRoomId / "archive-status" => EndpointHelpers.withUserAndBank(req) { (_, _, cc) => for { @@ -3273,7 +3278,7 @@ object Http4s600 { } // PUT /obp/v6.0.0/chat-rooms/CHAT_ROOM_ID/archive-status - lazy val archiveSystemChatRoom: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val archiveSystemChatRoom: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "chat-rooms" / chatRoomId / "archive-status" => EndpointHelpers.withUser(req) { (_, cc) => for { @@ -3288,7 +3293,7 @@ object Http4s600 { } // POST /obp/v6.0.0/banks/BANK_ID/chat-room-participants (201) - lazy val joinBankChatRoom: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val joinBankChatRoom: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "banks" / _ / "chat-room-participants" => EndpointHelpers.executeFutureCreated(req) { implicit val cc: CallContext = req.callContext @@ -3313,7 +3318,7 @@ object Http4s600 { } // PUT /obp/v6.0.0/banks/BANK_ID/chat-rooms/CHAT_ROOM_ID/joining-key - lazy val refreshBankJoiningKey: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val refreshBankJoiningKey: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "banks" / _ / "chat-rooms" / chatRoomId / "joining-key" => EndpointHelpers.withUserAndBank(req) { (user, _, cc) => for { @@ -3330,7 +3335,7 @@ object Http4s600 { } // PUT /obp/v6.0.0/chat-rooms/CHAT_ROOM_ID/joining-key - lazy val refreshSystemJoiningKey: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val refreshSystemJoiningKey: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "chat-rooms" / chatRoomId / "joining-key" => EndpointHelpers.withUser(req) { (user, cc) => for { @@ -3349,7 +3354,7 @@ object Http4s600 { // ─── Phase 2: Chat-room mutations (8 endpoints) ─────────────────────── // POST /obp/v6.0.0/banks/BANK_ID/chat-rooms (201) - lazy val createBankChatRoom: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val createBankChatRoom: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "banks" / bankIdStr / "chat-rooms" => EndpointHelpers.executeFutureCreated(req) { implicit val cc: CallContext = req.callContext @@ -3378,7 +3383,7 @@ object Http4s600 { } // POST /obp/v6.0.0/chat-rooms (201) - lazy val createSystemChatRoom: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val createSystemChatRoom: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "chat-rooms" => EndpointHelpers.executeFutureCreated(req) { implicit val cc: CallContext = req.callContext @@ -3407,7 +3412,7 @@ object Http4s600 { } // PUT /obp/v6.0.0/banks/BANK_ID/chat-rooms/CHAT_ROOM_ID - lazy val updateBankChatRoom: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val updateBankChatRoom: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "banks" / _ / "chat-rooms" / chatRoomId => EndpointHelpers.executeAndRespond(req) { implicit cc => val rawBody = cc.httpBody.getOrElse("") @@ -3432,7 +3437,7 @@ object Http4s600 { } // PUT /obp/v6.0.0/chat-rooms/CHAT_ROOM_ID - lazy val updateSystemChatRoom: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val updateSystemChatRoom: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "chat-rooms" / chatRoomId => EndpointHelpers.executeAndRespond(req) { implicit cc => val rawBody = cc.httpBody.getOrElse("") @@ -3457,7 +3462,7 @@ object Http4s600 { } // DELETE /obp/v6.0.0/banks/BANK_ID/chat-rooms/CHAT_ROOM_ID (204) - lazy val deleteBankChatRoom: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val deleteBankChatRoom: Http4sRoute = Http4sRoute { case req @ DELETE -> `prefixPath` / "banks" / _ / "chat-rooms" / chatRoomId => EndpointHelpers.executeDelete(req) { cc => for { @@ -3471,7 +3476,7 @@ object Http4s600 { } // DELETE /obp/v6.0.0/chat-rooms/CHAT_ROOM_ID (204) - lazy val deleteSystemChatRoom: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val deleteSystemChatRoom: Http4sRoute = Http4sRoute { case req @ DELETE -> `prefixPath` / "chat-rooms" / chatRoomId => EndpointHelpers.executeDelete(req) { cc => for { @@ -3485,7 +3490,7 @@ object Http4s600 { } // PUT /obp/v6.0.0/banks/BANK_ID/chat-rooms/CHAT_ROOM_ID/open-room - lazy val setBankChatRoomOpenRoom: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val setBankChatRoomOpenRoom: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "banks" / _ / "chat-rooms" / chatRoomId / "open-room" => EndpointHelpers.withUserAndBank(req) { (_, _, cc) => val rawBody = cc.httpBody.getOrElse("") @@ -3504,7 +3509,7 @@ object Http4s600 { } // PUT /obp/v6.0.0/chat-rooms/CHAT_ROOM_ID/open-room - lazy val setSystemChatRoomOpenRoom: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val setSystemChatRoomOpenRoom: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "chat-rooms" / chatRoomId / "open-room" => EndpointHelpers.executeAndRespond(req) { implicit cc => val rawBody = cc.httpBody.getOrElse("") @@ -3527,7 +3532,7 @@ object Http4s600 { // registrations live in a separate private def to keep under // the JVM 64KB method-size limit. Apply this pattern for future batches. - lazy val addBankChatRoomParticipant: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val addBankChatRoomParticipant: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "banks" / _ / "chat-rooms" / chatRoomId / "participants" => EndpointHelpers.executeFutureCreated(req) { implicit val cc: CallContext = req.callContext @@ -3565,7 +3570,7 @@ object Http4s600 { } } - lazy val addSystemChatRoomParticipant: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val addSystemChatRoomParticipant: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "chat-rooms" / chatRoomId / "participants" => EndpointHelpers.executeFutureCreated(req) { implicit val cc: CallContext = req.callContext @@ -3603,7 +3608,7 @@ object Http4s600 { } } - lazy val getBankChatRoomParticipants: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getBankChatRoomParticipants: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "chat-rooms" / chatRoomId / "participants" => EndpointHelpers.withUserAndBank(req) { (user, _, cc) => for { @@ -3618,7 +3623,7 @@ object Http4s600 { } } - lazy val getSystemChatRoomParticipants: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getSystemChatRoomParticipants: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "chat-rooms" / chatRoomId / "participants" => EndpointHelpers.withUser(req) { (user, cc) => for { @@ -3633,7 +3638,7 @@ object Http4s600 { } } - lazy val updateBankParticipantPermissions: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val updateBankParticipantPermissions: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "banks" / _ / "chat-rooms" / chatRoomId / "participants" / targetUserId => EndpointHelpers.withUserAndBank(req) { (user, _, cc) => val rawBody = cc.httpBody.getOrElse("") @@ -3657,7 +3662,7 @@ object Http4s600 { } } - lazy val updateSystemParticipantPermissions: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val updateSystemParticipantPermissions: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "chat-rooms" / chatRoomId / "participants" / targetUserId => EndpointHelpers.withUser(req) { (user, cc) => val rawBody = cc.httpBody.getOrElse("") @@ -3681,7 +3686,7 @@ object Http4s600 { } } - lazy val removeBankChatRoomParticipant: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val removeBankChatRoomParticipant: Http4sRoute = Http4sRoute { case req @ DELETE -> `prefixPath` / "banks" / _ / "chat-rooms" / chatRoomId / "participants" / targetUserId => EndpointHelpers.executeDelete(req) { cc => val u = cc.user.openOrThrowException("User not found in CallContext") @@ -3702,7 +3707,7 @@ object Http4s600 { } } - lazy val removeSystemChatRoomParticipant: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val removeSystemChatRoomParticipant: Http4sRoute = Http4sRoute { case req @ DELETE -> `prefixPath` / "chat-rooms" / chatRoomId / "participants" / targetUserId => EndpointHelpers.executeDelete(req) { cc => val u = cc.user.openOrThrowException("User not found in CallContext") @@ -3729,7 +3734,7 @@ object Http4s600 { // ─── Phase 2: Chat messages (10 endpoints) ──────────────────────────── - lazy val sendBankChatMessage: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val sendBankChatMessage: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "banks" / _ / "chat-rooms" / chatRoomId / "messages" => EndpointHelpers.executeFutureCreated(req) { implicit val cc: CallContext = req.callContext @@ -3779,7 +3784,7 @@ object Http4s600 { } } - lazy val sendSystemChatMessage: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val sendSystemChatMessage: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "chat-rooms" / chatRoomId / "messages" => EndpointHelpers.executeFutureCreated(req) { implicit val cc: CallContext = req.callContext @@ -3829,7 +3834,7 @@ object Http4s600 { } } - lazy val getBankChatMessages: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getBankChatMessages: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "chat-rooms" / chatRoomId / "messages" => EndpointHelpers.withUserAndBank(req) { (user, _, cc) => val qp = req.uri.query.params @@ -3848,7 +3853,7 @@ object Http4s600 { } } - lazy val getSystemChatMessages: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getSystemChatMessages: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "chat-rooms" / chatRoomId / "messages" => EndpointHelpers.withUser(req) { (user, cc) => val qp = req.uri.query.params @@ -3867,7 +3872,7 @@ object Http4s600 { } } - lazy val getBankChatMessage: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getBankChatMessage: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "chat-rooms" / chatRoomId / "messages" / chatMessageId => EndpointHelpers.withUserAndBank(req) { (user, _, cc) => for { @@ -3882,7 +3887,7 @@ object Http4s600 { } } - lazy val getSystemChatMessage: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getSystemChatMessage: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "chat-rooms" / chatRoomId / "messages" / chatMessageId => EndpointHelpers.withUser(req) { (user, cc) => for { @@ -3897,7 +3902,7 @@ object Http4s600 { } } - lazy val editBankChatMessage: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val editBankChatMessage: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "banks" / _ / "chat-rooms" / chatRoomId / "messages" / chatMessageId => EndpointHelpers.withUserAndBank(req) { (user, _, cc) => val rawBody = cc.httpBody.getOrElse("") @@ -3933,7 +3938,7 @@ object Http4s600 { } } - lazy val editSystemChatMessage: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val editSystemChatMessage: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "chat-rooms" / chatRoomId / "messages" / chatMessageId => EndpointHelpers.withUser(req) { (user, cc) => val rawBody = cc.httpBody.getOrElse("") @@ -3969,7 +3974,7 @@ object Http4s600 { } } - lazy val deleteBankChatMessage: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val deleteBankChatMessage: Http4sRoute = Http4sRoute { case req @ DELETE -> `prefixPath` / "banks" / _ / "chat-rooms" / chatRoomId / "messages" / chatMessageId => EndpointHelpers.executeDelete(req) { cc => val u = cc.user.openOrThrowException("User not found in CallContext") @@ -3993,7 +3998,7 @@ object Http4s600 { } } - lazy val deleteSystemChatMessage: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val deleteSystemChatMessage: Http4sRoute = Http4sRoute { case req @ DELETE -> `prefixPath` / "chat-rooms" / chatRoomId / "messages" / chatMessageId => EndpointHelpers.executeDelete(req) { cc => val u = cc.user.openOrThrowException("User not found in CallContext") @@ -4023,7 +4028,7 @@ object Http4s600 { // ─── Phase 2: Chat threads + reactions + typing (14 endpoints) ──────── - lazy val getBankThreadReplies: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getBankThreadReplies: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "chat-rooms" / chatRoomId / "messages" / chatMessageId / "thread" => EndpointHelpers.withUserAndBank(req) { (user, _, cc) => for { @@ -4046,7 +4051,7 @@ object Http4s600 { } } - lazy val getSystemThreadReplies: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getSystemThreadReplies: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "chat-rooms" / chatRoomId / "messages" / chatMessageId / "thread" => EndpointHelpers.withUser(req) { (user, cc) => for { @@ -4069,7 +4074,7 @@ object Http4s600 { } } - lazy val replyInBankThread: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val replyInBankThread: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "banks" / _ / "chat-rooms" / chatRoomId / "messages" / chatMessageId / "thread" => EndpointHelpers.executeFutureCreated(req) { implicit val cc: CallContext = req.callContext @@ -4124,7 +4129,7 @@ object Http4s600 { } } - lazy val replyInSystemThread: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val replyInSystemThread: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "chat-rooms" / chatRoomId / "messages" / chatMessageId / "thread" => EndpointHelpers.executeFutureCreated(req) { implicit val cc: CallContext = req.callContext @@ -4179,7 +4184,7 @@ object Http4s600 { } } - lazy val addBankReaction: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val addBankReaction: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "banks" / _ / "chat-rooms" / chatRoomId / "messages" / chatMessageId / "reactions" => EndpointHelpers.executeFutureCreated(req) { implicit val cc: CallContext = req.callContext @@ -4207,7 +4212,7 @@ object Http4s600 { } } - lazy val addSystemReaction: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val addSystemReaction: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "chat-rooms" / chatRoomId / "messages" / chatMessageId / "reactions" => EndpointHelpers.executeFutureCreated(req) { implicit val cc: CallContext = req.callContext @@ -4235,7 +4240,7 @@ object Http4s600 { } } - lazy val removeBankReaction: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val removeBankReaction: Http4sRoute = Http4sRoute { case req @ DELETE -> `prefixPath` / "banks" / _ / "chat-rooms" / chatRoomId / "messages" / chatMessageId / "reactions" / emoji => EndpointHelpers.executeDelete(req) { cc => val u = cc.user.openOrThrowException("User not found in CallContext") @@ -4258,7 +4263,7 @@ object Http4s600 { } } - lazy val removeSystemReaction: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val removeSystemReaction: Http4sRoute = Http4sRoute { case req @ DELETE -> `prefixPath` / "chat-rooms" / chatRoomId / "messages" / chatMessageId / "reactions" / emoji => EndpointHelpers.executeDelete(req) { cc => val u = cc.user.openOrThrowException("User not found in CallContext") @@ -4281,7 +4286,7 @@ object Http4s600 { } } - lazy val getBankReactions: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getBankReactions: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "chat-rooms" / chatRoomId / "messages" / chatMessageId / "reactions" => EndpointHelpers.withUserAndBank(req) { (user, _, cc) => for { @@ -4298,7 +4303,7 @@ object Http4s600 { } } - lazy val getSystemReactions: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getSystemReactions: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "chat-rooms" / chatRoomId / "messages" / chatMessageId / "reactions" => EndpointHelpers.withUser(req) { (user, cc) => for { @@ -4315,7 +4320,7 @@ object Http4s600 { } } - lazy val signalBankTyping: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val signalBankTyping: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "banks" / _ / "chat-rooms" / chatRoomId / "typing-indicators" => EndpointHelpers.executeAndRespond(req) { implicit cc => val u = cc.user.openOrThrowException("User not found in CallContext") @@ -4333,7 +4338,7 @@ object Http4s600 { } } - lazy val signalSystemTyping: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val signalSystemTyping: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "chat-rooms" / chatRoomId / "typing-indicators" => EndpointHelpers.executeAndRespond(req) { implicit cc => val u = cc.user.openOrThrowException("User not found in CallContext") @@ -4351,7 +4356,7 @@ object Http4s600 { } } - lazy val getBankTypingUsers: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getBankTypingUsers: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "chat-rooms" / chatRoomId / "typing-indicators" => EndpointHelpers.withUserAndBank(req) { (user, _, cc) => for { @@ -4381,7 +4386,7 @@ object Http4s600 { } } - lazy val getSystemTypingUsers: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getSystemTypingUsers: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "chat-rooms" / chatRoomId / "typing-indicators" => EndpointHelpers.withUser(req) { (user, cc) => for { @@ -4417,7 +4422,7 @@ object Http4s600 { // ─── Phase 2: Signatory Panels (5 endpoints) ───────────────────────── - lazy val createSignatoryPanel: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val createSignatoryPanel: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "banks" / _ / "mandates" / mandateId / "signatory-panels" => EndpointHelpers.executeFutureCreated(req) { implicit val cc: CallContext = req.callContext @@ -4435,7 +4440,7 @@ object Http4s600 { } } - lazy val getSignatoryPanels: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getSignatoryPanels: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "mandates" / mandateId / "signatory-panels" => EndpointHelpers.withUserAndBank(req) { (_, _, cc) => for { @@ -4447,7 +4452,7 @@ object Http4s600 { } } - lazy val getSignatoryPanel: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getSignatoryPanel: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "mandates" / _ / "signatory-panels" / panelId => EndpointHelpers.withUserAndBank(req) { (_, _, cc) => for { @@ -4459,7 +4464,7 @@ object Http4s600 { } } - lazy val updateSignatoryPanel: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val updateSignatoryPanel: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "banks" / _ / "mandates" / _ / "signatory-panels" / panelId => EndpointHelpers.executeAndRespond(req) { implicit cc => val rawBody = cc.httpBody.getOrElse("") @@ -4477,7 +4482,7 @@ object Http4s600 { } } - lazy val deleteSignatoryPanel: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val deleteSignatoryPanel: Http4sRoute = Http4sRoute { case req @ DELETE -> `prefixPath` / "banks" / _ / "mandates" / _ / "signatory-panels" / panelId => EndpointHelpers.executeDelete(req) { cc => for { @@ -4495,7 +4500,7 @@ object Http4s600 { // ─── Phase 2: Auth/JWT/validation/transaction-request endpoints (7) ── - lazy val validateUserEmail: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val validateUserEmail: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "users" / "email-validation" => EndpointHelpers.executeAndRespond(req) { implicit cc => val rawBody = cc.httpBody.getOrElse("") @@ -4544,7 +4549,7 @@ object Http4s600 { } } - lazy val resetPasswordComplete: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val resetPasswordComplete: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "users" / "password" => EndpointHelpers.executeFutureCreated(req) { implicit val cc: CallContext = req.callContext @@ -4586,7 +4591,7 @@ object Http4s600 { } } - lazy val resetPasswordUrlAnonymous: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val resetPasswordUrlAnonymous: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "users" / "password-reset-url" => EndpointHelpers.executeFutureCreated(req) { implicit val cc: CallContext = req.callContext @@ -4642,7 +4647,7 @@ object Http4s600 { } } - lazy val validateDynamicResourceDoc: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val validateDynamicResourceDoc: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "management" / "dynamic-resource-docs" / "validate" => EndpointHelpers.executeAndRespond(req) { implicit cc => val rawBody = cc.httpBody.getOrElse("") @@ -4735,25 +4740,25 @@ object Http4s600 { } } - lazy val createTransactionRequestHold: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val createTransactionRequestHold: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "banks" / bankIdStr / "accounts" / accountIdStr / viewIdStr / "transaction-request-types" / "HOLD" / "transaction-requests" => txReqDelegate(req, bankIdStr, accountIdStr, viewIdStr, "HOLD") } - lazy val createTransactionRequestCardano: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val createTransactionRequestCardano: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "banks" / bankIdStr / "accounts" / accountIdStr / viewIdStr / "transaction-request-types" / "CARDANO" / "transaction-requests" => txReqDelegate(req, bankIdStr, accountIdStr, viewIdStr, "CARDANO") } - lazy val createTransactionRequestEthereumeSendTransaction: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val createTransactionRequestEthereumeSendTransaction: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "banks" / bankIdStr / "accounts" / accountIdStr / viewIdStr / "transaction-request-types" / "ETH_SEND_TRANSACTION" / "transaction-requests" => txReqDelegate(req, bankIdStr, accountIdStr, viewIdStr, "ETH_SEND_TRANSACTION") } - lazy val createTransactionRequestEthSendRawTransaction: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val createTransactionRequestEthSendRawTransaction: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "banks" / bankIdStr / "accounts" / accountIdStr / viewIdStr / "transaction-request-types" / "ETH_SEND_RAW_TRANSACTION" / "transaction-requests" => txReqDelegate(req, bankIdStr, accountIdStr, viewIdStr, "ETH_SEND_RAW_TRANSACTION") @@ -4772,7 +4777,7 @@ object Http4s600 { // ─── Phase 2: User memberships, access listing, customer creation (4) ─ - lazy val getUserGroupMemberships: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getUserGroupMemberships: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "users" / userId / "group-entitlements" => EndpointHelpers.withUser(req) { (u, cc) => for { @@ -4810,7 +4815,7 @@ object Http4s600 { } } - lazy val getUsersWithAccountAccess: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getUsersWithAccountAccess: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / bankIdStr / "accounts" / accountIdStr / "views" / viewIdStr / "users-with-access" => EndpointHelpers.withBankAccount(req) { (_, _, cc) => val bankId = BankId(bankIdStr) @@ -4860,7 +4865,7 @@ object Http4s600 { } } - lazy val createRetailCustomer: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val createRetailCustomer: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "banks" / bankIdStr / "retail-customers" => EndpointHelpers.executeFutureCreated(req) { implicit val cc: CallContext = req.callContext @@ -4920,7 +4925,7 @@ object Http4s600 { } } - lazy val createCorporateCustomer: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val createCorporateCustomer: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "banks" / bankIdStr / "corporate-customers" => EndpointHelpers.executeFutureCreated(req) { implicit val cc: CallContext = req.callContext @@ -4967,7 +4972,7 @@ object Http4s600 { // ─── Phase 2: Final batch — getUserByUserId, directLogin, ABAC (5), dynamic-entity backup/cascade (3) ─ - lazy val getUserByUserId: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getUserByUserId: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "users" / "user-id" / userId => EndpointHelpers.withUser(req) { (_, cc) => for { @@ -5023,7 +5028,7 @@ object Http4s600 { }.toMap } - lazy val directLoginEndpoint: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val directLoginEndpoint: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "my" / "logins" / "direct" => EndpointHelpers.executeFutureCreated(req) { implicit val cc: CallContext = req.callContext @@ -5046,7 +5051,7 @@ object Http4s600 { } } - lazy val validateAbacRule: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val validateAbacRule: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "management" / "abac-rules" / "validate" => EndpointHelpers.executeAndRespond(req) { implicit cc => val rawBody = cc.httpBody.getOrElse("") @@ -5092,7 +5097,7 @@ object Http4s600 { } } - lazy val executeAbacRule: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val executeAbacRule: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "management" / "abac-rules" / ruleId / "execute" => EndpointHelpers.executeAndRespond(req) { implicit cc => val rawBody = cc.httpBody.getOrElse("") @@ -5118,7 +5123,7 @@ object Http4s600 { } } - lazy val executeAbacPolicy: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val executeAbacPolicy: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "management" / "abac-policies" / policy / "execute" => EndpointHelpers.executeAndRespond(req) { implicit cc => val rawBody = cc.httpBody.getOrElse("") @@ -5347,7 +5352,7 @@ object Http4s600 { ) ) - lazy val getAbacRuleSchema: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getAbacRuleSchema: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "management" / "abac-rules-schema" => EndpointHelpers.executeAndRespond(req) { implicit cc => Future.successful(buildAbacRuleSchemaJson()) @@ -5449,7 +5454,7 @@ object Http4s600 { } } - lazy val backupSystemDynamicEntity: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val backupSystemDynamicEntity: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "management" / "system-dynamic-entities" / dynamicEntityId / "backup" => EndpointHelpers.executeFutureCreated(req) { implicit val cc: CallContext = req.callContext @@ -5458,7 +5463,7 @@ object Http4s600 { } } - lazy val backupBankLevelDynamicEntity: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val backupBankLevelDynamicEntity: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "management" / "banks" / bankIdStr / "dynamic-entities" / dynamicEntityId / "backup" => EndpointHelpers.executeFutureCreated(req) { implicit val cc: CallContext = req.callContext @@ -5499,7 +5504,7 @@ object Http4s600 { _ <- NewStyle.function.deleteDynamicEntity(bankIdOpt, dynamicEntityId) } yield JObject(Nil) - lazy val deleteSystemDynamicEntityCascade: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val deleteSystemDynamicEntityCascade: Http4sRoute = Http4sRoute { case req @ DELETE -> `prefixPath` / "management" / "system-dynamic-entities" / "cascade" / dynamicEntityId => EndpointHelpers.executeAndRespond(req) { implicit cc => for { @@ -5514,7 +5519,7 @@ object Http4s600 { initFinal9ResourceDocs() // Route: GET /obp/v6.0.0/banks/BANK_ID/customers/CUSTOMER_ID/investigation-report - lazy val getCustomerInvestigationReport: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getCustomerInvestigationReport: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "customers" / customerId / "investigation-report" => EndpointHelpers.withUserAndBank(req) { (_, bank, cc) => val qp = req.uri.query.params @@ -5550,7 +5555,7 @@ object Http4s600 { // ─── Phase 2: banks/.../customer-links bucket (5 endpoints) ─────────── // Route: POST /obp/v6.0.0/banks/BANK_ID/customer-links (201) - lazy val createCustomerLink: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val createCustomerLink: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "banks" / _ / "customer-links" => EndpointHelpers.executeFutureCreated(req) { implicit val cc: CallContext = req.callContext @@ -5578,7 +5583,7 @@ object Http4s600 { // Route: GET /obp/v6.0.0/banks/BANK_ID/customer-links - lazy val getCustomerLinksByBankId: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getCustomerLinksByBankId: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "customer-links" => EndpointHelpers.withUserAndBank(req) { (_, bank, cc) => for { @@ -5589,7 +5594,7 @@ object Http4s600 { // Route: GET /obp/v6.0.0/banks/BANK_ID/customer-links/CUSTOMER_LINK_ID - lazy val getCustomerLinkById: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getCustomerLinkById: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "customer-links" / customerLinkId => EndpointHelpers.withUserAndBank(req) { (_, _, cc) => for { @@ -5600,7 +5605,7 @@ object Http4s600 { // Route: PUT /obp/v6.0.0/banks/BANK_ID/customer-links/CUSTOMER_LINK_ID - lazy val updateCustomerLink: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val updateCustomerLink: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "banks" / _ / "customer-links" / customerLinkId => EndpointHelpers.executeAndRespond(req) { implicit cc => val rawBody = cc.httpBody.getOrElse("") @@ -5615,7 +5620,7 @@ object Http4s600 { // Route: DELETE /obp/v6.0.0/banks/BANK_ID/customer-links/CUSTOMER_LINK_ID - lazy val deleteCustomerLink: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val deleteCustomerLink: Http4sRoute = Http4sRoute { case req @ DELETE -> `prefixPath` / "banks" / _ / "customer-links" / customerLinkId => EndpointHelpers.executeAndRespond(req) { implicit cc => for { @@ -5626,7 +5631,7 @@ object Http4s600 { // Route: GET /obp/v6.0.0/management/banks/BANK_ID/accounts/ACCOUNT_ID/views/VIEW_ID - lazy val getCustomViewById: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getCustomViewById: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "management" / "banks" / bankIdStr / "accounts" / accountIdStr / "views" / viewIdStr => EndpointHelpers.withUser(req) { (_, cc) => for { @@ -5640,7 +5645,7 @@ object Http4s600 { // Route: POST /obp/v6.0.0/management/cache/namespaces/invalidate - lazy val invalidateCacheNamespace: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val invalidateCacheNamespace: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "management" / "cache" / "namespaces" / "invalidate" => EndpointHelpers.executeAndRespond(req) { implicit cc => val rawBody = cc.httpBody.getOrElse("") @@ -5668,7 +5673,7 @@ object Http4s600 { // Route: GET /obp/v6.0.0/management/config-props - lazy val getConfigProps: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getConfigProps: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "management" / "config-props" => EndpointHelpers.withUser(req) { (_, _) => Future { @@ -5682,7 +5687,7 @@ object Http4s600 { // Route: GET /obp/v6.0.0/app-directory - lazy val getAppDirectory: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getAppDirectory: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "app-directory" => EndpointHelpers.executeAndRespond(req) { _ => Future { @@ -5694,7 +5699,7 @@ object Http4s600 { // Route: GET /obp/v6.0.0/management/custom-views - lazy val getCustomViews: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getCustomViews: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "management" / "custom-views" => EndpointHelpers.withUser(req) { (_, _) => Future(JSONFactory600.createViewsJsonV600(code.views.system.ViewDefinition.getCustomViews())) @@ -5703,7 +5708,7 @@ object Http4s600 { // Route: GET /obp/v6.0.0/management/roles-with-entitlement-counts - lazy val getRolesWithEntitlementCountsAtAllBanks: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getRolesWithEntitlementCountsAtAllBanks: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "management" / "roles-with-entitlement-counts" => EndpointHelpers.withUser(req) { (_, _) => val allRoles = code.api.util.ApiRole.availableRoles.sorted @@ -5719,7 +5724,7 @@ object Http4s600 { // ─── Phase 2: 5 small single-endpoint buckets ───────────────────────── // Route: GET /obp/v6.0.0/features - lazy val getFeatures: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getFeatures: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "features" => EndpointHelpers.executeAndRespond(req) { _ => Future.successful(FeaturesJsonV600( @@ -5741,7 +5746,7 @@ object Http4s600 { // Route: GET /obp/v6.0.0/providers - lazy val getProviders: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getProviders: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "providers" => EndpointHelpers.withUser(req) { (_, _) => Future(code.model.dataAccess.ResourceUser.getDistinctProviders) @@ -5751,7 +5756,7 @@ object Http4s600 { // Route: GET /obp/v6.0.0/consumers/current - lazy val getCurrentConsumer: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getCurrentConsumer: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "consumers" / "current" => EndpointHelpers.withUser(req) { (_, cc) => for { @@ -5771,7 +5776,7 @@ object Http4s600 { // Route: GET /obp/v6.0.0/api/popular-endpoints - lazy val getPopularApis: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getPopularApis: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "api" / "popular-endpoints" => EndpointHelpers.executeAndRespond(req) { implicit cc => for { @@ -5793,7 +5798,7 @@ object Http4s600 { // Route: GET /obp/v6.0.0/banks/BANK_ID/account-directory - lazy val getAccountDirectory: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getAccountDirectory: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "account-directory" => EndpointHelpers.withUserAndBank(req) { (_, bank, cc) => val allowedParams = List("limit", "offset", "sort_direction") @@ -5834,7 +5839,7 @@ object Http4s600 { list_of_roles = group.listOfRoles, is_enabled = group.isEnabled) // Route: POST /obp/v6.0.0/management/groups (201) - lazy val createGroup: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val createGroup: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "management" / "groups" => EndpointHelpers.executeFutureCreated(req) { implicit val cc: CallContext = req.callContext @@ -5858,7 +5863,7 @@ object Http4s600 { // Route: GET /obp/v6.0.0/management/groups/GROUP_ID - lazy val getGroup: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getGroup: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "management" / "groups" / groupId => EndpointHelpers.withUser(req) { (user, cc) => for { @@ -5871,7 +5876,7 @@ object Http4s600 { // Route: GET /obp/v6.0.0/management/groups - lazy val getGroups: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getGroups: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "management" / "groups" => EndpointHelpers.withUser(req) { (user, cc) => val bankIdParam = req.uri.query.params.get("bank_id") @@ -5893,7 +5898,7 @@ object Http4s600 { // Route: PUT /obp/v6.0.0/management/groups/GROUP_ID - lazy val updateGroup: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val updateGroup: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "management" / "groups" / groupId => EndpointHelpers.executeAndRespond(req) { implicit cc => val rawBody = cc.httpBody.getOrElse("") @@ -5915,7 +5920,7 @@ object Http4s600 { // Route: DELETE /obp/v6.0.0/management/groups/GROUP_ID - lazy val deleteGroup: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val deleteGroup: Http4sRoute = Http4sRoute { case req @ DELETE -> `prefixPath` / "management" / "groups" / groupId => EndpointHelpers.executeAndRespond(req) { implicit cc => val user = cc.user.openOrThrowException(AuthenticatedUserIsRequired) @@ -5933,7 +5938,7 @@ object Http4s600 { // Route: GET /obp/v6.0.0/management/groups/GROUP_ID/entitlements - lazy val getGroupEntitlements: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getGroupEntitlements: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "management" / "groups" / groupId / "entitlements" => EndpointHelpers.withUser(req) { (_, cc) => for { @@ -5960,7 +5965,7 @@ object Http4s600 { // classification + rule-engine integration warrants its own batch. // Route: POST /obp/v6.0.0/management/abac-rules (201) - lazy val createAbacRule: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val createAbacRule: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "management" / "abac-rules" => EndpointHelpers.executeFutureWithStatus(req) { implicit val cc: CallContext = req.callContext @@ -5994,7 +5999,7 @@ object Http4s600 { // Route: GET /obp/v6.0.0/management/abac-rules/ABAC_RULE_ID - lazy val getAbacRule: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getAbacRule: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "management" / "abac-rules" / ruleId => EndpointHelpers.withUser(req) { (_, cc) => for { @@ -6006,7 +6011,7 @@ object Http4s600 { // Route: GET /obp/v6.0.0/management/abac-rules - lazy val getAbacRules: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getAbacRules: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "management" / "abac-rules" => EndpointHelpers.withUser(req) { (_, _) => Future(createAbacRulesJsonV600(MappedAbacRuleProvider.getAllAbacRules())) @@ -6015,7 +6020,7 @@ object Http4s600 { // Route: GET /obp/v6.0.0/management/abac-rules/policy/POLICY - lazy val getAbacRulesByPolicy: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getAbacRulesByPolicy: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "management" / "abac-rules" / "policy" / policy => EndpointHelpers.withUser(req) { (_, _) => Future(createAbacRulesJsonV600(MappedAbacRuleProvider.getAbacRulesByPolicy(policy))) @@ -6024,7 +6029,7 @@ object Http4s600 { // Route: PUT /obp/v6.0.0/management/abac-rules/ABAC_RULE_ID - lazy val updateAbacRule: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val updateAbacRule: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "management" / "abac-rules" / ruleId => EndpointHelpers.executeFutureWithStatus(req) { implicit val cc: CallContext = req.callContext @@ -6061,7 +6066,7 @@ object Http4s600 { // Route: DELETE /obp/v6.0.0/management/abac-rules/ABAC_RULE_ID - lazy val deleteAbacRule: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val deleteAbacRule: Http4sRoute = Http4sRoute { case req @ DELETE -> `prefixPath` / "management" / "abac-rules" / ruleId => EndpointHelpers.executeFutureWithStatus(req) { implicit val cc: CallContext = req.callContext @@ -6093,7 +6098,7 @@ object Http4s600 { s"$InvalidJsonFormat The `type` field can only accept: ${AttributeTypeDocs.typesWithExamples}" // Route: POST /obp/v6.0.0/my/personal-data-fields (201) - lazy val createPersonalDataField: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val createPersonalDataField: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "my" / "personal-data-fields" => EndpointHelpers.executeFutureCreated(req) { implicit val cc: CallContext = req.callContext @@ -6114,7 +6119,7 @@ object Http4s600 { // Route: GET /obp/v6.0.0/my/personal-data-fields - lazy val getPersonalDataFields: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getPersonalDataFields: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "my" / "personal-data-fields" => EndpointHelpers.withUser(req) { (user, cc) => for { @@ -6125,7 +6130,7 @@ object Http4s600 { // Route: GET /obp/v6.0.0/my/personal-data-fields/USER_ATTRIBUTE_ID - lazy val getPersonalDataFieldById: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getPersonalDataFieldById: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "my" / "personal-data-fields" / userAttributeId => EndpointHelpers.withUser(req) { (user, cc) => for { @@ -6138,7 +6143,7 @@ object Http4s600 { // Route: PUT /obp/v6.0.0/my/personal-data-fields/USER_ATTRIBUTE_ID - lazy val updatePersonalDataField: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val updatePersonalDataField: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "my" / "personal-data-fields" / userAttributeId => EndpointHelpers.executeAndRespond(req) { implicit cc => val rawBody = cc.httpBody.getOrElse("") @@ -6161,7 +6166,7 @@ object Http4s600 { // Route: DELETE /obp/v6.0.0/my/personal-data-fields/USER_ATTRIBUTE_ID - lazy val deletePersonalDataField: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val deletePersonalDataField: Http4sRoute = Http4sRoute { case req @ DELETE -> `prefixPath` / "my" / "personal-data-fields" / userAttributeId => EndpointHelpers.executeAndRespond(req) { implicit cc => val user = cc.user.openOrThrowException(AuthenticatedUserIsRequired) @@ -6178,7 +6183,7 @@ object Http4s600 { // ─── Phase 2: management/consumers bucket (6 endpoints) ─────────────── // Route: GET /obp/v6.0.0/management/consumers/CONSUMER_ID/call-counters - lazy val getConsumerCallCounters: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getConsumerCallCounters: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "management" / "consumers" / consumerId / "call-counters" => EndpointHelpers.withUser(req) { (_, cc) => for { @@ -6190,7 +6195,7 @@ object Http4s600 { // Route: POST /obp/v6.0.0/management/consumers/CONSUMER_ID/consumer/rate-limits (201) - lazy val createCallLimits: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val createCallLimits: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "management" / "consumers" / consumerId / "consumer" / "rate-limits" => EndpointHelpers.executeFutureCreated(req) { implicit val cc: CallContext = req.callContext @@ -6213,7 +6218,7 @@ object Http4s600 { // Route: PUT /obp/v6.0.0/management/consumers/CONSUMER_ID/consumer/rate-limits/RATE_LIMITING_ID - lazy val updateRateLimits: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val updateRateLimits: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "management" / "consumers" / consumerId / "consumer" / "rate-limits" / rateLimitingId => EndpointHelpers.executeAndRespond(req) { implicit cc => val rawBody = cc.httpBody.getOrElse("") @@ -6236,7 +6241,7 @@ object Http4s600 { // Route: DELETE /obp/v6.0.0/management/consumers/CONSUMER_ID/consumer/rate-limits/RATE_LIMITING_ID - lazy val deleteCallLimits: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val deleteCallLimits: Http4sRoute = Http4sRoute { case req @ DELETE -> `prefixPath` / "management" / "consumers" / consumerId / "consumer" / "rate-limits" / rateLimitingId => EndpointHelpers.executeDelete(req) { cc => for { @@ -6248,7 +6253,7 @@ object Http4s600 { // Route: GET /obp/v6.0.0/management/consumers/CONSUMER_ID/active-rate-limits - lazy val getActiveRateLimitsNow: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getActiveRateLimitsNow: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "management" / "consumers" / consumerId / "active-rate-limits" => EndpointHelpers.withUser(req) { (_, cc) => for { @@ -6261,7 +6266,7 @@ object Http4s600 { // Route: GET /obp/v6.0.0/management/consumers/CONSUMER_ID/active-rate-limits/DATE_WITH_HOUR - lazy val getActiveRateLimitsAtDate: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getActiveRateLimitsAtDate: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "management" / "consumers" / consumerId / "active-rate-limits" / dateWithHourString => EndpointHelpers.withUser(req) { (_, cc) => for { @@ -6282,7 +6287,7 @@ object Http4s600 { // ─── Phase 2: management/api-collections bucket (4 endpoints) ───────── // Route: POST /obp/v6.0.0/management/api-collections/featured (201) - lazy val createFeaturedApiCollection: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val createFeaturedApiCollection: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "management" / "api-collections" / "featured" => EndpointHelpers.executeFutureCreated(req) { implicit val cc: CallContext = req.callContext @@ -6304,7 +6309,7 @@ object Http4s600 { // Route: GET /obp/v6.0.0/management/api-collections/featured - lazy val getFeaturedApiCollectionsAdmin: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getFeaturedApiCollectionsAdmin: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "management" / "api-collections" / "featured" => EndpointHelpers.withUser(req) { (_, cc) => for { @@ -6315,7 +6320,7 @@ object Http4s600 { // Route: PUT /obp/v6.0.0/management/api-collections/featured/API_COLLECTION_ID - lazy val updateFeaturedApiCollection: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val updateFeaturedApiCollection: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "management" / "api-collections" / "featured" / apiCollectionId => EndpointHelpers.executeAndRespond(req) { implicit cc => val rawBody = cc.httpBody.getOrElse("") @@ -6331,7 +6336,7 @@ object Http4s600 { // Route: DELETE /obp/v6.0.0/management/api-collections/featured/API_COLLECTION_ID - lazy val deleteFeaturedApiCollection: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val deleteFeaturedApiCollection: Http4sRoute = Http4sRoute { case req @ DELETE -> `prefixPath` / "management" / "api-collections" / "featured" / apiCollectionId => EndpointHelpers.executeAndRespond(req) { implicit cc => for { @@ -6347,7 +6352,7 @@ object Http4s600 { // gating would be a Phase 3 follow-up if needed. // Route: POST /obp/v6.0.0/banks/BANK_ID/api-products/API_PRODUCT_CODE (201) - lazy val createApiProduct: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val createApiProduct: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "banks" / _ / "api-products" / apiProductCode => EndpointHelpers.executeFutureCreated(req) { implicit val cc: CallContext = req.callContext @@ -6375,7 +6380,7 @@ object Http4s600 { // Route: PUT /obp/v6.0.0/banks/BANK_ID/api-products/API_PRODUCT_CODE (201 — Lift returns 201) - lazy val createOrUpdateApiProduct: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val createOrUpdateApiProduct: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "banks" / _ / "api-products" / apiProductCode => EndpointHelpers.executeFutureCreated(req) { implicit val cc: CallContext = req.callContext @@ -6403,7 +6408,7 @@ object Http4s600 { // Route: GET /obp/v6.0.0/banks/BANK_ID/api-products/API_PRODUCT_CODE - lazy val getApiProduct: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getApiProduct: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "api-products" / apiProductCode => EndpointHelpers.withUserAndBank(req) { (_, bank, cc) => for { @@ -6415,7 +6420,7 @@ object Http4s600 { // Route: GET /obp/v6.0.0/banks/BANK_ID/api-products - lazy val getApiProducts: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getApiProducts: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "api-products" => EndpointHelpers.withUserAndBank(req) { (_, bank, cc) => val tagFilter = req.uri.query.params.get("tag").map(_.trim).filter(_.nonEmpty) @@ -6427,7 +6432,7 @@ object Http4s600 { // Route: DELETE /obp/v6.0.0/banks/BANK_ID/api-products/API_PRODUCT_CODE - lazy val deleteApiProduct: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val deleteApiProduct: Http4sRoute = Http4sRoute { case req @ DELETE -> `prefixPath` / "banks" / _ / "api-products" / apiProductCode => EndpointHelpers.withUserAndBank(req) { (_, bank, cc) => for { @@ -6439,7 +6444,7 @@ object Http4s600 { // Route: POST /obp/v6.0.0/banks/BANK_ID/api-products/API_PRODUCT_CODE/attribute (201) - lazy val createApiProductAttribute: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val createApiProductAttribute: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "banks" / _ / "api-products" / apiProductCode / "attribute" => EndpointHelpers.executeFutureCreated(req) { implicit val cc: CallContext = req.callContext @@ -6459,7 +6464,7 @@ object Http4s600 { // Route: PUT /obp/v6.0.0/banks/BANK_ID/api-products/API_PRODUCT_CODE/attributes/API_PRODUCT_ATTRIBUTE_ID - lazy val updateApiProductAttribute: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val updateApiProductAttribute: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "banks" / _ / "api-products" / apiProductCode / "attributes" / apiProductAttributeId => EndpointHelpers.executeAndRespond(req) { implicit cc => val rawBody = cc.httpBody.getOrElse("") @@ -6478,7 +6483,7 @@ object Http4s600 { // Route: GET /obp/v6.0.0/banks/BANK_ID/api-products/API_PRODUCT_CODE/attributes/API_PRODUCT_ATTRIBUTE_ID - lazy val getApiProductAttribute: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getApiProductAttribute: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "api-products" / _ / "attributes" / apiProductAttributeId => EndpointHelpers.withUserAndBank(req) { (_, _, cc) => for { @@ -6489,7 +6494,7 @@ object Http4s600 { // Route: DELETE /obp/v6.0.0/banks/BANK_ID/api-products/API_PRODUCT_CODE/attributes/API_PRODUCT_ATTRIBUTE_ID - lazy val deleteApiProductAttribute: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val deleteApiProductAttribute: Http4sRoute = Http4sRoute { case req @ DELETE -> `prefixPath` / "banks" / _ / "api-products" / _ / "attributes" / apiProductAttributeId => EndpointHelpers.withUserAndBank(req) { (_, _, cc) => for { @@ -6513,7 +6518,7 @@ object Http4s600 { } // Route: POST /obp/v6.0.0/banks/BANK_ID/accounts/ACCOUNT_ID/mandates (201) - lazy val createMandate: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val createMandate: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "banks" / _ / "accounts" / _ / "mandates" => EndpointHelpers.executeFutureCreated(req) { implicit val cc: CallContext = req.callContext @@ -6538,7 +6543,7 @@ object Http4s600 { // Route: GET /obp/v6.0.0/banks/BANK_ID/accounts/ACCOUNT_ID/mandates - lazy val getMandates: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getMandates: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "accounts" / _ / "mandates" => EndpointHelpers.withBankAccount(req) { (_, account, cc) => for { @@ -6551,7 +6556,7 @@ object Http4s600 { // Route: GET /obp/v6.0.0/banks/BANK_ID/accounts/ACCOUNT_ID/mandates/MANDATE_ID - lazy val getMandate: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getMandate: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "accounts" / _ / "mandates" / mandateId => EndpointHelpers.withBankAccount(req) { (_, _, cc) => for { @@ -6563,7 +6568,7 @@ object Http4s600 { // Route: PUT /obp/v6.0.0/banks/BANK_ID/accounts/ACCOUNT_ID/mandates/MANDATE_ID - lazy val updateMandate: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val updateMandate: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "banks" / _ / "accounts" / _ / "mandates" / mandateId => EndpointHelpers.executeAndRespond(req) { implicit cc => val rawBody = cc.httpBody.getOrElse("") @@ -6584,7 +6589,7 @@ object Http4s600 { // Route: DELETE /obp/v6.0.0/banks/BANK_ID/accounts/ACCOUNT_ID/mandates/MANDATE_ID (204) - lazy val deleteMandate: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val deleteMandate: Http4sRoute = Http4sRoute { case req @ DELETE -> `prefixPath` / "banks" / _ / "accounts" / _ / "mandates" / mandateId => EndpointHelpers.executeAndRespond(req) { implicit cc => for { @@ -6601,7 +6606,7 @@ object Http4s600 { } // Route: POST /obp/v6.0.0/banks/BANK_ID/mandates/MANDATE_ID/provisions (201) - lazy val createMandateProvision: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val createMandateProvision: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "banks" / _ / "mandates" / mandateId / "provisions" => EndpointHelpers.executeFutureCreated(req) { implicit val cc: CallContext = req.callContext @@ -6626,7 +6631,7 @@ object Http4s600 { // Route: GET /obp/v6.0.0/banks/BANK_ID/mandates/MANDATE_ID/provisions - lazy val getMandateProvisions: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getMandateProvisions: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "mandates" / mandateId / "provisions" => EndpointHelpers.withUserAndBank(req) { (_, _, cc) => for { @@ -6638,7 +6643,7 @@ object Http4s600 { // Route: GET /obp/v6.0.0/banks/BANK_ID/mandates/MANDATE_ID/provisions/PROVISION_ID - lazy val getMandateProvision: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getMandateProvision: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "mandates" / _ / "provisions" / provisionId => EndpointHelpers.withUserAndBank(req) { (_, _, cc) => for { @@ -6650,7 +6655,7 @@ object Http4s600 { // Route: PUT /obp/v6.0.0/banks/BANK_ID/mandates/MANDATE_ID/provisions/PROVISION_ID - lazy val updateMandateProvision: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val updateMandateProvision: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "banks" / _ / "mandates" / _ / "provisions" / provisionId => EndpointHelpers.executeAndRespond(req) { implicit cc => val rawBody = cc.httpBody.getOrElse("") @@ -6674,7 +6679,7 @@ object Http4s600 { // Route: DELETE /obp/v6.0.0/banks/BANK_ID/mandates/MANDATE_ID/provisions/PROVISION_ID (204) - lazy val deleteMandateProvision: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val deleteMandateProvision: Http4sRoute = Http4sRoute { case req @ DELETE -> `prefixPath` / "banks" / _ / "mandates" / _ / "provisions" / provisionId => EndpointHelpers.executeAndRespond(req) { implicit cc => for { @@ -6695,7 +6700,7 @@ object Http4s600 { // Deferring index construction to first request (post object-init) lets every // registration land before the snapshot is taken. lazy val allRoutesWithMiddleware: HttpRoutes[IO] = - ResourceDocMiddleware.apply(resourceDocs)(IdempotencyMiddleware(allRoutes)) + ResourceDocMiddleware.apply(orderedResourceDocs, routes => IdempotencyMiddleware(routes))(IdempotencyMiddleware(allRoutes)) // ─── path-rewriting bridge: /obp/v6.0.0/… → /obp/v5.1.0/… ───────────── // Targets v5.1.0; Http4s510 has its own working cascade down to v5.0.0 → v4.0.0 → … @@ -6703,7 +6708,7 @@ object Http4s600 { lazy val v600ToV510Bridge: HttpRoutes[IO] = Kleisli[HttpF, Request[IO], Response[IO]] { req => val rawPath = req.uri.path.renderString if (rawPath.startsWith("/obp/v6.0.0/") && - ResourceDocMatcher.findResourceDoc(req.method.name, req.uri.path, v6ResourceDocIndex).isEmpty) { + ResourceDocMatcher.selectByRoute(req, v6RouteIndex).isEmpty) { val rewritten = rawPath.replaceFirst("/obp/v6\\.0\\.0/", "/obp/v5.1.0/") val newUri = req.uri.withPath(Uri.Path.unsafeFromString(rewritten)) Http4s510.wrappedRoutesV510Services.run(req.withUri(newUri)) @@ -15641,8 +15646,8 @@ object Http4s600 { } } - private lazy val v6ResourceDocIndex: ResourceDocMatcher.ResourceDocIndex = - ResourceDocMatcher.buildIndex(resourceDocs) + private lazy val v6RouteIndex: ResourceDocMatcher.RouteIndex = + ResourceDocMatcher.buildRouteIndex(Implementations6_0_0.orderedResourceDocs) // `lazy val`, not `val`: other objects reference // `Http4s600.Implementations6_0_0` directly via getstatic. When either is loaded From 86977f443a1b33dab3871e1eaacf07af57bc9103 Mon Sep 17 00:00:00 2001 From: Hongwei Date: Wed, 7 Oct 2026 17:29:58 +0200 Subject: [PATCH 11/29] refactor: bind every v5.1.0 ResourceDoc to its route Declare each endpoint as an Http4sRoute and derive the route chain from one list, so the middleware selects the doc of the route that serves a request and runs only that route. The path-rewriting bridge asks the routes whether the version serves a path instead of matching the URL against doc templates. --- .../scala/code/api/v5_1_0/Http4s510.scala | 478 +++++++++--------- 1 file changed, 241 insertions(+), 237 deletions(-) diff --git a/obp-api/src/main/scala/code/api/v5_1_0/Http4s510.scala b/obp-api/src/main/scala/code/api/v5_1_0/Http4s510.scala index 0cda869526..fe7c78f878 100644 --- a/obp-api/src/main/scala/code/api/v5_1_0/Http4s510.scala +++ b/obp-api/src/main/scala/code/api/v5_1_0/Http4s510.scala @@ -158,7 +158,7 @@ object Http4s510 { // ─── root (GET /root and GET / — v5.1 override of every prior version) ── - val root: HttpRoutes[IO] = HttpRoutes.of[IO] { + val root: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` => EndpointHelpers.executeFuture(req) { Future.successful(JSONFactory510.getApiInfoJSON(ApiVersion.v5_1_0, versionStatus)) @@ -192,7 +192,7 @@ object Http4s510 { // ─── getMyConsentsByBank (GET /banks/BANK_ID/my/consents) — v5.1 override - val getMyConsentsByBank: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getMyConsentsByBank: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / bankIdStr / "my" / "consents" => EndpointHelpers.executeFuture(req) { implicit val cc: code.api.util.CallContext = req.callContext @@ -269,7 +269,7 @@ object Http4s510 { // ─── getAggregateMetrics (GET /management/aggregate-metrics) — v5.1 override - val getAggregateMetrics: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getAggregateMetrics: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "management" / "aggregate-metrics" => EndpointHelpers.executeFuture(req) { implicit val cc: code.api.util.CallContext = req.callContext @@ -338,7 +338,7 @@ object Http4s510 { // ─── getBanks — kept in v5.1.0 layer so metrics are attributed to this version ── - val getBanks: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getBanks: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" => EndpointHelpers.executeAndRespond(req) { implicit cc => for { @@ -361,7 +361,7 @@ object Http4s510 { // ─── ATM CRUD (createAtm/updateAtm/getAtms/getAtm/deleteAtm) — v5.1 overrides - val createAtm: HttpRoutes[IO] = HttpRoutes.of[IO] { + val createAtm: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "banks" / bankIdStr / "atms" => EndpointHelpers.executeFutureCreated(req) { implicit val cc: code.api.util.CallContext = req.callContext @@ -399,7 +399,7 @@ object Http4s510 { http4sPartialFunction = Some(createAtm) ) - val updateAtm: HttpRoutes[IO] = HttpRoutes.of[IO] { + val updateAtm: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "banks" / bankIdStr / "atms" / atmIdStr => EndpointHelpers.executeFutureCreated(req) { implicit val cc: code.api.util.CallContext = req.callContext @@ -521,7 +521,7 @@ object Http4s510 { } } - val getAtms: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getAtms: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / bankIdStr / "atms" => Implementations5_1_0.respondWithETag(req, { cc => implicit val ccImpl: code.api.util.CallContext = cc @@ -576,7 +576,7 @@ object Http4s510 { http4sPartialFunction = Some(getAtms) ) - val getAtm: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getAtm: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / bankIdStr / "atms" / atmIdStr => Implementations5_1_0.respondWithETag(req, { cc => implicit val ccImpl: code.api.util.CallContext = cc @@ -609,7 +609,7 @@ object Http4s510 { http4sPartialFunction = Some(getAtm) ) - val deleteAtm: HttpRoutes[IO] = HttpRoutes.of[IO] { + val deleteAtm: Http4sRoute = Http4sRoute { case req @ DELETE -> `prefixPath` / "banks" / bankIdStr / "atms" / atmIdStr => EndpointHelpers.withUserDelete(req) { (_, cc) => val bankId = BankId(bankIdStr) @@ -643,7 +643,7 @@ object Http4s510 { // ─── createConsumer / getConsumer / getConsumers — v5.1 overrides ────── - val createConsumer: HttpRoutes[IO] = HttpRoutes.of[IO] { + val createConsumer: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "management" / "consumers" => EndpointHelpers.executeFutureCreated(req) { implicit val cc: code.api.util.CallContext = req.callContext @@ -783,7 +783,7 @@ object Http4s510 { http4sPartialFunction = Some(createConsumer) ) - val getConsumer: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getConsumer: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "management" / "consumers" / consumerId => EndpointHelpers.executeFuture(req) { implicit val cc: code.api.util.CallContext = req.callContext @@ -811,7 +811,7 @@ object Http4s510 { http4sPartialFunction = Some(getConsumer) ) - val getConsumers: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getConsumers: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "management" / "consumers" => EndpointHelpers.executeFuture(req) { implicit val cc: code.api.util.CallContext = req.callContext @@ -847,7 +847,7 @@ object Http4s510 { // ─── getTransactionRequests (v5.1 — adds attributes filter) ───────────── - val getTransactionRequests: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getTransactionRequests: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / bankIdStr / "accounts" / accountIdStr / viewIdStr / "transaction-requests" => EndpointHelpers.executeFuture(req) { implicit val cc: code.api.util.CallContext = req.callContext @@ -915,7 +915,7 @@ object Http4s510 { // ─── getBankAccountsBalances (v5.1 — same shape as v4 but in v5.1) ───── - val getBankAccountsBalances: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getBankAccountsBalances: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / bankIdStr / "balances" => EndpointHelpers.executeFuture(req) { implicit val cc: code.api.util.CallContext = req.callContext @@ -945,7 +945,7 @@ object Http4s510 { // ─── getAllBankAccountBalances (v5.1 override returns BankAccountBalancesJsonV510) - val getAllBankAccountBalances: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getAllBankAccountBalances: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / bankIdStr / "accounts" / accountIdStr / "balances" => EndpointHelpers.executeFuture(req) { implicit val cc: code.api.util.CallContext = req.callContext @@ -982,7 +982,7 @@ object Http4s510 { // waitingForGodot, getApiTags, mtlsClientCertificateInfo // (plus log-cache×6, regulated-entities CRUD, getAllApiCollections) - val suggestedSessionTimeout: HttpRoutes[IO] = HttpRoutes.of[IO] { + val suggestedSessionTimeout: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "ui" / "suggested-session-timeout" => EndpointHelpers.executeFuture(req) { Future(APIUtil.getPropsAsIntValue("session_inactivity_timeout_in_seconds", 300)) @@ -1007,7 +1007,7 @@ object Http4s510 { http4sPartialFunction = Some(suggestedSessionTimeout) ) - val getOAuth2ServerWellKnown: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getOAuth2ServerWellKnown: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "well-known" => EndpointHelpers.executeFuture(req) { implicit val cc: code.api.util.CallContext = req.callContext @@ -1053,7 +1053,7 @@ object Http4s510 { http4sPartialFunction = Some(getOAuth2ServerWellKnown) ) - val regulatedEntities: HttpRoutes[IO] = HttpRoutes.of[IO] { + val regulatedEntities: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "regulated-entities" => EndpointHelpers.executeFuture(req) { implicit val cc: code.api.util.CallContext = req.callContext @@ -1079,7 +1079,7 @@ object Http4s510 { http4sPartialFunction = Some(regulatedEntities) ) - val getRegulatedEntityById: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getRegulatedEntityById: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "regulated-entities" / regulatedEntityId => EndpointHelpers.executeFuture(req) { implicit val cc: code.api.util.CallContext = req.callContext @@ -1103,7 +1103,7 @@ object Http4s510 { http4sPartialFunction = Some(getRegulatedEntityById) ) - val createRegulatedEntity: HttpRoutes[IO] = HttpRoutes.of[IO] { + val createRegulatedEntity: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "regulated-entities" => EndpointHelpers.executeFutureCreated(req) { implicit val cc: code.api.util.CallContext = req.callContext @@ -1152,7 +1152,7 @@ object Http4s510 { http4sPartialFunction = Some(createRegulatedEntity) ) - val deleteRegulatedEntity: HttpRoutes[IO] = HttpRoutes.of[IO] { + val deleteRegulatedEntity: Http4sRoute = Http4sRoute { case req @ DELETE -> `prefixPath` / "regulated-entities" / regulatedEntityId => EndpointHelpers.executeFuture(req) { implicit val cc: code.api.util.CallContext = req.callContext @@ -1188,7 +1188,7 @@ object Http4s510 { } yield logs } - val logCacheTraceEndpoint: HttpRoutes[IO] = HttpRoutes.of[IO] { + val logCacheTraceEndpoint: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "system" / "log-cache" / "trace" => logCacheHandler(req, code.api.cache.RedisLogger.LogLevel.TRACE) } @@ -1215,7 +1215,7 @@ object Http4s510 { http4sPartialFunction = Some(logCacheTraceEndpoint) ) - val logCacheDebugEndpoint: HttpRoutes[IO] = HttpRoutes.of[IO] { + val logCacheDebugEndpoint: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "system" / "log-cache" / "debug" => logCacheHandler(req, code.api.cache.RedisLogger.LogLevel.DEBUG) } @@ -1242,7 +1242,7 @@ object Http4s510 { http4sPartialFunction = Some(logCacheDebugEndpoint) ) - val logCacheInfoEndpoint: HttpRoutes[IO] = HttpRoutes.of[IO] { + val logCacheInfoEndpoint: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "system" / "log-cache" / "info" => logCacheHandler(req, code.api.cache.RedisLogger.LogLevel.INFO) } @@ -1269,7 +1269,7 @@ object Http4s510 { http4sPartialFunction = Some(logCacheInfoEndpoint) ) - val logCacheWarningEndpoint: HttpRoutes[IO] = HttpRoutes.of[IO] { + val logCacheWarningEndpoint: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "system" / "log-cache" / "warning" => logCacheHandler(req, code.api.cache.RedisLogger.LogLevel.WARNING) } @@ -1296,7 +1296,7 @@ object Http4s510 { http4sPartialFunction = Some(logCacheWarningEndpoint) ) - val logCacheErrorEndpoint: HttpRoutes[IO] = HttpRoutes.of[IO] { + val logCacheErrorEndpoint: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "system" / "log-cache" / "error" => logCacheHandler(req, code.api.cache.RedisLogger.LogLevel.ERROR) } @@ -1323,7 +1323,7 @@ object Http4s510 { http4sPartialFunction = Some(logCacheErrorEndpoint) ) - val logCacheAllEndpoint: HttpRoutes[IO] = HttpRoutes.of[IO] { + val logCacheAllEndpoint: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "system" / "log-cache" / "all" => logCacheHandler(req, code.api.cache.RedisLogger.LogLevel.ALL) } @@ -1350,7 +1350,7 @@ object Http4s510 { http4sPartialFunction = Some(logCacheAllEndpoint) ) - val waitingForGodot: HttpRoutes[IO] = HttpRoutes.of[IO] { + val waitingForGodot: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "waiting-for-godot" => EndpointHelpers.executeFuture(req) { val sleep = req.uri.query.params.get("sleep").getOrElse("0") @@ -1378,7 +1378,7 @@ object Http4s510 { http4sPartialFunction = Some(waitingForGodot) ) - val getAllApiCollections: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getAllApiCollections: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "management" / "api-collections" => EndpointHelpers.executeFuture(req) { implicit val cc: code.api.util.CallContext = req.callContext @@ -1406,7 +1406,7 @@ object Http4s510 { // ─── ATM attributes (5) ──────────────────────────────────────────────── - val createAtmAttribute: HttpRoutes[IO] = HttpRoutes.of[IO] { + val createAtmAttribute: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "banks" / bankIdStr / "atms" / atmIdStr / "attributes" => EndpointHelpers.executeFutureCreated(req) { implicit val cc: code.api.util.CallContext = req.callContext @@ -1446,7 +1446,7 @@ object Http4s510 { http4sPartialFunction = Some(createAtmAttribute) ) - val getAtmAttributes: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getAtmAttributes: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / bankIdStr / "atms" / atmIdStr / "attributes" => EndpointHelpers.executeFuture(req) { implicit val cc: code.api.util.CallContext = req.callContext @@ -1476,7 +1476,7 @@ object Http4s510 { http4sPartialFunction = Some(getAtmAttributes) ) - val getAtmAttribute: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getAtmAttribute: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / bankIdStr / "atms" / atmIdStr / "attributes" / atmAttributeId => EndpointHelpers.executeFuture(req) { implicit val cc: code.api.util.CallContext = req.callContext @@ -1506,7 +1506,7 @@ object Http4s510 { http4sPartialFunction = Some(getAtmAttribute) ) - val updateAtmAttribute: HttpRoutes[IO] = HttpRoutes.of[IO] { + val updateAtmAttribute: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "banks" / bankIdStr / "atms" / atmIdStr / "attributes" / atmAttributeId => EndpointHelpers.executeFuture(req) { implicit val cc: code.api.util.CallContext = req.callContext @@ -1547,7 +1547,7 @@ object Http4s510 { http4sPartialFunction = Some(updateAtmAttribute) ) - val deleteAtmAttribute: HttpRoutes[IO] = HttpRoutes.of[IO] { + val deleteAtmAttribute: Http4sRoute = Http4sRoute { case req @ DELETE -> `prefixPath` / "banks" / bankIdStr / "atms" / atmIdStr / "attributes" / atmAttributeId => EndpointHelpers.withUserAndBankDelete(req) { (_, _, cc) => val bankId = BankId(bankIdStr); val atmId = AtmId(atmIdStr) @@ -1580,7 +1580,7 @@ object Http4s510 { // ─── Agents (4) ───────────────────────────────────────────────────────── - val createAgent: HttpRoutes[IO] = HttpRoutes.of[IO] { + val createAgent: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "banks" / bankIdStr / "agents" => EndpointHelpers.executeFutureCreated(req) { implicit val cc: code.api.util.CallContext = req.callContext @@ -1616,7 +1616,7 @@ object Http4s510 { http4sPartialFunction = Some(createAgent) ) - val updateAgentStatus: HttpRoutes[IO] = HttpRoutes.of[IO] { + val updateAgentStatus: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "banks" / _ / "agents" / agentId => EndpointHelpers.executeFuture(req) { implicit val cc: code.api.util.CallContext = req.callContext @@ -1649,7 +1649,7 @@ object Http4s510 { http4sPartialFunction = Some(updateAgentStatus) ) - val getAgent: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getAgent: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / bankIdStr / "agents" / agentId => EndpointHelpers.executeFuture(req) { implicit val cc: code.api.util.CallContext = req.callContext @@ -1679,7 +1679,7 @@ object Http4s510 { http4sPartialFunction = Some(getAgent) ) - val getAgents: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getAgents: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / bankIdStr / "agents" => EndpointHelpers.executeFuture(req) { implicit val cc: code.api.util.CallContext = req.callContext @@ -1712,7 +1712,7 @@ object Http4s510 { // ─── Regulated entity attributes (5) ─────────────────────────────────── - val createRegulatedEntityAttribute: HttpRoutes[IO] = HttpRoutes.of[IO] { + val createRegulatedEntityAttribute: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "regulated-entities" / entityIdStr / "attributes" => EndpointHelpers.executeFutureCreated(req) { implicit val cc: code.api.util.CallContext = req.callContext @@ -1754,7 +1754,7 @@ object Http4s510 { http4sPartialFunction = Some(createRegulatedEntityAttribute) ) - val deleteRegulatedEntityAttribute: HttpRoutes[IO] = HttpRoutes.of[IO] { + val deleteRegulatedEntityAttribute: Http4sRoute = Http4sRoute { case req @ DELETE -> `prefixPath` / "regulated-entities" / entityIdStr / "attributes" / attributeId => EndpointHelpers.withUserDelete(req) { (_, cc) => for { @@ -1783,7 +1783,7 @@ object Http4s510 { http4sPartialFunction = Some(deleteRegulatedEntityAttribute) ) - val getRegulatedEntityAttributeById: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getRegulatedEntityAttributeById: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "regulated-entities" / entityIdStr / "attributes" / attributeId => EndpointHelpers.executeFuture(req) { implicit val cc: code.api.util.CallContext = req.callContext @@ -1813,7 +1813,7 @@ object Http4s510 { http4sPartialFunction = Some(getRegulatedEntityAttributeById) ) - val getAllRegulatedEntityAttributes: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getAllRegulatedEntityAttributes: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "regulated-entities" / entityIdStr / "attributes" => EndpointHelpers.executeFuture(req) { implicit val cc: code.api.util.CallContext = req.callContext @@ -1844,7 +1844,7 @@ object Http4s510 { http4sPartialFunction = Some(getAllRegulatedEntityAttributes) ) - val updateRegulatedEntityAttribute: HttpRoutes[IO] = HttpRoutes.of[IO] { + val updateRegulatedEntityAttribute: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "regulated-entities" / entityIdStr / "attributes" / attributeId => EndpointHelpers.executeFuture(req) { implicit val cc: code.api.util.CallContext = req.callContext @@ -1888,7 +1888,7 @@ object Http4s510 { // ─── mtls / api-collection / api-tags / metrics / webui-props (5) ───── - val mtlsClientCertificateInfo: HttpRoutes[IO] = HttpRoutes.of[IO] { + val mtlsClientCertificateInfo: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "my" / "mtls" / "certificate" / "current" => EndpointHelpers.executeFuture(req) { implicit val cc: code.api.util.CallContext = req.callContext @@ -1918,7 +1918,7 @@ object Http4s510 { http4sPartialFunction = Some(mtlsClientCertificateInfo) ) - val updateMyApiCollection: HttpRoutes[IO] = HttpRoutes.of[IO] { + val updateMyApiCollection: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "my" / "api-collections" / apiCollectionId => EndpointHelpers.executeFuture(req) { implicit val cc: code.api.util.CallContext = req.callContext @@ -1950,7 +1950,7 @@ object Http4s510 { http4sPartialFunction = Some(updateMyApiCollection) ) - val getApiTags: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getApiTags: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "tags" => EndpointHelpers.executeFuture(req) { Future.successful(code.api.v5_1_0.APITags(code.api.util.ApiTag.allDisplayTagNames.toList)) @@ -1977,7 +1977,7 @@ object Http4s510 { http4sPartialFunction = Some(getApiTags) ) - val getMetrics: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getMetrics: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "management" / "metrics" => EndpointHelpers.executeFuture(req) { implicit val cc: code.api.util.CallContext = req.callContext @@ -2089,7 +2089,7 @@ object Http4s510 { http4sPartialFunction = Some(getMetrics) ) - val getWebUiProps: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getWebUiProps: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "webui-props" => EndpointHelpers.executeFuture(req) { implicit val cc: code.api.util.CallContext = req.callContext @@ -2137,7 +2137,7 @@ object Http4s510 { // ─── Non-personal user attributes (3) ───────────────────────────────── - val createNonPersonalUserAttribute: HttpRoutes[IO] = HttpRoutes.of[IO] { + val createNonPersonalUserAttribute: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "users" / userId / "non-personal" / "attributes" => EndpointHelpers.executeFutureCreated(req) { implicit val cc: code.api.util.CallContext = req.callContext @@ -2176,7 +2176,7 @@ object Http4s510 { http4sPartialFunction = Some(createNonPersonalUserAttribute) ) - val deleteNonPersonalUserAttribute: HttpRoutes[IO] = HttpRoutes.of[IO] { + val deleteNonPersonalUserAttribute: Http4sRoute = Http4sRoute { case req @ DELETE -> `prefixPath` / "users" / userId / "non-personal" / "attributes" / userAttributeId => EndpointHelpers.withUserDelete(req) { (_, cc) => for { @@ -2200,7 +2200,7 @@ object Http4s510 { http4sPartialFunction = Some(deleteNonPersonalUserAttribute) ) - val getNonPersonalUserAttributes: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getNonPersonalUserAttributes: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "users" / userId / "non-personal" / "attributes" => EndpointHelpers.executeFuture(req) { implicit val cc: code.api.util.CallContext = req.callContext @@ -2226,7 +2226,7 @@ object Http4s510 { // ─── User / lock / sync (8) ─────────────────────────────────────────── - val syncExternalUser: HttpRoutes[IO] = HttpRoutes.of[IO] { + val syncExternalUser: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "users" / provider / providerId / "sync" => EndpointHelpers.executeFutureCreated(req) { implicit val cc: code.api.util.CallContext = req.callContext @@ -2258,7 +2258,7 @@ object Http4s510 { http4sPartialFunction = Some(syncExternalUser) ) - val getEntitlementsAndPermissions: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getEntitlementsAndPermissions: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "users" / userId / "entitlements-and-permissions" => EndpointHelpers.executeFuture(req) { implicit val cc: code.api.util.CallContext = req.callContext @@ -2290,7 +2290,7 @@ object Http4s510 { http4sPartialFunction = Some(getEntitlementsAndPermissions) ) - val getUserByProviderAndUsername: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getUserByProviderAndUsername: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "users" / "provider" / provider / "username" / username => EndpointHelpers.executeFuture(req) { implicit val cc: code.api.util.CallContext = req.callContext @@ -2345,7 +2345,7 @@ object Http4s510 { http4sPartialFunction = Some(getUserByProviderAndUsername) ) - val getUserLockStatus: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getUserLockStatus: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "users" / provider / username / "lock-status" => EndpointHelpers.executeFuture(req) { implicit val cc: code.api.util.CallContext = req.callContext @@ -2372,7 +2372,7 @@ object Http4s510 { http4sPartialFunction = Some(getUserLockStatus) ) - val unlockUserByProviderAndUsername: HttpRoutes[IO] = HttpRoutes.of[IO] { + val unlockUserByProviderAndUsername: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "users" / provider / username / "lock-status" => EndpointHelpers.executeFuture(req) { implicit val cc: code.api.util.CallContext = req.callContext @@ -2404,7 +2404,7 @@ object Http4s510 { http4sPartialFunction = Some(unlockUserByProviderAndUsername) ) - val lockUserByProviderAndUsername: HttpRoutes[IO] = HttpRoutes.of[IO] { + val lockUserByProviderAndUsername: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "users" / provider / username / "locks" => EndpointHelpers.executeFuture(req) { implicit val cc: code.api.util.CallContext = req.callContext @@ -2434,7 +2434,7 @@ object Http4s510 { http4sPartialFunction = Some(lockUserByProviderAndUsername) ) - val validateUserByUserId: HttpRoutes[IO] = HttpRoutes.of[IO] { + val validateUserByUserId: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "management" / "users" / userId => EndpointHelpers.executeFuture(req) { implicit val cc: code.api.util.CallContext = req.callContext @@ -2471,7 +2471,7 @@ object Http4s510 { http4sPartialFunction = Some(validateUserByUserId) ) - val getAccountAccessByUserId: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getAccountAccessByUserId: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "users" / userId / "account-access" => EndpointHelpers.executeFuture(req) { implicit val cc: code.api.util.CallContext = req.callContext @@ -2530,7 +2530,7 @@ object Http4s510 { } } - val getAccountsHeldByUserAtBank: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getAccountsHeldByUserAtBank: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "users" / userId / "banks" / bankIdStr / "accounts-held" => EndpointHelpers.executeFuture(req) { implicit val cc: code.api.util.CallContext = req.callContext @@ -2568,7 +2568,7 @@ object Http4s510 { http4sPartialFunction = Some(getAccountsHeldByUserAtBank) ) - val getAccountsHeldByUser: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getAccountsHeldByUser: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "users" / userId / "accounts-held" => EndpointHelpers.executeFuture(req) { implicit val cc: code.api.util.CallContext = req.callContext @@ -2607,7 +2607,7 @@ object Http4s510 { // ─── Customer helpers (2) ───────────────────────────────────────────── - val getCustomersForUserIdsOnly: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getCustomersForUserIdsOnly: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "users" / "current" / "customers" / "customer_ids" => EndpointHelpers.executeFuture(req) { implicit val cc: code.api.util.CallContext = req.callContext @@ -2637,7 +2637,7 @@ object Http4s510 { http4sPartialFunction = Some(getCustomersForUserIdsOnly) ) - val getCustomersByLegalName: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getCustomersByLegalName: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "banks" / bankIdStr / "customers" / "legal-name" => EndpointHelpers.executeFuture(req) { implicit val cc: code.api.util.CallContext = req.callContext @@ -2669,7 +2669,7 @@ object Http4s510 { // ─── System integrity (5) + currencies (1) ──────────────────────────── - val customViewNamesCheck: HttpRoutes[IO] = HttpRoutes.of[IO] { + val customViewNamesCheck: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "management" / "system" / "integrity" / "custom-view-names-check" => EndpointHelpers.executeFuture(req) { for { @@ -2697,7 +2697,7 @@ object Http4s510 { http4sPartialFunction = Some(customViewNamesCheck) ) - val systemViewNamesCheck: HttpRoutes[IO] = HttpRoutes.of[IO] { + val systemViewNamesCheck: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "management" / "system" / "integrity" / "system-view-names-check" => EndpointHelpers.executeFuture(req) { for { @@ -2725,7 +2725,7 @@ object Http4s510 { http4sPartialFunction = Some(systemViewNamesCheck) ) - val accountAccessUniqueIndexCheck: HttpRoutes[IO] = HttpRoutes.of[IO] { + val accountAccessUniqueIndexCheck: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "management" / "system" / "integrity" / "account-access-unique-index-1-check" => EndpointHelpers.executeFuture(req) { for { @@ -2755,7 +2755,7 @@ object Http4s510 { http4sPartialFunction = Some(accountAccessUniqueIndexCheck) ) - val accountCurrencyCheck: HttpRoutes[IO] = HttpRoutes.of[IO] { + val accountCurrencyCheck: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "management" / "system" / "integrity" / "banks" / bankIdStr / "account-currency-check" => EndpointHelpers.executeFuture(req) { implicit val cc: code.api.util.CallContext = req.callContext @@ -2786,7 +2786,7 @@ object Http4s510 { http4sPartialFunction = Some(accountCurrencyCheck) ) - val orphanedAccountCheck: HttpRoutes[IO] = HttpRoutes.of[IO] { + val orphanedAccountCheck: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "management" / "system" / "integrity" / "banks" / bankIdStr / "orphaned-account-check" => EndpointHelpers.executeFuture(req) { val bankId = BankId(bankIdStr) @@ -2821,7 +2821,7 @@ object Http4s510 { http4sPartialFunction = Some(orphanedAccountCheck) ) - val getCurrenciesAtBank: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getCurrenciesAtBank: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / bankIdStr / "currencies" => EndpointHelpers.executeFuture(req) { implicit val cc: code.api.util.CallContext = req.callContext @@ -2855,7 +2855,7 @@ object Http4s510 { // ─── Consumer mgmt PUTs (4) + getCallsLimit + createMyConsumer + // createConsumerDynamicRegistration (7 total) ─────────────────────── - val updateConsumerRedirectURL: HttpRoutes[IO] = HttpRoutes.of[IO] { + val updateConsumerRedirectURL: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "management" / "consumers" / consumerId / "consumer" / "redirect_url" => EndpointHelpers.executeFuture(req) { implicit val cc: code.api.util.CallContext = req.callContext @@ -2903,7 +2903,7 @@ object Http4s510 { http4sPartialFunction = Some(updateConsumerRedirectURL) ) - val updateConsumerLogoURL: HttpRoutes[IO] = HttpRoutes.of[IO] { + val updateConsumerLogoURL: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "management" / "consumers" / consumerId / "consumer" / "logo_url" => EndpointHelpers.executeFuture(req) { implicit val cc: code.api.util.CallContext = req.callContext @@ -2940,7 +2940,7 @@ object Http4s510 { http4sPartialFunction = Some(updateConsumerLogoURL) ) - val updateConsumerCertificate: HttpRoutes[IO] = HttpRoutes.of[IO] { + val updateConsumerCertificate: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "management" / "consumers" / consumerId / "consumer" / "certificate" => EndpointHelpers.executeFuture(req) { implicit val cc: code.api.util.CallContext = req.callContext @@ -2977,7 +2977,7 @@ object Http4s510 { http4sPartialFunction = Some(updateConsumerCertificate) ) - val updateConsumerName: HttpRoutes[IO] = HttpRoutes.of[IO] { + val updateConsumerName: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "management" / "consumers" / consumerId / "consumer" / "name" => EndpointHelpers.executeFuture(req) { implicit val cc: code.api.util.CallContext = req.callContext @@ -3014,7 +3014,7 @@ object Http4s510 { http4sPartialFunction = Some(updateConsumerName) ) - val getCallsLimit: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getCallsLimit: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "management" / "consumers" / consumerId / "consumer" / "rate-limits" => EndpointHelpers.executeFuture(req) { implicit val cc: code.api.util.CallContext = req.callContext @@ -3044,7 +3044,7 @@ object Http4s510 { http4sPartialFunction = Some(getCallsLimit) ) - val createMyConsumer: HttpRoutes[IO] = HttpRoutes.of[IO] { + val createMyConsumer: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "my" / "consumers" => EndpointHelpers.executeFutureCreated(req) { implicit val cc: code.api.util.CallContext = req.callContext @@ -3125,7 +3125,7 @@ object Http4s510 { NewStyle.function.tryons(PostJsonIsNotSigned, 400, Some(cc)) { throw t } } - val createConsumerDynamicRegistration: HttpRoutes[IO] = HttpRoutes.of[IO] { + val createConsumerDynamicRegistration: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "dynamic-registration" / "consumers" => EndpointHelpers.executeFutureCreated(req) { implicit val cc: code.api.util.CallContext = req.callContext @@ -3273,7 +3273,7 @@ object Http4s510 { // ─── View access (3) + transaction-request mgmt (2) ─────────────────── - val grantUserAccessToViewById: HttpRoutes[IO] = HttpRoutes.of[IO] { + val grantUserAccessToViewById: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "banks" / bankIdStr / "accounts" / accountIdStr / "views" / viewIdStr / "account-access" / "grant" => EndpointHelpers.executeFutureCreated(req) { implicit val cc: code.api.util.CallContext = req.callContext @@ -3352,7 +3352,7 @@ object Http4s510 { http4sPartialFunction = Some(grantUserAccessToViewById) ) - val revokeUserAccessToViewById: HttpRoutes[IO] = HttpRoutes.of[IO] { + val revokeUserAccessToViewById: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "banks" / bankIdStr / "accounts" / accountIdStr / "views" / viewIdStr / "account-access" / "revoke" => EndpointHelpers.executeFutureCreated(req) { implicit val cc: code.api.util.CallContext = req.callContext @@ -3399,7 +3399,7 @@ object Http4s510 { http4sPartialFunction = Some(revokeUserAccessToViewById) ) - val createUserWithAccountAccessById: HttpRoutes[IO] = HttpRoutes.of[IO] { + val createUserWithAccountAccessById: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "banks" / bankIdStr / "accounts" / accountIdStr / "views" / viewIdStr / "user-account-access" => EndpointHelpers.executeFutureCreated(req) { implicit val cc: code.api.util.CallContext = req.callContext @@ -3458,7 +3458,7 @@ object Http4s510 { http4sPartialFunction = Some(createUserWithAccountAccessById) ) - val getTransactionRequestById: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getTransactionRequestById: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "management" / "transaction-requests" / requestIdStr => EndpointHelpers.executeFuture(req) { implicit val cc: code.api.util.CallContext = req.callContext @@ -3488,7 +3488,7 @@ object Http4s510 { http4sPartialFunction = Some(getTransactionRequestById) ) - val updateTransactionRequestStatus: HttpRoutes[IO] = HttpRoutes.of[IO] { + val updateTransactionRequestStatus: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "management" / "transaction-requests" / requestIdStr => EndpointHelpers.executeFuture(req) { implicit val cc: code.api.util.CallContext = req.callContext @@ -3535,7 +3535,7 @@ object Http4s510 { // ─── View account/balance reads (3) ─────────────────────────────────── - val getCoreAccountByIdThroughView: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getCoreAccountByIdThroughView: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / bankIdStr / "accounts" / accountIdStr / "views" / viewIdStr => EndpointHelpers.executeFuture(req) { implicit val cc: code.api.util.CallContext = req.callContext @@ -3567,7 +3567,7 @@ object Http4s510 { http4sPartialFunction = Some(getCoreAccountByIdThroughView) ) - val getBankAccountBalances: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getBankAccountBalances: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / bankIdStr / "accounts" / accountIdStr / "views" / viewIdStr / "balances" => EndpointHelpers.executeFuture(req) { implicit val cc: code.api.util.CallContext = req.callContext @@ -3600,7 +3600,7 @@ object Http4s510 { http4sPartialFunction = Some(getBankAccountBalances) ) - val getBankAccountsBalancesThroughView: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getBankAccountsBalancesThroughView: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / bankIdStr / "views" / viewIdStr / "balances" => EndpointHelpers.executeFuture(req) { implicit val cc: code.api.util.CallContext = req.callContext @@ -3629,7 +3629,7 @@ object Http4s510 { // ─── Counterparty limits (4 simple) — getCounterpartyLimitStatus deferred (complex) - val createCounterpartyLimit: HttpRoutes[IO] = HttpRoutes.of[IO] { + val createCounterpartyLimit: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "banks" / bankIdStr / "accounts" / accountIdStr / "views" / viewIdStr / "counterparties" / counterpartyIdStr / "limits" => EndpointHelpers.executeFutureCreated(req) { implicit val cc: code.api.util.CallContext = req.callContext @@ -3687,7 +3687,7 @@ object Http4s510 { http4sPartialFunction = Some(createCounterpartyLimit) ) - val updateCounterpartyLimit: HttpRoutes[IO] = HttpRoutes.of[IO] { + val updateCounterpartyLimit: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "banks" / bankIdStr / "accounts" / accountIdStr / "views" / viewIdStr / "counterparties" / counterpartyIdStr / "limits" => EndpointHelpers.executeFuture(req) { implicit val cc: code.api.util.CallContext = req.callContext @@ -3729,7 +3729,7 @@ object Http4s510 { http4sPartialFunction = Some(updateCounterpartyLimit) ) - val getCounterpartyLimit: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getCounterpartyLimit: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / bankIdStr / "accounts" / accountIdStr / "views" / viewIdStr / "counterparties" / counterpartyIdStr / "limits" => EndpointHelpers.executeFuture(req) { implicit val cc: code.api.util.CallContext = req.callContext @@ -3755,7 +3755,7 @@ object Http4s510 { http4sPartialFunction = Some(getCounterpartyLimit) ) - val getCounterpartyLimitStatus: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getCounterpartyLimitStatus: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / bankIdStr / "accounts" / accountIdStr / "views" / viewIdStr / "counterparties" / counterpartyIdStr / "limit-status" => EndpointHelpers.executeFuture(req) { implicit val cc: code.api.util.CallContext = req.callContext @@ -3831,7 +3831,7 @@ object Http4s510 { http4sPartialFunction = Some(getCounterpartyLimitStatus) ) - val deleteCounterpartyLimit: HttpRoutes[IO] = HttpRoutes.of[IO] { + val deleteCounterpartyLimit: Http4sRoute = Http4sRoute { case req @ DELETE -> `prefixPath` / "banks" / bankIdStr / "accounts" / accountIdStr / "views" / viewIdStr / "counterparties" / counterpartyIdStr / "limits" => EndpointHelpers.withUserDelete(req) { (_, cc) => val bankId = BankId(bankIdStr); val accountId = AccountId(accountIdStr); val viewId = ViewId(viewIdStr); val counterpartyId = CounterpartyId(counterpartyIdStr) @@ -3858,7 +3858,7 @@ object Http4s510 { // ─── Custom view CRUD (4) ───────────────────────────────────────────── - val createCustomView: HttpRoutes[IO] = HttpRoutes.of[IO] { + val createCustomView: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "banks" / bankIdStr / "accounts" / accountIdStr / "views" / viewIdStr / "target-views" => EndpointHelpers.withViewCreated(req) { (user, account, view, cc) => val bankId = BankId(bankIdStr); val accountId = AccountId(accountIdStr); val viewId = ViewId(viewIdStr) @@ -3909,7 +3909,7 @@ object Http4s510 { http4sPartialFunction = Some(createCustomView) ) - val updateCustomView: HttpRoutes[IO] = HttpRoutes.of[IO] { + val updateCustomView: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "banks" / bankIdStr / "accounts" / accountIdStr / "views" / viewIdStr / "target-views" / targetViewIdStr => EndpointHelpers.withView(req) { (user, account, view, cc) => val bankId = BankId(bankIdStr); val accountId = AccountId(accountIdStr); val viewId = ViewId(viewIdStr); val targetViewId = ViewId(targetViewIdStr) @@ -3948,7 +3948,7 @@ object Http4s510 { http4sPartialFunction = Some(updateCustomView) ) - val getCustomView: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getCustomView: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / bankIdStr / "accounts" / accountIdStr / "views" / viewIdStr / "target-views" / targetViewIdStr => EndpointHelpers.withView(req) { (_, _, view, cc) => val bankId = BankId(bankIdStr); val accountId = AccountId(accountIdStr); val viewId = ViewId(viewIdStr); val targetViewId = ViewId(targetViewIdStr) @@ -4001,7 +4001,7 @@ object Http4s510 { http4sPartialFunction = Some(getCustomView) ) - val deleteCustomView: HttpRoutes[IO] = HttpRoutes.of[IO] { + val deleteCustomView: Http4sRoute = Http4sRoute { case req @ DELETE -> `prefixPath` / "banks" / bankIdStr / "accounts" / accountIdStr / "views" / viewIdStr / "target-views" / targetViewIdStr => EndpointHelpers.executeDelete(req) { cc => val bankId = BankId(bankIdStr); val accountId = AccountId(accountIdStr); val viewId = ViewId(viewIdStr); val targetViewId = ViewId(targetViewIdStr) @@ -4035,7 +4035,7 @@ object Http4s510 { // ─── Bank account balance CRUD (4) ──────────────────────────────────── - val createBankAccountBalance: HttpRoutes[IO] = HttpRoutes.of[IO] { + val createBankAccountBalance: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "banks" / bankIdStr / "accounts" / accountIdStr / "balances" => EndpointHelpers.executeFutureCreated(req) { implicit val cc: code.api.util.CallContext = req.callContext @@ -4071,7 +4071,7 @@ object Http4s510 { http4sPartialFunction = Some(createBankAccountBalance) ) - val getBankAccountBalanceById: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getBankAccountBalanceById: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "accounts" / _ / "balances" / balanceIdStr => EndpointHelpers.executeFuture(req) { implicit val cc: code.api.util.CallContext = req.callContext @@ -4099,7 +4099,7 @@ object Http4s510 { http4sPartialFunction = Some(getBankAccountBalanceById) ) - val updateBankAccountBalance: HttpRoutes[IO] = HttpRoutes.of[IO] { + val updateBankAccountBalance: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "banks" / bankIdStr / "accounts" / accountIdStr / "balances" / balanceIdStr => EndpointHelpers.executeFuture(req) { implicit val cc: code.api.util.CallContext = req.callContext @@ -4136,7 +4136,7 @@ object Http4s510 { http4sPartialFunction = Some(updateBankAccountBalance) ) - val deleteBankAccountBalance: HttpRoutes[IO] = HttpRoutes.of[IO] { + val deleteBankAccountBalance: Http4sRoute = Http4sRoute { case req @ DELETE -> `prefixPath` / "banks" / _ / "accounts" / _ / "balances" / balanceIdStr => EndpointHelpers.withUserDelete(req) { (_, cc) => val balanceId = BalanceId(balanceIdStr) @@ -4167,7 +4167,7 @@ object Http4s510 { // ─── System view permissions (2) ────────────────────────────────────── - val addSystemViewPermission: HttpRoutes[IO] = HttpRoutes.of[IO] { + val addSystemViewPermission: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "system-views" / viewIdStr / "permissions" => EndpointHelpers.executeFutureCreated(req) { implicit val cc: code.api.util.CallContext = req.callContext @@ -4198,7 +4198,7 @@ object Http4s510 { http4sPartialFunction = Some(addSystemViewPermission) ) - val deleteSystemViewPermission: HttpRoutes[IO] = HttpRoutes.of[IO] { + val deleteSystemViewPermission: Http4sRoute = Http4sRoute { case req @ DELETE -> `prefixPath` / "system-views" / viewIdStr / "permissions" / permissionName => EndpointHelpers.withUserDelete(req) { (_, cc) => val viewId = ViewId(viewIdStr) @@ -4224,7 +4224,7 @@ object Http4s510 { // ─── Consents family (12) ───────────────────────────────────────────── - val updateConsentStatusByConsent: HttpRoutes[IO] = HttpRoutes.of[IO] { + val updateConsentStatusByConsent: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "management" / "banks" / _ / "consents" / consentId => EndpointHelpers.executeFuture(req) { implicit val cc: code.api.util.CallContext = req.callContext @@ -4268,7 +4268,7 @@ object Http4s510 { http4sPartialFunction = Some(updateConsentStatusByConsent) ) - val updateConsentAccountAccessByConsentId: HttpRoutes[IO] = HttpRoutes.of[IO] { + val updateConsentAccountAccessByConsentId: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "management" / "banks" / _ / "consents" / consentId / "account-access" => EndpointHelpers.executeFuture(req) { implicit val cc: code.api.util.CallContext = req.callContext @@ -4324,7 +4324,7 @@ object Http4s510 { http4sPartialFunction = Some(updateConsentAccountAccessByConsentId) ) - val updateConsentUserIdByConsentId: HttpRoutes[IO] = HttpRoutes.of[IO] { + val updateConsentUserIdByConsentId: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "management" / "banks" / _ / "consents" / consentId / "created-by-user" => EndpointHelpers.executeFuture(req) { implicit val cc: code.api.util.CallContext = req.callContext @@ -4385,7 +4385,7 @@ object Http4s510 { // uses — with ChallengeType.OBP_CONSENT_CHALLENGE, which is status-agnostic and so works // on an AWAITINGAUTHORISATION consent. The OTP is delivered per the configured SCA method // (props suggested_default_sca_method: DUMMY answer "123" for dev, EMAIL/SMS for prod). - val authoriseUKConsentChallenge: HttpRoutes[IO] = HttpRoutes.of[IO] { + val authoriseUKConsentChallenge: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "banks" / _ / "consents" / consentId / "authorise" / "challenge" => EndpointHelpers.executeFuture(req) { implicit val cc: code.api.util.CallContext = req.callContext @@ -4459,7 +4459,7 @@ object Http4s510 { // access token + the challenge answer to verify SCA, bind the consent to that user, and // flip it to AUTHORISED — the missing "authorisation binding" step of the UK flow. // User-authenticated but role-free (the account holder is approving their own consent). - val authoriseUKConsent: HttpRoutes[IO] = HttpRoutes.of[IO] { + val authoriseUKConsent: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "banks" / bankIdStr / "consents" / consentId / "authorise" => EndpointHelpers.executeFuture(req) { implicit val cc: code.api.util.CallContext = req.callContext @@ -4608,7 +4608,7 @@ object Http4s510 { http4sPartialFunction = Some(authoriseUKConsent) ) - val getMyConsents: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getMyConsents: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "my" / "consents" => EndpointHelpers.executeFuture(req) { implicit val cc: code.api.util.CallContext = req.callContext @@ -4662,7 +4662,7 @@ object Http4s510 { http4sPartialFunction = Some(getMyConsents) ) - val getConsentsAtBank: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getConsentsAtBank: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "management" / "consents" / "banks" / bankIdStr => EndpointHelpers.executeFuture(req) { implicit val cc: code.api.util.CallContext = req.callContext @@ -4710,7 +4710,7 @@ object Http4s510 { http4sPartialFunction = Some(getConsentsAtBank) ) - val getConsents: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getConsents: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "management" / "consents" => EndpointHelpers.executeFuture(req) { implicit val cc: code.api.util.CallContext = req.callContext @@ -4762,7 +4762,7 @@ object Http4s510 { http4sPartialFunction = Some(getConsents) ) - val getConsentByConsentId: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getConsentByConsentId: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "user" / "current" / "consents" / consentId => EndpointHelpers.executeFuture(req) { implicit val cc: code.api.util.CallContext = req.callContext @@ -4801,7 +4801,7 @@ object Http4s510 { http4sPartialFunction = Some(getConsentByConsentId) ) - val getConsentByConsentIdViaConsumer: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getConsentByConsentIdViaConsumer: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "consumer" / "current" / "consents" / consentId => EndpointHelpers.executeFuture(req) { implicit val cc: code.api.util.CallContext = req.callContext @@ -4835,7 +4835,7 @@ object Http4s510 { http4sPartialFunction = Some(getConsentByConsentIdViaConsumer) ) - val revokeConsentAtBank: HttpRoutes[IO] = HttpRoutes.of[IO] { + val revokeConsentAtBank: Http4sRoute = Http4sRoute { case req @ DELETE -> `prefixPath` / "banks" / bankIdStr / "consents" / consentId => EndpointHelpers.executeFuture(req) { implicit val cc: code.api.util.CallContext = req.callContext @@ -4881,7 +4881,7 @@ object Http4s510 { http4sPartialFunction = Some(revokeConsentAtBank) ) - val selfRevokeConsent: HttpRoutes[IO] = HttpRoutes.of[IO] { + val selfRevokeConsent: Http4sRoute = Http4sRoute { case req @ DELETE -> `prefixPath` / "my" / "consent" / "current" => EndpointHelpers.executeFuture(req) { implicit val cc: code.api.util.CallContext = req.callContext @@ -4985,7 +4985,7 @@ object Http4s510 { // ─── createConsent (IMPLICIT alias) — handles SCA: EMAIL/SMS/IMPLICIT ── - val revokeMyConsent: HttpRoutes[IO] = HttpRoutes.of[IO] { + val revokeMyConsent: Http4sRoute = Http4sRoute { case req @ DELETE -> `prefixPath` / "my" / "consents" / consentId => EndpointHelpers.executeFuture(req) { implicit val cc: code.api.util.CallContext = req.callContext @@ -5028,7 +5028,7 @@ object Http4s510 { ) // Lift named this endpoint "createConsent"; test tag nameOf references still use that name. - val createConsent: HttpRoutes[IO] = HttpRoutes.of[IO] { + val createConsent: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "my" / "consents" / scaMethod if scaMethod == "EMAIL" || scaMethod == "SMS" || scaMethod == "IMPLICIT" => EndpointHelpers.executeFutureCreated(req) { @@ -5220,7 +5220,7 @@ object Http4s510 { // ─── createVRPConsentRequest ──────────────────────────────────────────── - val createVRPConsentRequest: HttpRoutes[IO] = HttpRoutes.of[IO] { + val createVRPConsentRequest: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "consumer" / "vrp-consent-requests" => EndpointHelpers.executeFutureCreated(req) { implicit val cc: code.api.util.CallContext = req.callContext @@ -5319,133 +5319,137 @@ object Http4s510 { http4sPartialFunction = Some(createVRPConsentRequest) ) - val allRoutes: HttpRoutes[IO] = - Kleisli[HttpF, Request[IO], Response[IO]] { req: Request[IO] => - root(req) - .orElse(getMyConsentsByBank(req)) - .orElse(getAggregateMetrics(req)) - .orElse(getBanks(req)) - .orElse(createAtm(req)) - .orElse(updateAtm(req)) - .orElse(getAtms(req)) - .orElse(getAtm(req)) - .orElse(deleteAtm(req)) - .orElse(createConsumer(req)) - .orElse(getConsumer(req)) - .orElse(getConsumers(req)) - .orElse(getTransactionRequests(req)) - .orElse(getBankAccountsBalances(req)) - .orElse(getAllBankAccountBalances(req)) - .orElse(suggestedSessionTimeout(req)) - .orElse(getOAuth2ServerWellKnown(req)) - .orElse(regulatedEntities(req)) - .orElse(getRegulatedEntityById(req)) - .orElse(createRegulatedEntity(req)) - .orElse(deleteRegulatedEntity(req)) - .orElse(logCacheTraceEndpoint(req)) - .orElse(logCacheDebugEndpoint(req)) - .orElse(logCacheInfoEndpoint(req)) - .orElse(logCacheWarningEndpoint(req)) - .orElse(logCacheErrorEndpoint(req)) - .orElse(logCacheAllEndpoint(req)) - .orElse(waitingForGodot(req)) - .orElse(getAllApiCollections(req)) - .orElse(createAtmAttribute(req)) - .orElse(getAtmAttributes(req)) - .orElse(getAtmAttribute(req)) - .orElse(updateAtmAttribute(req)) - .orElse(deleteAtmAttribute(req)) - .orElse(createAgent(req)) - .orElse(updateAgentStatus(req)) - .orElse(getAgent(req)) - .orElse(getAgents(req)) - .orElse(createRegulatedEntityAttribute(req)) - .orElse(deleteRegulatedEntityAttribute(req)) - .orElse(getRegulatedEntityAttributeById(req)) - .orElse(getAllRegulatedEntityAttributes(req)) - .orElse(updateRegulatedEntityAttribute(req)) - .orElse(mtlsClientCertificateInfo(req)) - .orElse(updateMyApiCollection(req)) - .orElse(getApiTags(req)) - .orElse(getMetrics(req)) - .orElse(getWebUiProps(req)) - .orElse(createNonPersonalUserAttribute(req)) - .orElse(deleteNonPersonalUserAttribute(req)) - .orElse(getNonPersonalUserAttributes(req)) - .orElse(syncExternalUser(req)) - .orElse(getEntitlementsAndPermissions(req)) - .orElse(getUserByProviderAndUsername(req)) - .orElse(getUserLockStatus(req)) - .orElse(unlockUserByProviderAndUsername(req)) - .orElse(lockUserByProviderAndUsername(req)) - .orElse(lockUserByProviderAndUsername(req)) - .orElse(validateUserByUserId(req)) - .orElse(getAccountAccessByUserId(req)) - .orElse(getAccountsHeldByUserAtBank(req)) - .orElse(getAccountsHeldByUser(req)) - .orElse(getCustomersForUserIdsOnly(req)) - .orElse(getCustomersByLegalName(req)) - .orElse(customViewNamesCheck(req)) - .orElse(systemViewNamesCheck(req)) - .orElse(accountAccessUniqueIndexCheck(req)) - .orElse(accountCurrencyCheck(req)) - .orElse(orphanedAccountCheck(req)) - .orElse(getCurrenciesAtBank(req)) - .orElse(updateConsumerRedirectURL(req)) - .orElse(updateConsumerLogoURL(req)) - .orElse(updateConsumerCertificate(req)) - .orElse(updateConsumerName(req)) - .orElse(getCallsLimit(req)) - .orElse(createMyConsumer(req)) - .orElse(createConsumerDynamicRegistration(req)) - .orElse(grantUserAccessToViewById(req)) - .orElse(revokeUserAccessToViewById(req)) - .orElse(createUserWithAccountAccessById(req)) - .orElse(getTransactionRequestById(req)) - .orElse(updateTransactionRequestStatus(req)) - .orElse(getCoreAccountByIdThroughView(req)) - .orElse(getBankAccountBalances(req)) - .orElse(getBankAccountsBalancesThroughView(req)) - .orElse(createCounterpartyLimit(req)) - .orElse(updateCounterpartyLimit(req)) - .orElse(getCounterpartyLimit(req)) - .orElse(getCounterpartyLimitStatus(req)) - .orElse(deleteCounterpartyLimit(req)) - .orElse(createCustomView(req)) - .orElse(updateCustomView(req)) - .orElse(getCustomView(req)) - .orElse(deleteCustomView(req)) - .orElse(createBankAccountBalance(req)) - .orElse(getBankAccountBalanceById(req)) - .orElse(updateBankAccountBalance(req)) - .orElse(deleteBankAccountBalance(req)) - .orElse(addSystemViewPermission(req)) - .orElse(deleteSystemViewPermission(req)) - .orElse(updateConsentStatusByConsent(req)) - .orElse(updateConsentAccountAccessByConsentId(req)) - .orElse(updateConsentUserIdByConsentId(req)) - .orElse(authoriseUKConsentChallenge(req)) - .orElse(authoriseUKConsent(req)) - .orElse(getMyConsents(req)) - .orElse(getConsentsAtBank(req)) - .orElse(getConsents(req)) - .orElse(getConsentByConsentId(req)) - .orElse(getConsentByConsentIdViaConsumer(req)) - .orElse(revokeConsentAtBank(req)) - .orElse(selfRevokeConsent(req)) - .orElse(revokeMyConsent(req)) - .orElse(createConsent(req)) - .orElse(createVRPConsentRequest(req)) - } + // The routes in the order they are tried. ResourceDocMiddleware selects the doc of the first + // route that serves a request, so it is given the docs in this same order. + lazy val routesInOrder: List[Http4sHandler] = List( + root, + getMyConsentsByBank, + getAggregateMetrics, + getBanks, + createAtm, + updateAtm, + getAtms, + getAtm, + deleteAtm, + createConsumer, + getConsumer, + getConsumers, + getTransactionRequests, + getBankAccountsBalances, + getAllBankAccountBalances, + suggestedSessionTimeout, + getOAuth2ServerWellKnown, + regulatedEntities, + getRegulatedEntityById, + createRegulatedEntity, + deleteRegulatedEntity, + logCacheTraceEndpoint, + logCacheDebugEndpoint, + logCacheInfoEndpoint, + logCacheWarningEndpoint, + logCacheErrorEndpoint, + logCacheAllEndpoint, + waitingForGodot, + getAllApiCollections, + createAtmAttribute, + getAtmAttributes, + getAtmAttribute, + updateAtmAttribute, + deleteAtmAttribute, + createAgent, + updateAgentStatus, + getAgent, + getAgents, + createRegulatedEntityAttribute, + deleteRegulatedEntityAttribute, + getRegulatedEntityAttributeById, + getAllRegulatedEntityAttributes, + updateRegulatedEntityAttribute, + mtlsClientCertificateInfo, + updateMyApiCollection, + getApiTags, + getMetrics, + getWebUiProps, + createNonPersonalUserAttribute, + deleteNonPersonalUserAttribute, + getNonPersonalUserAttributes, + syncExternalUser, + getEntitlementsAndPermissions, + getUserByProviderAndUsername, + getUserLockStatus, + unlockUserByProviderAndUsername, + lockUserByProviderAndUsername, + validateUserByUserId, + getAccountAccessByUserId, + getAccountsHeldByUserAtBank, + getAccountsHeldByUser, + getCustomersForUserIdsOnly, + getCustomersByLegalName, + customViewNamesCheck, + systemViewNamesCheck, + accountAccessUniqueIndexCheck, + accountCurrencyCheck, + orphanedAccountCheck, + getCurrenciesAtBank, + updateConsumerRedirectURL, + updateConsumerLogoURL, + updateConsumerCertificate, + updateConsumerName, + getCallsLimit, + createMyConsumer, + createConsumerDynamicRegistration, + grantUserAccessToViewById, + revokeUserAccessToViewById, + createUserWithAccountAccessById, + getTransactionRequestById, + updateTransactionRequestStatus, + getCoreAccountByIdThroughView, + getBankAccountBalances, + getBankAccountsBalancesThroughView, + createCounterpartyLimit, + updateCounterpartyLimit, + getCounterpartyLimit, + getCounterpartyLimitStatus, + deleteCounterpartyLimit, + createCustomView, + updateCustomView, + getCustomView, + deleteCustomView, + createBankAccountBalance, + getBankAccountBalanceById, + updateBankAccountBalance, + deleteBankAccountBalance, + addSystemViewPermission, + deleteSystemViewPermission, + updateConsentStatusByConsent, + updateConsentAccountAccessByConsentId, + updateConsentUserIdByConsentId, + authoriseUKConsentChallenge, + authoriseUKConsent, + getMyConsents, + getConsentsAtBank, + getConsents, + getConsentByConsentId, + getConsentByConsentIdViaConsumer, + revokeConsentAtBank, + selfRevokeConsent, + revokeMyConsent, + createConsent, + createVRPConsentRequest + ) + + lazy val orderedResourceDocs: ArrayBuffer[ResourceDoc] = ResourceDocMatcher.orderByRoutes(resourceDocs, routesInOrder) + + lazy val allRoutes: HttpRoutes[IO] = Http4sRoute.chain(routesInOrder) - val allRoutesWithMiddleware: HttpRoutes[IO] = - ResourceDocMiddleware.apply(resourceDocs)(IdempotencyMiddleware(allRoutes)) + lazy val allRoutesWithMiddleware: HttpRoutes[IO] = + ResourceDocMiddleware.apply(orderedResourceDocs, routes => IdempotencyMiddleware(routes))(IdempotencyMiddleware(allRoutes)) // ─── path-rewriting bridge: /obp/v5.1.0/… → /obp/v5.0.0/… ───────────── lazy val v510ToV500Bridge: HttpRoutes[IO] = Kleisli[HttpF, Request[IO], Response[IO]] { req => val rawPath = req.uri.path.renderString if (rawPath.startsWith("/obp/v5.1.0/") && - ResourceDocMatcher.findResourceDoc(req.method.name, req.uri.path, v5_1ResourceDocIndex).isEmpty) { + ResourceDocMatcher.selectByRoute(req, v5_1RouteIndex).isEmpty) { val rewritten = rawPath.replaceFirst("/obp/v5\\.1\\.0/", "/obp/v5.0.0/") val newUri = req.uri.withPath(Uri.Path.unsafeFromString(rewritten)) Http4s500.wrappedRoutesV500Services.run(req.withUri(newUri)) @@ -5456,8 +5460,8 @@ object Http4s510 { } } - private lazy val v5_1ResourceDocIndex: ResourceDocMatcher.ResourceDocIndex = - ResourceDocMatcher.buildIndex(resourceDocs) + private lazy val v5_1RouteIndex: ResourceDocMatcher.RouteIndex = + ResourceDocMatcher.buildRouteIndex(Implementations5_1_0.orderedResourceDocs) lazy val wrappedRoutesV510Services: HttpRoutes[IO] = Kleisli[HttpF, Request[IO], Response[IO]] { req => From b3379267bc0f174cd69d939c6bbf439f2ec4e8b3 Mon Sep 17 00:00:00 2001 From: Hongwei Date: Wed, 7 Oct 2026 17:29:58 +0200 Subject: [PATCH 12/29] refactor: bind every v5.0.0 ResourceDoc to its route Declare each endpoint as an Http4sRoute and derive the route chain from one list, so the middleware selects the doc of the route that serves a request and runs only that route. The path-rewriting bridge asks the routes whether the version serves a path instead of matching the URL against doc templates. --- .../scala/code/api/v5_0_0/Http4s500.scala | 177 +++++++++--------- 1 file changed, 91 insertions(+), 86 deletions(-) diff --git a/obp-api/src/main/scala/code/api/v5_0_0/Http4s500.scala b/obp-api/src/main/scala/code/api/v5_0_0/Http4s500.scala index 2283aa4606..63ea7c5320 100644 --- a/obp-api/src/main/scala/code/api/v5_0_0/Http4s500.scala +++ b/obp-api/src/main/scala/code/api/v5_0_0/Http4s500.scala @@ -40,7 +40,7 @@ import code.api.util.ApiTag._ import code.api.util.ErrorMessages import code.api.util.ErrorMessages._ import code.api.util.http4s.Http4sRequestAttributes.{EndpointHelpers, RequestOps} -import code.api.util.http4s.ResourceDocMiddleware +import code.api.util.http4s.{ResourceDocMatcher, ResourceDocMiddleware} import code.api.util.http4s.IdempotencyMiddleware import code.api.util.newstyle.ViewNewStyle import code.api.util.{APIUtil, ConsentJWT, ConsentView, Consent, CustomJsonFormats, JwtUtil, NewStyle, OBPBankId, SecureRandomUtil} @@ -131,7 +131,7 @@ object Http4s500 { http4sPartialFunction = Some(root) ) - val root: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val root: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "root" => val responseJson = convertAnyToJsonString( JSONFactory400.getApiInfoJSON(ApiVersion.v5_0_0, versionStatus) @@ -161,7 +161,7 @@ object Http4s500 { http4sPartialFunction = Some(getBanks) ) - val getBanks: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getBanks: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" => EndpointHelpers.executeAndRespond(req) { implicit cc => for { @@ -192,7 +192,7 @@ object Http4s500 { http4sPartialFunction = Some(getBank) ) - val getBank: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getBank: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / bankId => EndpointHelpers.withBank(req) { (bank, cc) => for { @@ -224,7 +224,7 @@ object Http4s500 { http4sPartialFunction = Some(getProducts) ) - val getProducts: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getProducts: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / bankId / "products" => EndpointHelpers.executeFuture(req) { val cc = req.callContext @@ -253,7 +253,7 @@ object Http4s500 { http4sPartialFunction = Some(getProduct) ) - val getProduct: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getProduct: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / bankId / "products" / productCode => EndpointHelpers.executeFuture(req) { val cc = req.callContext @@ -306,7 +306,7 @@ object Http4s500 { http4sPartialFunction = Some(createSystemView) ) - val createSystemView: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val createSystemView: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "system-views" => EndpointHelpers.executeFutureCreated(req) { implicit val cc = req.callContext @@ -356,7 +356,7 @@ object Http4s500 { http4sPartialFunction = Some(getSystemView) ) - val getSystemView: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getSystemView: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "system-views" / viewId => EndpointHelpers.executeFuture(req) { implicit val cc = req.callContext @@ -392,7 +392,7 @@ object Http4s500 { http4sPartialFunction = Some(updateSystemView) ) - val updateSystemView: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val updateSystemView: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "system-views" / viewId => EndpointHelpers.executeFuture(req) { implicit val cc = req.callContext @@ -435,7 +435,7 @@ object Http4s500 { http4sPartialFunction = Some(deleteSystemView) ) - val deleteSystemView: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val deleteSystemView: Http4sRoute = Http4sRoute { case req @ DELETE -> `prefixPath` / "system-views" / viewId => EndpointHelpers.executeFuture(req) { implicit val cc = req.callContext @@ -450,7 +450,7 @@ object Http4s500 { // v5 uses PostBankJson500 (id is Option[String], includes bank_routings). // Must live in own routes so the bridge cascade can't hijack down to v4/v2.2. - val createBank: HttpRoutes[IO] = HttpRoutes.of[IO] { + val createBank: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "banks" => EndpointHelpers.executeFutureCreated(req) { implicit val cc: code.api.util.CallContext = req.callContext @@ -541,7 +541,7 @@ object Http4s500 { // ─── updateBank (PUT /banks → 200) ────────────────────────────────────── - val updateBank: HttpRoutes[IO] = HttpRoutes.of[IO] { + val updateBank: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "banks" => EndpointHelpers.executeFuture(req) { implicit val cc: code.api.util.CallContext = req.callContext @@ -600,7 +600,7 @@ object Http4s500 { // Account doesn't exist yet — use NEW_ACCOUNT_ID so middleware's // validateAccount doesn't 404 the create. (See CLAUDE.md gotcha.) - val createAccount: HttpRoutes[IO] = HttpRoutes.of[IO] { + val createAccount: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "banks" / bankIdStr / "accounts" / accountIdStr => EndpointHelpers.executeFutureCreated(req) { implicit val cc: code.api.util.CallContext = req.callContext @@ -699,7 +699,7 @@ object Http4s500 { // ─── createUserAuthContext (POST /users/USER_ID/auth-context → 201) ───── - val createUserAuthContext: HttpRoutes[IO] = HttpRoutes.of[IO] { + val createUserAuthContext: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "users" / userId / "auth-context" => EndpointHelpers.executeFutureCreated(req) { implicit val cc: code.api.util.CallContext = req.callContext @@ -735,7 +735,7 @@ object Http4s500 { // ─── getUserAuthContexts (GET /users/USER_ID/auth-context → 200) ──────── - val getUserAuthContexts: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getUserAuthContexts: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "users" / userId / "auth-context" => EndpointHelpers.executeFuture(req) { implicit val cc: code.api.util.CallContext = req.callContext @@ -771,7 +771,7 @@ object Http4s500 { // SCA_METHOD is a literal in {"SMS", "EMAIL"} per ResourceDocMatcher; the // handler also validates inline. - val createUserAuthContextUpdateRequest: HttpRoutes[IO] = HttpRoutes.of[IO] { + val createUserAuthContextUpdateRequest: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "banks" / bankIdStr / "users" / "current" / "auth-context-updates" / scaMethod => EndpointHelpers.executeFutureCreated(req) { implicit val cc: code.api.util.CallContext = req.callContext @@ -818,7 +818,7 @@ object Http4s500 { // ─── answerUserAuthContextUpdateChallenge ───────────────────────────── // POST /banks/BANK_ID/users/current/auth-context-updates/AUTH_CONTEXT_UPDATE_ID/challenge → 200 - val answerUserAuthContextUpdateChallenge: HttpRoutes[IO] = HttpRoutes.of[IO] { + val answerUserAuthContextUpdateChallenge: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "banks" / bankIdStr / "users" / "current" / "auth-context-updates" / authContextUpdateId / "challenge" => EndpointHelpers.executeFuture(req) { implicit val cc: code.api.util.CallContext = req.callContext @@ -870,7 +870,7 @@ object Http4s500 { // AuthenticatedUserIsRequired, so middleware skips auth and we call // applicationAccess inline. - val createConsentRequest: HttpRoutes[IO] = HttpRoutes.of[IO] { + val createConsentRequest: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "consumer" / "consent-requests" => EndpointHelpers.executeFutureCreated(req) { implicit val cc: code.api.util.CallContext = req.callContext @@ -943,7 +943,7 @@ object Http4s500 { // ─── getConsentRequest (GET /consumer/consent-requests/CONSENT_REQUEST_ID → 200) ─── - val getConsentRequest: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getConsentRequest: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "consumer" / "consent-requests" / consentRequestId => EndpointHelpers.executeFuture(req) { implicit val cc: code.api.util.CallContext = req.callContext @@ -983,7 +983,7 @@ object Http4s500 { // ─── getConsentByConsentRequestId ──────────────────────────────────────── // GET /consumer/consent-requests/CONSENT_REQUEST_ID/consents → 200 - val getConsentByConsentRequestId: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getConsentByConsentRequestId: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "consumer" / "consent-requests" / consentRequestId / "consents" => EndpointHelpers.executeFuture(req) { implicit val cc: code.api.util.CallContext = req.callContext @@ -1088,7 +1088,7 @@ object Http4s500 { StrongCustomerAuthentication.SMS, consentScaPhoneNumber, None, challengeText, callContextOpt) } yield status - val createConsentByConsentRequestId: HttpRoutes[IO] = HttpRoutes.of[IO] { + val createConsentByConsentRequestId: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "consumer" / "consent-requests" / consentRequestId / scaMethod / "consents" if scaMethod == "EMAIL" || scaMethod == "SMS" || scaMethod == "IMPLICIT" => EndpointHelpers.executeFutureCreated(req) { @@ -1463,7 +1463,7 @@ object Http4s500 { // ─── headAtms (HEAD /banks/BANK_ID/atms → 200) ────────────────────────── - val headAtms: HttpRoutes[IO] = HttpRoutes.of[IO] { + val headAtms: Http4sRoute = Http4sRoute { case req @ Method.HEAD -> `prefixPath` / "banks" / _ / "atms" => EndpointHelpers.executeFuture(req) { implicit val cc: code.api.util.CallContext = req.callContext @@ -1491,7 +1491,7 @@ object Http4s500 { // ─── createCustomer (POST /banks/BANK_ID/customers → 201) — v5 override ── // v5 uses PostCustomerJsonV500 with extra fields (kyc_status default, etc.) - val createCustomer: HttpRoutes[IO] = HttpRoutes.of[IO] { + val createCustomer: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "banks" / bankIdStr / "customers" => EndpointHelpers.executeFutureCreated(req) { implicit val cc: code.api.util.CallContext = req.callContext @@ -1566,7 +1566,7 @@ object Http4s500 { // ─── getCustomerOverview (POST /banks/.../customers/customer-number-query/overview) ─ - val getCustomerOverview: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getCustomerOverview: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "banks" / bankIdStr / "customers" / "customer-number-query" / "overview" => EndpointHelpers.executeFuture(req) { implicit val cc: code.api.util.CallContext = req.callContext @@ -1609,7 +1609,7 @@ object Http4s500 { // ─── getCustomerOverviewFlat ──────────────────────────────────────────── - val getCustomerOverviewFlat: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getCustomerOverviewFlat: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "banks" / bankIdStr / "customers" / "customer-number-query" / "overview-flat" => EndpointHelpers.executeFuture(req) { implicit val cc: code.api.util.CallContext = req.callContext @@ -1652,7 +1652,7 @@ object Http4s500 { // ─── getMyCustomersAtAnyBank (GET /my/customers) ──────────────────────── - val getMyCustomersAtAnyBank: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getMyCustomersAtAnyBank: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "my" / "customers" => EndpointHelpers.withUser(req) { (user, cc) => for { @@ -1681,7 +1681,7 @@ object Http4s500 { // ─── getMyCustomersAtBank (GET /banks/BANK_ID/my/customers) ───────────── - val getMyCustomersAtBank: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getMyCustomersAtBank: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / bankIdStr / "my" / "customers" => EndpointHelpers.executeFuture(req) { implicit val cc: code.api.util.CallContext = req.callContext @@ -1715,7 +1715,7 @@ object Http4s500 { // ─── getCustomersAtOneBank (GET /banks/BANK_ID/customers) — override ──── - val getCustomersAtOneBank: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getCustomersAtOneBank: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / bankIdStr / "customers" => EndpointHelpers.executeFuture(req) { implicit val cc: code.api.util.CallContext = req.callContext @@ -1750,7 +1750,7 @@ object Http4s500 { // ─── getCustomersMinimalAtOneBank (GET /banks/BANK_ID/customers-minimal) ─ - val getCustomersMinimalAtOneBank: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getCustomersMinimalAtOneBank: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / bankIdStr / "customers-minimal" => EndpointHelpers.executeFuture(req) { implicit val cc: code.api.util.CallContext = req.callContext @@ -1785,7 +1785,7 @@ object Http4s500 { // ─── createProduct (PUT /banks/BANK_ID/products/PRODUCT_CODE → 201) ──── // v5 override of v3.1.0/v4.0.0 — uses PutProductJsonV500 (parent_product_code). - val createProduct: HttpRoutes[IO] = HttpRoutes.of[IO] { + val createProduct: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "banks" / bankIdStr / "products" / productCodeStr => EndpointHelpers.executeFutureCreated(req) { implicit val cc: code.api.util.CallContext = req.callContext @@ -1856,7 +1856,7 @@ object Http4s500 { // ─── addCardForBank (POST /management/banks/BANK_ID/cards → 201) ─────── - val addCardForBank: HttpRoutes[IO] = HttpRoutes.of[IO] { + val addCardForBank: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "management" / "banks" / bankIdStr / "cards" => EndpointHelpers.executeFutureCreated(req) { implicit val cc: code.api.util.CallContext = req.callContext @@ -1940,7 +1940,7 @@ object Http4s500 { // ─── getViewsForBankAccount (GET /banks/BANK_ID/accounts/ACCOUNT_ID/views) ─ - val getViewsForBankAccount: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getViewsForBankAccount: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / bankIdStr / "accounts" / accountIdStr / "views" => EndpointHelpers.withBankAccount(req) { (user, _, cc) => val bankId = BankId(bankIdStr) @@ -2000,7 +2000,7 @@ object Http4s500 { // ─── getMetricsAtBank (GET /management/metrics/banks/BANK_ID) ────────── - val getMetricsAtBank: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getMetricsAtBank: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "management" / "metrics" / "banks" / bankIdStr => EndpointHelpers.executeFuture(req) { implicit val cc: code.api.util.CallContext = req.callContext @@ -2085,7 +2085,7 @@ object Http4s500 { // ─── getSystemViewsIds (GET /system-views-ids) ────────────────────────── - val getSystemViewsIds: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getSystemViewsIds: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "system-views-ids" => EndpointHelpers.executeFuture(req) { implicit val cc: code.api.util.CallContext = req.callContext @@ -2116,7 +2116,7 @@ object Http4s500 { // ─── customer-account-link endpoints (6) ──────────────────────────────── - val createCustomerAccountLink: HttpRoutes[IO] = HttpRoutes.of[IO] { + val createCustomerAccountLink: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "banks" / bankIdStr / "customer-account-links" => EndpointHelpers.executeFutureCreated(req) { implicit val cc: code.api.util.CallContext = req.callContext @@ -2164,7 +2164,7 @@ object Http4s500 { http4sPartialFunction = Some(createCustomerAccountLink) ) - val getCustomerAccountLinksByCustomerId: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getCustomerAccountLinksByCustomerId: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / bankIdStr / "customers" / customerId / "customer-account-links" => EndpointHelpers.executeFuture(req) { implicit val cc: code.api.util.CallContext = req.callContext @@ -2199,7 +2199,7 @@ object Http4s500 { http4sPartialFunction = Some(getCustomerAccountLinksByCustomerId) ) - val getCustomerAccountLinksByBankIdAccountId: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getCustomerAccountLinksByBankIdAccountId: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / bankIdStr / "accounts" / accountIdStr / "customer-account-links" => EndpointHelpers.withBankAccount(req) { (_, _, cc) => for { @@ -2228,7 +2228,7 @@ object Http4s500 { http4sPartialFunction = Some(getCustomerAccountLinksByBankIdAccountId) ) - val getCustomerAccountLinkById: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getCustomerAccountLinkById: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / bankIdStr / "customer-account-links" / customerAccountLinkId => EndpointHelpers.withBank(req) { (_, cc) => for { @@ -2256,7 +2256,7 @@ object Http4s500 { http4sPartialFunction = Some(getCustomerAccountLinkById) ) - val updateCustomerAccountLinkById: HttpRoutes[IO] = HttpRoutes.of[IO] { + val updateCustomerAccountLinkById: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "banks" / bankIdStr / "customer-account-links" / customerAccountLinkId => EndpointHelpers.executeFuture(req) { implicit val cc: code.api.util.CallContext = req.callContext @@ -2292,7 +2292,7 @@ object Http4s500 { http4sPartialFunction = Some(updateCustomerAccountLinkById) ) - val deleteCustomerAccountLinkById: HttpRoutes[IO] = HttpRoutes.of[IO] { + val deleteCustomerAccountLinkById: Http4sRoute = Http4sRoute { case req @ DELETE -> `prefixPath` / "banks" / bankIdStr / "customer-account-links" / customerAccountLinkId => EndpointHelpers.withUserAndBankDelete(req) { (_, _, cc) => for { @@ -2323,7 +2323,7 @@ object Http4s500 { // ─── getAdapterInfo (GET /adapter) — v3.1.0 override ─────────────────── - val getAdapterInfo: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getAdapterInfo: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "adapter" => EndpointHelpers.executeFuture(req) { implicit val cc: code.api.util.CallContext = req.callContext @@ -2347,52 +2347,57 @@ object Http4s500 { http4sPartialFunction = Some(getAdapterInfo) ) - val allRoutes: HttpRoutes[IO] = - Kleisli[HttpF, Request[IO], Response[IO]] { req: Request[IO] => - root(req) - .orElse(getBanks(req)) - .orElse(getBank(req)) - .orElse(createBank(req)) - .orElse(updateBank(req)) - .orElse(createAccount(req)) - .orElse(getProducts(req)) - .orElse(getProduct(req)) - .orElse(createProduct(req)) - .orElse(addCardForBank(req)) - .orElse(getViewsForBankAccount(req)) - .orElse(createSystemView(req)) - .orElse(getSystemView(req)) - .orElse(updateSystemView(req)) - .orElse(deleteSystemView(req)) - .orElse(getSystemViewsIds(req)) - .orElse(createUserAuthContext(req)) - .orElse(getUserAuthContexts(req)) - .orElse(createUserAuthContextUpdateRequest(req)) - .orElse(answerUserAuthContextUpdateChallenge(req)) - .orElse(createConsentRequest(req)) - .orElse(getConsentRequest(req)) - .orElse(getConsentByConsentRequestId(req)) - .orElse(createConsentByConsentRequestId(req)) - .orElse(headAtms(req)) - .orElse(createCustomer(req)) - .orElse(getCustomerOverview(req)) - .orElse(getCustomerOverviewFlat(req)) - .orElse(getMyCustomersAtAnyBank(req)) - .orElse(getMyCustomersAtBank(req)) - .orElse(getCustomersAtOneBank(req)) - .orElse(getCustomersMinimalAtOneBank(req)) - .orElse(createCustomerAccountLink(req)) - .orElse(getCustomerAccountLinksByCustomerId(req)) - .orElse(getCustomerAccountLinksByBankIdAccountId(req)) - .orElse(getCustomerAccountLinkById(req)) - .orElse(updateCustomerAccountLinkById(req)) - .orElse(deleteCustomerAccountLinkById(req)) - .orElse(getMetricsAtBank(req)) - .orElse(getAdapterInfo(req)) - } + // The routes in the order they are tried. ResourceDocMiddleware selects the doc of the first + // route that serves a request, so it is given the docs in this same order. + lazy val routesInOrder: List[Http4sHandler] = List( + root, + getBanks, + getBank, + createBank, + updateBank, + createAccount, + getProducts, + getProduct, + createProduct, + addCardForBank, + getViewsForBankAccount, + createSystemView, + getSystemView, + updateSystemView, + deleteSystemView, + getSystemViewsIds, + createUserAuthContext, + getUserAuthContexts, + createUserAuthContextUpdateRequest, + answerUserAuthContextUpdateChallenge, + createConsentRequest, + getConsentRequest, + getConsentByConsentRequestId, + createConsentByConsentRequestId, + headAtms, + createCustomer, + getCustomerOverview, + getCustomerOverviewFlat, + getMyCustomersAtAnyBank, + getMyCustomersAtBank, + getCustomersAtOneBank, + getCustomersMinimalAtOneBank, + createCustomerAccountLink, + getCustomerAccountLinksByCustomerId, + getCustomerAccountLinksByBankIdAccountId, + getCustomerAccountLinkById, + updateCustomerAccountLinkById, + deleteCustomerAccountLinkById, + getMetricsAtBank, + getAdapterInfo + ) + + lazy val orderedResourceDocs: ArrayBuffer[ResourceDoc] = ResourceDocMatcher.orderByRoutes(resourceDocs, routesInOrder) + + lazy val allRoutes: HttpRoutes[IO] = Http4sRoute.chain(routesInOrder) - val allRoutesWithMiddleware: HttpRoutes[IO] = - ResourceDocMiddleware.apply(resourceDocs)(IdempotencyMiddleware(allRoutes)) + lazy val allRoutesWithMiddleware: HttpRoutes[IO] = + ResourceDocMiddleware.apply(orderedResourceDocs, routes => IdempotencyMiddleware(routes))(IdempotencyMiddleware(allRoutes)) // ─── path-rewriting bridge: /obp/v5.0.0/… → /obp/v4.0.0/… ───────────── // Cascades inherited (v1.2.1–v4.0.0) endpoints through the http4s versions From 0ffab86039faf0f4818c3113dc61d6c28df70ab2 Mon Sep 17 00:00:00 2001 From: Hongwei Date: Wed, 7 Oct 2026 17:29:58 +0200 Subject: [PATCH 13/29] refactor: bind every v4.0.0 ResourceDoc to its route Declare each endpoint as an Http4sRoute and derive the route chain from one list, so the middleware selects the doc of the route that serves a request and runs only that route. The path-rewriting bridge asks the routes whether the version serves a path instead of matching the URL against doc templates. The createTransactionRequestAccount doc now documents the ACCOUNT URL, as the Lift baseline does; a request for a type that has no doc of its own is validated under it, the first doc of the shared route. --- .../scala/code/api/v4_0_0/Http4s400.scala | 1075 +++++++++-------- 1 file changed, 541 insertions(+), 534 deletions(-) diff --git a/obp-api/src/main/scala/code/api/v4_0_0/Http4s400.scala b/obp-api/src/main/scala/code/api/v4_0_0/Http4s400.scala index e405fdb17c..7e3e4feef1 100644 --- a/obp-api/src/main/scala/code/api/v4_0_0/Http4s400.scala +++ b/obp-api/src/main/scala/code/api/v4_0_0/Http4s400.scala @@ -37,7 +37,7 @@ import code.api.v3_1_0.ConsentChallengeJsonV310 import code.consent.ConsentStatus import com.openbankproject.commons.model.enums.{AttributeCategory, AttributeType, UserInvitationPurpose} import code.api.util.ApiVersionUtils -import code.api.util.APIUtil.{EmptyBody, ResourceDoc, _} +import code.api.util.APIUtil.{Http4sHandler, Http4sRoute, EmptyBody, ResourceDoc, _} import code.api.util.ApiRole._ import code.api.util.ApiTag._ import code.api.util.ErrorMessages._ @@ -63,7 +63,7 @@ import code.views.Views import code.api.v1_4_0.JSONFactory1_4_0 import code.DynamicEndpoint.DynamicEndpointSwagger import code.api.util.http4s.Http4sRequestAttributes.{EndpointHelpers, RequestOps} -import code.api.util.http4s.ResourceDocMiddleware +import code.api.util.http4s.{ResourceDocMatcher, ResourceDocMiddleware} import code.api.util.http4s.IdempotencyMiddleware import code.api.util.{APIUtil, AttributeTypeDocs, CallContext, CustomJsonFormats, NewStyle} import code.api.v4_0_0.JSONFactory400._ @@ -245,7 +245,7 @@ object Http4s400 { // ─── getMapperDatabaseInfo ──────────────────────────────────────────────── - lazy val getMapperDatabaseInfo: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getMapperDatabaseInfo: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "database" / "info" => EndpointHelpers.withUser(req) { (user, cc) => for { @@ -275,7 +275,7 @@ object Http4s400 { // ─── getLogoutLink ──────────────────────────────────────────────────────── - lazy val getLogoutLink: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getLogoutLink: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "users" / "current" / "logout-link" => EndpointHelpers.withUser(req) { (_, _) => Future { @@ -306,7 +306,7 @@ object Http4s400 { // ─── getBanks ───────────────────────────────────────────────────────────── // v4.0.0 overrides v3.x getBanks — v4 uses createBanksJson which adds attributes. - lazy val getBanks: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getBanks: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" => EndpointHelpers.executeAndRespond(req) { cc => for { @@ -339,7 +339,7 @@ object Http4s400 { // ─── getBank ────────────────────────────────────────────────────────────── // v4.0.0 overrides v3.x getBank — v4 includes bank attributes. - lazy val getBank: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getBank: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ => EndpointHelpers.withBank(req) { (bank, cc) => for { @@ -370,7 +370,7 @@ object Http4s400 { // ─── ibanChecker (POST → 200) ───────────────────────────────────────────── - lazy val ibanChecker: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val ibanChecker: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "account" / "check" / "scheme" / "iban" => EndpointHelpers.executeFutureWithBody[IbanAddress, Any](req) { (ibanJson, cc) => for { @@ -400,7 +400,7 @@ object Http4s400 { // v4.0.0 overrides v3.1.0 — v4 takes additional api_version / api_name / bank_id fields // in the request body for finer-grained rate limiting. - lazy val callsLimit: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val callsLimit: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "management" / "consumers" / consumerIdStr / "consumer" / "call-limits" => EndpointHelpers.withUserAndBody[CallLimitPostJsonV400, Any](req) { (user, postJson, cc) => for { @@ -458,7 +458,7 @@ object Http4s400 { // Must live in Http4s400's own routes so the bridge cascade can't hijack POST /banks // down to Http4s220 (which has its own v2.2.0 createBank — different behavior). - lazy val createBank: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val createBank: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "banks" => EndpointHelpers.executeFutureCreated(req) { implicit val cc: CallContext = req.callContext @@ -537,7 +537,7 @@ object Http4s400 { // ─── root (GET) — v4 override of v3.1.0's /root ────────────────────────── - lazy val root: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val root: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` => EndpointHelpers.executeAndRespond(req) { _ => Future.successful(JSONFactory400.getApiInfoJSON( @@ -573,7 +573,7 @@ object Http4s400 { // ─── getAtms (GET) — v4 override; conditional auth via getAtmsIsPublic ─── - lazy val getAtms: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getAtms: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "atms" => EndpointHelpers.withBank(req) { (bank, cc) => val limit = req.uri.query.params.get("limit").map(Full(_)).getOrElse(net.liftweb.common.Empty) @@ -629,7 +629,7 @@ object Http4s400 { // ─── getAtm (GET) — v4 override; conditional auth ──────────────────────── - lazy val getAtm: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getAtm: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "atms" / atmIdStr => EndpointHelpers.withBank(req) { (bank, cc) => for { @@ -665,7 +665,7 @@ object Http4s400 { // ─── getProducts (GET) — v4 override; conditional auth ─────────────────── - lazy val getProducts: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getProducts: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / bankIdStr / "products" => EndpointHelpers.executeAndRespond(req) { cc => val params = req.uri.query.multiParams.toList.flatMap { @@ -715,7 +715,7 @@ object Http4s400 { // ─── getProduct (GET) — v4 override; loads attributes + fees ───────────── - lazy val getProduct: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getProduct: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / bankIdStr / "products" / productCodeStr => EndpointHelpers.executeAndRespond(req) { cc => for { @@ -765,7 +765,7 @@ object Http4s400 { // ─── createAtm (POST → 201) — v4 override ───────────────────────────────── - lazy val createAtm: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val createAtm: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "banks" / _ / "atms" => EndpointHelpers.executeFutureCreated(req) { implicit val cc: CallContext = req.callContext @@ -810,7 +810,7 @@ object Http4s400 { // ─── createProduct (PUT → 201) — v4 override ────────────────────────────── - lazy val createProduct: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val createProduct: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "banks" / bankIdStr / "products" / productCodeStr => EndpointHelpers.executeFutureCreated(req) { implicit val cc: CallContext = req.callContext @@ -887,7 +887,7 @@ object Http4s400 { // ─── createProductAttribute (POST → 201) — v4 override ──────────────────── - lazy val createProductAttribute: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val createProductAttribute: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "banks" / bankIdStr / "products" / productCodeStr / "attribute" => EndpointHelpers.withUserAndBodyCreated[ProductAttributeJsonV400, Any](req) { (user, postedData, cc) => for { @@ -946,7 +946,7 @@ object Http4s400 { // ─── updateProductAttribute (PUT → 200) — v4 override ───────────────────── - lazy val updateProductAttribute: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val updateProductAttribute: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "banks" / bankIdStr / "products" / productCodeStr / "attributes" / productAttributeIdStr => EndpointHelpers.withUserAndBody[ProductAttributeJsonV400, Any](req) { (user, postedData, cc) => for { @@ -990,7 +990,7 @@ object Http4s400 { // ─── getEntitlements (GET /users/USER_ID/entitlements) — v4 override ──── - lazy val getEntitlements: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getEntitlements: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "users" / userIdStr / "entitlements" => EndpointHelpers.withUser(req) { (_, cc) => for { @@ -1025,7 +1025,7 @@ object Http4s400 { // ─── getUserByUserId (GET /users/user_id/USER_ID) — v4 override ───────── - lazy val getUserByUserId: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getUserByUserId: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "users" / "user_id" / userIdStr => EndpointHelpers.withUser(req) { (_, cc) => for { @@ -1066,7 +1066,7 @@ object Http4s400 { // ─── getUserByUsername (GET /users/username/USERNAME) — v4 override ───── - lazy val getUserByUsername: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getUserByUsername: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "users" / "username" / username => EndpointHelpers.withUser(req) { (_, cc) => for { @@ -1102,7 +1102,7 @@ object Http4s400 { // ─── getUsersByEmail (GET /users/email/EMAIL/terminator) — v4 override ── - lazy val getUsersByEmail: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getUsersByEmail: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "users" / "email" / email / "terminator" => EndpointHelpers.withUser(req) { (_, _) => for { @@ -1132,7 +1132,7 @@ object Http4s400 { // ─── getUsers (GET /users) — v4 override ───────────────────────────────── - lazy val getUsers: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getUsers: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "users" => EndpointHelpers.withUser(req) { (_, cc) => val httpParams = req.headers.headers.toList.map(h => @@ -1173,7 +1173,7 @@ object Http4s400 { // ─── getCustomersByAttributes (GET /banks/BANK_ID/customers) — v4 override - lazy val getCustomersByAttributes: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getCustomersByAttributes: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "customers" => EndpointHelpers.withUserAndBank(req) { (_, bank, cc) => val params = req.uri.query.multiParams.map { case (k, vs) => k -> vs.toList } @@ -1218,7 +1218,7 @@ object Http4s400 { // ─── createCustomer (POST /banks/BANK_ID/customers → 201) — v4 override ── - lazy val createCustomer: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val createCustomer: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "banks" / _ / "customers" => EndpointHelpers.withUserAndBankAndBodyCreated[code.api.v3_1_0.PostCustomerJsonV310, Any](req) { (_, bank, postedData, cc) => for { @@ -1276,7 +1276,7 @@ object Http4s400 { // ─── getBankAccountsBalancesForCurrentUser (GET /banks/BANK_ID/balances) — v4 - lazy val getBankAccountsBalancesForCurrentUser: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getBankAccountsBalancesForCurrentUser: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "balances" => EndpointHelpers.withUserAndBank(req) { (user, bank, cc) => for { @@ -1303,7 +1303,7 @@ object Http4s400 { // ─── getCoreAccountById (GET /my/banks/BANK_ID/accounts/ACCOUNT_ID/account) - lazy val getCoreAccountById: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getCoreAccountById: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "my" / "banks" / bankIdStr / "accounts" / accountIdStr / "account" => EndpointHelpers.withUser(req) { (user, cc) => for { @@ -1352,7 +1352,7 @@ object Http4s400 { // ─── getPrivateAccountByIdFull (GET /banks/BANK_ID/.../VIEW_ID/account) ── - lazy val getPrivateAccountByIdFull: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getPrivateAccountByIdFull: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / bankIdStr / "accounts" / _ / _ / "account" => EndpointHelpers.withView(req) { (user, account, view, cc) => for { @@ -1408,7 +1408,7 @@ object Http4s400 { // ─── getPrivateAccountsAtOneBank (GET /banks/BANK_ID/accounts) — v4 override - lazy val getPrivateAccountsAtOneBank: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getPrivateAccountsAtOneBank: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "accounts" => EndpointHelpers.withUserAndBank(req) { (user, bank, cc) => val params: Map[String, String] = req.uri.query.params @@ -1462,7 +1462,7 @@ object Http4s400 { // ─── createUserCustomerLinks (POST → 201) — v4 override ───────────────── - lazy val createUserCustomerLinks: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val createUserCustomerLinks: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "banks" / bankIdStr / "user_customer_links" => EndpointHelpers.withUserAndBankAndBodyCreated[code.api.v2_0_0.CreateUserCustomerLinkJson, Any](req) { (_, bank, postedData, cc) => for { @@ -1528,7 +1528,7 @@ object Http4s400 { // ─── getSystemDynamicEntities ───────────────────────────────────────────── - lazy val getSystemDynamicEntities: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getSystemDynamicEntities: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "management" / "system-dynamic-entities" => EndpointHelpers.withUser(req) { (user, cc) => for { @@ -1569,7 +1569,7 @@ object Http4s400 { // ─── getBankLevelDynamicEntities ────────────────────────────────────────── - lazy val getBankLevelDynamicEntities: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getBankLevelDynamicEntities: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "management" / "banks" / _ / "dynamic-entities" => EndpointHelpers.withUserAndBank(req) { (user, bank, cc) => for { @@ -1610,7 +1610,7 @@ object Http4s400 { // ─── getMyDynamicEntities ───────────────────────────────────────────────── - lazy val getMyDynamicEntities: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getMyDynamicEntities: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "my" / "dynamic-entities" => EndpointHelpers.withUser(req) { (user, cc) => for { @@ -1734,7 +1734,7 @@ object Http4s400 { // ─── createSystemDynamicEntity ──────────────────────────────────────────── - lazy val createSystemDynamicEntity: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val createSystemDynamicEntity: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "management" / "system-dynamic-entities" => EndpointHelpers.executeFutureCreated(req) { implicit val cc: CallContext = req.callContext @@ -1771,7 +1771,7 @@ object Http4s400 { // ─── createBankLevelDynamicEntity ───────────────────────────────────────── - lazy val createBankLevelDynamicEntity: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val createBankLevelDynamicEntity: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "management" / "banks" / _ / "dynamic-entities" => EndpointHelpers.executeFutureCreated(req) { implicit val cc: CallContext = req.callContext @@ -1807,7 +1807,7 @@ object Http4s400 { // ─── updateSystemDynamicEntity ──────────────────────────────────────────── - lazy val updateSystemDynamicEntity: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val updateSystemDynamicEntity: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "management" / "system-dynamic-entities" / dynamicEntityId => EndpointHelpers.executeAndRespond(req) { cc => val rawBody = cc.httpBody.getOrElse("") @@ -1845,7 +1845,7 @@ object Http4s400 { // ─── updateBankLevelDynamicEntity ───────────────────────────────────────── - lazy val updateBankLevelDynamicEntity: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val updateBankLevelDynamicEntity: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "management" / "banks" / bankIdStr / "dynamic-entities" / dynamicEntityId => EndpointHelpers.withUserAndBank(req) { (_, bank, cc) => val rawBody = cc.httpBody.getOrElse("") @@ -1881,7 +1881,7 @@ object Http4s400 { // ─── deleteSystemDynamicEntity (200) ───────────────────────────────────── - lazy val deleteSystemDynamicEntity: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val deleteSystemDynamicEntity: Http4sRoute = Http4sRoute { case req @ DELETE -> `prefixPath` / "management" / "system-dynamic-entities" / dynamicEntityId => EndpointHelpers.withUser(req) { (_, cc) => for { @@ -1918,7 +1918,7 @@ object Http4s400 { // ─── deleteBankLevelDynamicEntity (200) ────────────────────────────────── - lazy val deleteBankLevelDynamicEntity: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val deleteBankLevelDynamicEntity: Http4sRoute = Http4sRoute { case req @ DELETE -> `prefixPath` / "management" / "banks" / _ / "dynamic-entities" / dynamicEntityId => EndpointHelpers.withUserAndBank(req) { (_, bank, cc) => deleteDynamicEntityImpl(Some(bank.bankId.value), dynamicEntityId, cc).map(_ => JObject(Nil)) @@ -1953,7 +1953,7 @@ object Http4s400 { // ─── updateMyDynamicEntity ──────────────────────────────────────────────── - lazy val updateMyDynamicEntity: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val updateMyDynamicEntity: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "my" / "dynamic-entities" / dynamicEntityId => EndpointHelpers.withUser(req) { (user, cc) => val rawBody = cc.httpBody.getOrElse("") @@ -2008,7 +2008,7 @@ object Http4s400 { // ─── deleteMyDynamicEntity (200) ───────────────────────────────────────── - lazy val deleteMyDynamicEntity: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val deleteMyDynamicEntity: Http4sRoute = Http4sRoute { case req @ DELETE -> `prefixPath` / "my" / "dynamic-entities" / dynamicEntityId => EndpointHelpers.withUser(req) { (user, cc) => for { @@ -2126,7 +2126,7 @@ object Http4s400 { // ─── createDynamicEndpoint (POST → 201) ────────────────────────────────── - lazy val createDynamicEndpoint: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val createDynamicEndpoint: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "management" / "dynamic-endpoints" => EndpointHelpers.executeFutureCreated(req) { implicit val cc: CallContext = req.callContext @@ -2173,7 +2173,7 @@ object Http4s400 { // ─── createBankLevelDynamicEndpoint (POST → 201) ───────────────────────── - lazy val createBankLevelDynamicEndpoint: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val createBankLevelDynamicEndpoint: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "management" / "banks" / _ / "dynamic-endpoints" => EndpointHelpers.executeFutureCreated(req) { implicit val cc: CallContext = req.callContext @@ -2222,7 +2222,7 @@ object Http4s400 { // ─── updateDynamicEndpointHost (PUT → 201) ─────────────────────────────── - lazy val updateDynamicEndpointHost: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val updateDynamicEndpointHost: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "management" / "dynamic-endpoints" / dynamicEndpointId / "host" => EndpointHelpers.executeFutureCreated(req) { implicit val cc: CallContext = req.callContext @@ -2259,7 +2259,7 @@ object Http4s400 { // ─── updateBankLevelDynamicEndpointHost (PUT → 201) ────────────────────── - lazy val updateBankLevelDynamicEndpointHost: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val updateBankLevelDynamicEndpointHost: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "management" / "banks" / _ / "dynamic-endpoints" / dynamicEndpointId / "host" => EndpointHelpers.executeFutureCreated(req) { implicit val cc: CallContext = req.callContext @@ -2298,7 +2298,7 @@ object Http4s400 { // ─── getDynamicEndpoint (GET → 200) ────────────────────────────────────── - lazy val getDynamicEndpoint: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getDynamicEndpoint: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "management" / "dynamic-endpoints" / dynamicEndpointId => EndpointHelpers.executeAndRespond(req) { cc => getDynamicEndpointImpl(None, dynamicEndpointId, cc) @@ -2333,7 +2333,7 @@ object Http4s400 { // ─── getDynamicEndpoints (GET → 200) ───────────────────────────────────── - lazy val getDynamicEndpoints: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getDynamicEndpoints: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "management" / "dynamic-endpoints" => EndpointHelpers.executeAndRespond(req) { cc => getDynamicEndpointsImpl(None, cc) @@ -2369,7 +2369,7 @@ object Http4s400 { // ─── getBankLevelDynamicEndpoint (GET → 200) ───────────────────────────── - lazy val getBankLevelDynamicEndpoint: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getBankLevelDynamicEndpoint: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "management" / "banks" / _ / "dynamic-endpoints" / dynamicEndpointId => EndpointHelpers.withUserAndBank(req) { (_, bank, cc) => getDynamicEndpointImpl(Some(bank.bankId.value), dynamicEndpointId, cc) @@ -2401,7 +2401,7 @@ object Http4s400 { // ─── getBankLevelDynamicEndpoints (GET → 200) ──────────────────────────── - lazy val getBankLevelDynamicEndpoints: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getBankLevelDynamicEndpoints: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "management" / "banks" / _ / "dynamic-endpoints" => EndpointHelpers.withUserAndBank(req) { (_, bank, cc) => getDynamicEndpointsImpl(Some(bank.bankId.value), cc) @@ -2438,7 +2438,7 @@ object Http4s400 { // ─── deleteDynamicEndpoint (DELETE → 204) ──────────────────────────────── - lazy val deleteDynamicEndpoint: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val deleteDynamicEndpoint: Http4sRoute = Http4sRoute { case req @ DELETE -> `prefixPath` / "management" / "dynamic-endpoints" / dynamicEndpointId => EndpointHelpers.withUserDelete(req) { (_, cc) => NewStyle.function.deleteDynamicEndpoint(None, dynamicEndpointId, Some(cc)) @@ -2466,7 +2466,7 @@ object Http4s400 { // ─── deleteBankLevelDynamicEndpoint (DELETE → 204) ─────────────────────── - lazy val deleteBankLevelDynamicEndpoint: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val deleteBankLevelDynamicEndpoint: Http4sRoute = Http4sRoute { case req @ DELETE -> `prefixPath` / "management" / "banks" / _ / "dynamic-endpoints" / dynamicEndpointId => EndpointHelpers.withUserAndBankDelete(req) { (_, bank, cc) => NewStyle.function.deleteDynamicEndpoint(Some(bank.bankId.value), dynamicEndpointId, Some(cc)) @@ -2495,7 +2495,7 @@ object Http4s400 { // ─── getMyDynamicEndpoints (GET → 200) ─────────────────────────────────── - lazy val getMyDynamicEndpoints: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getMyDynamicEndpoints: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "my" / "dynamic-endpoints" => EndpointHelpers.withUser(req) { (user, cc) => for { @@ -2535,7 +2535,7 @@ object Http4s400 { // ─── deleteMyDynamicEndpoint (DELETE → 204) ────────────────────────────── - lazy val deleteMyDynamicEndpoint: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val deleteMyDynamicEndpoint: Http4sRoute = Http4sRoute { case req @ DELETE -> `prefixPath` / "my" / "dynamic-endpoints" / dynamicEndpointId => EndpointHelpers.withUserDelete(req) { (user, cc) => for { @@ -2564,7 +2564,7 @@ object Http4s400 { // ─── getProductAttribute (v4 override of Http4s310 — Lift declared role mismatch fixed) ─ - lazy val getProductAttribute: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getProductAttribute: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / bankIdStr / "products" / _ / "attributes" / productAttributeIdStr => EndpointHelpers.withUser(req) { (user, cc) => for { @@ -2600,7 +2600,7 @@ object Http4s400 { // ─── getScopes (GET /consumers/CONSUMER_ID/scopes) — v4 override of Http4s300 ─ - lazy val getScopes: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getScopes: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "consumers" / uuidOfConsumer / "scopes" => EndpointHelpers.withUser(req) { (user, cc) => for { @@ -2642,7 +2642,7 @@ object Http4s400 { // ─── addScope (POST /consumers/CONSUMER_ID/scopes → 201) — v4 override ──── - lazy val addScope: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val addScope: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "consumers" / consumerId / "scopes" => EndpointHelpers.withUserAndBodyCreated[code.api.v3_0_0.CreateScopeJson, Any](req) { (user, postedData, cc) => for { @@ -2700,7 +2700,7 @@ object Http4s400 { // ─── getConsents (GET /banks/BANK_ID/my/consents) — v4 override of Http4s310 ─ - lazy val getConsents: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getConsents: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "my" / "consents" => EndpointHelpers.withUserAndBank(req) { (user, bank, _) => val params = req.uri.query.params @@ -2749,7 +2749,7 @@ object Http4s400 { // ─── updateAccountLabel (POST /banks/BANK_ID/accounts/ACCOUNT_ID → 200) — v4 override of Http4s121 ─ - lazy val updateAccountLabel: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val updateAccountLabel: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "banks" / bankIdStr / "accounts" / accountIdStr => EndpointHelpers.withUserAndBody[UpdateAccountJsonV400, Any](req) { (user, postedData, cc) => for { @@ -2804,7 +2804,7 @@ object Http4s400 { // ─── getExplicitCounterpartiesForAccount (GET .../counterparties) — v4 override ─ - lazy val getExplicitCounterpartiesForAccount: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getExplicitCounterpartiesForAccount: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "accounts" / _ / _ / "counterparties" => EndpointHelpers.withView(req) { (user, account, view, cc) => for { @@ -2844,7 +2844,7 @@ object Http4s400 { // ─── getExplicitCounterpartyById (GET .../counterparties/COUNTERPARTY_ID) — v4 override ─ - lazy val getExplicitCounterpartyById: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getExplicitCounterpartyById: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "accounts" / _ / _ / "counterparties" / counterpartyIdStr => EndpointHelpers.withView(req) { (_, account, view, cc) => for { @@ -2879,7 +2879,7 @@ object Http4s400 { // ─── createExplicitCounterparty (POST .../counterparties → 201) — v4 override ─ - lazy val createExplicitCounterparty: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val createExplicitCounterparty: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "banks" / bankIdStr / "accounts" / accountIdStr / _ / "counterparties" => EndpointHelpers.withViewCreated(req) { (user, account, view, cc) => val bodyStr = cc.httpBody.getOrElse("") @@ -2991,7 +2991,7 @@ object Http4s400 { // ModeratedFirehoseAccountsJsonV400 (with `accounts`/`product_code` etc.) instead // of v3.0.0's ModeratedCoreAccountsJsonV300 shape that FirehoseTest can't parse. - lazy val getFirehoseAccountsAtOneBank: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getFirehoseAccountsAtOneBank: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / bankIdStr / "firehose" / "accounts" / "views" / viewIdStr => EndpointHelpers.withUser(req) { (user, cc) => val roles = ApiRoleObj.canUseAccountFirehose :: canUseAccountFirehoseAtAnyBank :: Nil @@ -3121,7 +3121,7 @@ object Http4s400 { } } - lazy val createTransactionRequest: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val createTransactionRequest: Http4sRoute = Http4sRoute { // GRANT_VIEW_ID in the ResourceDoc URL → middleware skips view validation. // Lift's v4 endpoint does no view-access check upfront; it lets // `checkAuthorisationToCreateTransactionRequest` inside the connector decide @@ -3182,7 +3182,7 @@ object Http4s400 { staticResourceDocs += ResourceDoc( implementedInApiVersion, "createTransactionRequestAccount", "POST", - "/banks/BANK_ID/accounts/ACCOUNT_ID/GRANT_VIEW_ID/transaction-request-types/TRANSACTION_REQUEST_TYPE/transaction-requests", + "/banks/BANK_ID/accounts/ACCOUNT_ID/GRANT_VIEW_ID/transaction-request-types/ACCOUNT/transaction-requests", "Create Transaction Request (ACCOUNT)", s"""When using ACCOUNT, the payee is set in the request body. | @@ -3219,8 +3219,9 @@ object Http4s400 { // createTransactionRequestAccount) all share the same body — call // `LocalMappedConnectorInternal.createTransactionRequest`. The already-migrated // `createTransactionRequest` http4s route uses a wildcard segment, so adding a - // ResourceDoc per type (with the literal type segment — recognised in - // `literalAllCapsSegments`) is enough; no new `lazy val` needed. + // ResourceDoc per type (with the literal type segment) is enough; no new `lazy val` needed. + // The docs share the one route, so a request for a type that has no doc of its own is validated + // under the first of them, `createTransactionRequestAccount`. private def initBatch9AliasResourceDocs(): Unit = { staticResourceDocs += ResourceDoc( implementedInApiVersion, "createTransactionRequestAccountOtp", "POST", @@ -3486,7 +3487,7 @@ object Http4s400 { // and the Lift body calls the connector with empty BankId/AccountId. Add as its own // route + ResourceDoc. The connector reads the user via SS.user, so prime SS with // the user only; bank/account/view are connector-resolved from card details. - lazy val createTransactionRequestCard: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val createTransactionRequestCard: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "transaction-request-types" / "CARD" / "transaction-requests" => implicit val cc: CallContext = req.callContext EndpointHelpers.executeFutureCreated(req) { @@ -3549,7 +3550,7 @@ object Http4s400 { // maker-checker separation, multi-challenge flow, NEXT_CHALLENGE_PENDING status, // FORWARDED status, and REJECT answer for SEPA refund reversal. - lazy val answerTransactionRequestChallenge: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val answerTransactionRequestChallenge: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "banks" / bankIdStr / "accounts" / accountIdStr / viewIdStr / "transaction-request-types" / transactionRequestTypeStr / "transaction-requests" / transReqIdStr / "challenge" => implicit val cc: CallContext = req.callContext val io = for { @@ -3790,28 +3791,28 @@ object Http4s400 { // Batch 1 — simple GETs (mostly mechanical) // ═══════════════════════════════════════════════════════════════════════════ - lazy val getCallContext: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getCallContext: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "development" / "call_context" => EndpointHelpers.withUser(req) { (_, cc) => Future(cc) } } - lazy val verifyRequestSignResponse: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val verifyRequestSignResponse: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "development" / "echo" / "jws-verified-request-jws-signed-response" => EndpointHelpers.withUser(req) { (_, cc) => Future(cc) } } - lazy val getCurrentUserId: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getCurrentUserId: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "users" / "current" / "user_id" => EndpointHelpers.withUser(req) { (user, _) => Future(JSONFactory400.createUserIdInfoJson(user)) } } - lazy val getScannedApiVersions: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getScannedApiVersions: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "api" / "versions" => EndpointHelpers.executeAndRespond(req) { _ => Future { @@ -3826,7 +3827,7 @@ object Http4s400 { } } - lazy val getMySpaces: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getMySpaces: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "my" / "spaces" => EndpointHelpers.withUser(req) { (user, cc) => for { @@ -3839,7 +3840,7 @@ object Http4s400 { } } - lazy val getBankAttributes: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getBankAttributes: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "attributes" => EndpointHelpers.withBank(req) { (bank, cc) => for { @@ -3848,7 +3849,7 @@ object Http4s400 { } } - lazy val getBankAttribute: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getBankAttribute: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "attributes" / bankAttributeId => EndpointHelpers.withBank(req) { (_, cc) => for { @@ -3857,7 +3858,7 @@ object Http4s400 { } } - lazy val getSystemLevelEndpointTags: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getSystemLevelEndpointTags: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "management" / "endpoints" / operationId / "tags" => EndpointHelpers.withUser(req) { (_, cc) => for { @@ -3868,7 +3869,7 @@ object Http4s400 { } } - lazy val getBankLevelEndpointTags: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getBankLevelEndpointTags: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "management" / "banks" / bankIdStr / "endpoints" / operationId / "tags" => EndpointHelpers.withUserAndBank(req) { (_, _, cc) => for { @@ -3895,28 +3896,28 @@ object Http4s400 { commonsData.toJson } - lazy val getEndpointMapping: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getEndpointMapping: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "management" / "endpoint-mappings" / endpointMappingId => EndpointHelpers.withUser(req) { (_, cc) => getEndpointMappingMethodHttp4s(None, endpointMappingId, cc) } } - lazy val getBankLevelEndpointMapping: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getBankLevelEndpointMapping: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "management" / "banks" / _ / "endpoint-mappings" / endpointMappingId => EndpointHelpers.withUserAndBank(req) { (_, bank, cc) => getEndpointMappingMethodHttp4s(Some(bank.bankId.value), endpointMappingId, cc) } } - lazy val getAllEndpointMappings: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getAllEndpointMappings: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "management" / "endpoint-mappings" => EndpointHelpers.withUser(req) { (_, cc) => getEndpointMappingsMethodHttp4s(None, cc) } } - lazy val getAllBankLevelEndpointMappings: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getAllBankLevelEndpointMappings: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "management" / "banks" / _ / "endpoint-mappings" => EndpointHelpers.withUserAndBank(req) { (_, bank, cc) => getEndpointMappingsMethodHttp4s(Some(bank.bankId.value), cc) @@ -3927,7 +3928,7 @@ object Http4s400 { // Batch 8 — Counterparty management endpoints // ═══════════════════════════════════════════════════════════════════════════ - lazy val getCounterpartiesForAnyAccount: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getCounterpartiesForAnyAccount: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "management" / "banks" / _ / "accounts" / _ / viewIdStr / "counterparties" => EndpointHelpers.withBankAccount(req) { (_, account, cc) => for { @@ -3947,7 +3948,7 @@ object Http4s400 { } } - lazy val getCounterpartyByIdForAnyAccount: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getCounterpartyByIdForAnyAccount: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "management" / "banks" / _ / "accounts" / _ / _ / "counterparties" / counterpartyIdStr => EndpointHelpers.withBankAccount(req) { (_, account, cc) => for { @@ -3959,7 +3960,7 @@ object Http4s400 { } } - lazy val getCounterpartyByNameForAnyAccount: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getCounterpartyByNameForAnyAccount: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "management" / "banks" / _ / "accounts" / _ / viewIdStr / "counterparty-names" / counterpartyName => EndpointHelpers.withBankAccount(req) { (_, account, cc) => for { @@ -4069,7 +4070,7 @@ object Http4s400 { // Most v4 DELETEs return 200 with body (Lift `HttpCode.\`200\``); use the // non-Delete helpers. - lazy val deleteExplicitCounterparty: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val deleteExplicitCounterparty: Http4sRoute = Http4sRoute { case req @ DELETE -> `prefixPath` / "banks" / bankIdStr / "accounts" / accountIdStr / viewIdStr / "counterparties" / counterpartyIdStr => EndpointHelpers.withView(req) { (_, account, view, cc) => for { @@ -4088,7 +4089,7 @@ object Http4s400 { } } - lazy val deleteCounterpartyForAnyAccount: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val deleteCounterpartyForAnyAccount: Http4sRoute = Http4sRoute { case req @ DELETE -> `prefixPath` / "management" / "banks" / bankIdStr / "accounts" / accountIdStr / _ / "counterparties" / counterpartyIdStr => EndpointHelpers.withBankAccount(req) { (_, account, cc) => for { @@ -4102,7 +4103,7 @@ object Http4s400 { } } - lazy val deleteTagForViewOnAccount: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val deleteTagForViewOnAccount: Http4sRoute = Http4sRoute { case req @ DELETE -> `prefixPath` / "banks" / _ / "accounts" / _ / viewIdStr / "metadata" / "tags" / tagId => EndpointHelpers.withView(req) { (_, _, view, cc) => for { @@ -4118,7 +4119,7 @@ object Http4s400 { } } - lazy val getTagsForViewOnAccount: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getTagsForViewOnAccount: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "accounts" / _ / viewIdStr / "metadata" / "tags" => EndpointHelpers.withView(req) { (_, account, view, cc) => for { @@ -4133,7 +4134,7 @@ object Http4s400 { } } - lazy val addTagForViewOnAccount: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val addTagForViewOnAccount: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "banks" / _ / "accounts" / _ / viewIdStr / "metadata" / "tags" => implicit val cc: CallContext = req.callContext EndpointHelpers.executeFutureCreated(req) { @@ -4162,7 +4163,7 @@ object Http4s400 { // ─── simpler GETs ──────────────────────────────────────────────────────── - lazy val getDoubleEntryTransaction: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getDoubleEntryTransaction: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / bankIdStr / "accounts" / accountIdStr / _ / "transactions" / transactionIdStr / "double-entry-transaction" => EndpointHelpers.withView(req) { (_, _, _, cc) => for { @@ -4174,7 +4175,7 @@ object Http4s400 { } } - lazy val getBalancingTransaction: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getBalancingTransaction: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "transactions" / transactionIdStr / "balancing-transaction" => EndpointHelpers.withUser(req) { (user, cc) => for { @@ -4186,7 +4187,7 @@ object Http4s400 { } } - lazy val getBankAccountBalancesForCurrentUser: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getBankAccountBalancesForCurrentUser: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "accounts" / accountIdStr / "balances" => EndpointHelpers.withUserAndBank(req) { (user, bank, cc) => for { @@ -4202,7 +4203,7 @@ object Http4s400 { } } - lazy val getAccountByAccountRouting: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getAccountByAccountRouting: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "management" / "accounts" / "account-routing-query" => EndpointHelpers.withUserAndBody[BankAccountRoutingJson, Any](req) { (user, postJson, cc) => for { @@ -4229,7 +4230,7 @@ object Http4s400 { } } - lazy val getAccountsByAccountRoutingRegex: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getAccountsByAccountRoutingRegex: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "management" / "accounts" / "account-routing-regex-query" => EndpointHelpers.withUserAndBody[BankAccountRoutingJson, Any](req) { (user, postJson, cc) => for { @@ -4265,7 +4266,7 @@ object Http4s400 { // ─── lockUser / resetPasswordUrl ───────────────────────────────────────── - lazy val lockUser: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val lockUser: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "users" / username / "locks" => EndpointHelpers.withUser(req) { (_, cc) => for { @@ -4280,7 +4281,7 @@ object Http4s400 { } } - lazy val resetPasswordUrl: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val resetPasswordUrl: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "management" / "user" / "reset-password-url" => EndpointHelpers.withUserAndBodyCreated[PostResetPasswordUrlJsonV400, ResetPasswordUrlJsonV400](req) { (_, postedData, cc) => @@ -4297,7 +4298,7 @@ object Http4s400 { // ─── settlement-accounts ──────────────────────────────────────────────── - lazy val getSettlementAccounts: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getSettlementAccounts: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / bankIdStr / "settlement-accounts" => EndpointHelpers.withUserAndBank(req) { (user, bank, cc) => for { @@ -4616,7 +4617,7 @@ object Http4s400 { // ─── Bank Attribute ───────────────────────────────────────────────────── - lazy val createBankAttribute: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val createBankAttribute: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "banks" / bankIdStr / "attribute" => EndpointHelpers.withUserAndBankAndBodyCreated[BankAttributeJsonV400, Any](req) { (_, _, postedData, cc) => for { @@ -4633,7 +4634,7 @@ object Http4s400 { } } - lazy val updateBankAttribute: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val updateBankAttribute: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "banks" / bankIdStr / "attributes" / bankAttributeId => EndpointHelpers.withUserAndBankAndBody[BankAttributeJsonV400, Any](req) { (user, _, postedData, cc) => for { @@ -4663,7 +4664,7 @@ object Http4s400 { .replaceAll("The Customer", s"The Customer($customerId)"), cc = Some(cc)) { customer.bankId == bankId } - lazy val createCustomerAttribute: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val createCustomerAttribute: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "banks" / bankIdStr / "customers" / customerIdStr / "attribute" => EndpointHelpers.withUserAndBodyCreated[CustomerAttributeJsonV400, Any](req) { (_, postedData, cc) => for { @@ -4682,7 +4683,7 @@ object Http4s400 { } } - lazy val updateCustomerAttribute: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val updateCustomerAttribute: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "banks" / bankIdStr / "customers" / customerIdStr / "attributes" / customerAttributeId => EndpointHelpers.withUserAndBody[CustomerAttributeJsonV400, Any](req) { (_, postedData, cc) => for { @@ -4704,7 +4705,7 @@ object Http4s400 { // ─── Transaction Attribute ─────────────────────────────────────────────── - lazy val createTransactionAttribute: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val createTransactionAttribute: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "banks" / bankIdStr / "accounts" / accountIdStr / "transactions" / transactionIdStr / "attribute" => EndpointHelpers.withUserAndBankAndBodyCreated[TransactionAttributeJsonV400, Any](req) { (_, bank, postedData, cc) => for { @@ -4723,7 +4724,7 @@ object Http4s400 { } } - lazy val updateTransactionAttribute: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val updateTransactionAttribute: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "banks" / bankIdStr / "accounts" / accountIdStr / "transactions" / transactionIdStr / "attributes" / transactionAttributeId => EndpointHelpers.withUserAndBankAndBody[TransactionAttributeJsonV400, Any](req) { (_, bank, postedData, cc) => for { @@ -4745,7 +4746,7 @@ object Http4s400 { // ─── Transaction Request Attribute ─────────────────────────────────────── - lazy val createTransactionRequestAttribute: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val createTransactionRequestAttribute: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "banks" / bankIdStr / "accounts" / _ / "transaction-requests" / transactionRequestIdStr / "attribute" => EndpointHelpers.withUserAndBankAndBodyCreated[TransactionRequestAttributeJsonV400, Any](req) { (_, bank, postedData, cc) => for { @@ -4764,7 +4765,7 @@ object Http4s400 { } } - lazy val updateTransactionRequestAttribute: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val updateTransactionRequestAttribute: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "banks" / bankIdStr / "accounts" / _ / "transaction-requests" / transactionRequestIdStr / "attributes" / transactionRequestAttributeId => EndpointHelpers.withUserAndBankAndBody[TransactionRequestAttributeJsonV400, Any](req) { (_, bank, postedData, cc) => for { @@ -4788,7 +4789,7 @@ object Http4s400 { // ─── Product Fee ───────────────────────────────────────────────────────── - lazy val createProductFee: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val createProductFee: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "banks" / bankIdStr / "products" / productCode / "fee" => EndpointHelpers.withUserAndBodyCreated[ProductFeeJsonV400, Any](req) { (_, postedData, cc) => for { @@ -4802,7 +4803,7 @@ object Http4s400 { } } - lazy val updateProductFee: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val updateProductFee: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "banks" / bankIdStr / "products" / productCode / "fees" / productFeeId => EndpointHelpers.withUserAndBodyCreated[ProductFeeJsonV400, Any](req) { (_, postedData, cc) => for { @@ -4819,7 +4820,7 @@ object Http4s400 { // ─── My Personal User Attribute ────────────────────────────────────────── - lazy val createMyPersonalUserAttribute: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val createMyPersonalUserAttribute: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "my" / "user" / "attributes" => EndpointHelpers.withUserAndBodyCreated[UserAttributeJsonV400, Any](req) { (user, postedData, cc) => for { @@ -4836,7 +4837,7 @@ object Http4s400 { } } - lazy val updateMyPersonalUserAttribute: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val updateMyPersonalUserAttribute: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "my" / "user" / "attributes" / userAttributeId => EndpointHelpers.withUserAndBody[UserAttributeJsonV400, Any](req) { (user, postedData, cc) => for { @@ -5131,7 +5132,7 @@ object Http4s400 { // Batch 11 — Account access, user invitations, consents, api collections // ═══════════════════════════════════════════════════════════════════════════ - lazy val getUserInvitationAnonymous: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getUserInvitationAnonymous: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "banks" / bankIdStr / "user-invitations" => EndpointHelpers.executeFutureWithBodyCreated[PostUserInvitationAnonymousJsonV400, Any](req) { (postedData, cc) => for { @@ -5150,7 +5151,7 @@ object Http4s400 { } } - lazy val grantUserAccessToView: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val grantUserAccessToView: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "banks" / bankIdStr / "accounts" / accountIdStr / "account-access" / "grant" => EndpointHelpers.withUserAndBodyCreated[PostAccountAccessJsonV400, Any](req) { (loggedInUser, postJson, cc) => val bankId = BankId(bankIdStr) @@ -5169,7 +5170,7 @@ object Http4s400 { } } - lazy val revokeUserAccessToView: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val revokeUserAccessToView: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "banks" / bankIdStr / "accounts" / accountIdStr / "account-access" / "revoke" => EndpointHelpers.withUserAndBodyCreated[PostAccountAccessJsonV400, Any](req) { (loggedInUser, postJson, cc) => val bankId = BankId(bankIdStr) @@ -5195,7 +5196,7 @@ object Http4s400 { } } - lazy val revokeGrantUserAccessToViews: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val revokeGrantUserAccessToViews: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "banks" / bankIdStr / "accounts" / accountIdStr / "account-access" => EndpointHelpers.withUserAndBodyCreated[PostRevokeGrantAccountAccessJsonV400, Any](req) { (loggedInUser, postJson, cc) => val bankId = BankId(bankIdStr) @@ -5222,7 +5223,7 @@ object Http4s400 { } } - lazy val createMyApiCollection: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val createMyApiCollection: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "my" / "api-collections" => EndpointHelpers.withUserAndBodyCreated[PostApiCollectionJson400, Any](req) { (user, postJson, cc) => for { @@ -5242,7 +5243,7 @@ object Http4s400 { } } - lazy val createMyApiCollectionEndpoint: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val createMyApiCollectionEndpoint: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "my" / "api-collections" / apiCollectionName / "api-collection-endpoints" => EndpointHelpers.withUserAndBodyCreated[PostApiCollectionEndpointJson400, Any](req) { (user, postJson, cc) => for { @@ -5269,7 +5270,7 @@ object Http4s400 { } } - lazy val createMyApiCollectionEndpointById: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val createMyApiCollectionEndpointById: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "my" / "api-collection-ids" / apiCollectionIdStr / "api-collection-endpoints" => EndpointHelpers.withUserAndBodyCreated[PostApiCollectionEndpointJson400, Any](req) { (_, postJson, cc) => for { @@ -5295,7 +5296,7 @@ object Http4s400 { } } - lazy val updateConsentStatus: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val updateConsentStatus: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "banks" / _ / "consents" / consentId => EndpointHelpers.withUserAndBankAndBody[PutConsentStatusJsonV400, Any](req) { (_, _, consentJson, cc) => for { @@ -5315,7 +5316,7 @@ object Http4s400 { } } - lazy val addConsentUser: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val addConsentUser: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "banks" / _ / "consents" / consentId / "user-update-request" => EndpointHelpers.withUserAndBankAndBody[PutConsentUserJsonV400, Any](req) { (_, _, putJson, cc) => for { @@ -5571,7 +5572,7 @@ object Http4s400 { } yield JSONFactory400.createDirectDebitJSON(directDebit) } - lazy val createDirectDebit: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val createDirectDebit: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "banks" / bankIdStr / "accounts" / accountIdStr / viewIdStr / "direct-debit" => EndpointHelpers.withViewCreated[DirectDebitJsonV400](req) { (_, _, view, cc) => implicit val ccx: CallContext = cc @@ -5592,7 +5593,7 @@ object Http4s400 { } } - lazy val createDirectDebitManagement: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val createDirectDebitManagement: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "management" / "banks" / bankIdStr / "accounts" / accountIdStr / "direct-debit" => EndpointHelpers.withUserAndBankAndBodyCreated[PostDirectDebitJsonV400, Any](req) { (_, _, postJson, cc) => directDebitImpl(bankIdStr, accountIdStr, postJson, cc) @@ -5626,7 +5627,7 @@ object Http4s400 { } yield JSONFactory400.createStandingOrderJSON(order) } - lazy val createStandingOrder: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val createStandingOrder: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "banks" / bankIdStr / "accounts" / accountIdStr / viewIdStr / "standing-order" => EndpointHelpers.withViewCreated[StandingOrderJsonV400](req) { (_, _, view, cc) => val bodyStr = cc.httpBody.getOrElse("") @@ -5646,14 +5647,14 @@ object Http4s400 { } } - lazy val createStandingOrderManagement: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val createStandingOrderManagement: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "management" / "banks" / bankIdStr / "accounts" / accountIdStr / "standing-order" => EndpointHelpers.withUserAndBankAndBodyCreated[PostStandingOrderJsonV400, Any](req) { (_, _, postJson, cc) => standingOrderImpl(bankIdStr, accountIdStr, postJson, cc) } } - lazy val createSystemAccountNotificationWebhook: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val createSystemAccountNotificationWebhook: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "web-hooks" / "account" / "notifications" / "on-create-transaction" => EndpointHelpers.withUserAndBodyCreated[AccountNotificationWebhookPostJson, Any](req) { (user, postJson, cc) => for { @@ -5676,7 +5677,7 @@ object Http4s400 { } } - lazy val createBankAccountNotificationWebhook: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val createBankAccountNotificationWebhook: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "banks" / bankIdStr / "web-hooks" / "account" / "notifications" / "on-create-transaction" => EndpointHelpers.withUserAndBankAndBodyCreated[AccountNotificationWebhookPostJson, Any](req) { (user, bank, postJson, cc) => for { @@ -5700,7 +5701,7 @@ object Http4s400 { } } - lazy val getFastFirehoseAccountsAtOneBank: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getFastFirehoseAccountsAtOneBank: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "management" / "banks" / bankIdStr / "fast-firehose" / "accounts" => EndpointHelpers.withUserAndBank(req) { (_, _, cc) => for { @@ -5920,7 +5921,7 @@ object Http4s400 { } yield JSONFactory400.createAttributeDefinitionJson(attributeDefinition) } - lazy val createOrUpdateCustomerAttributeAttributeDefinition: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val createOrUpdateCustomerAttributeAttributeDefinition: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "banks" / bankIdStr / "attribute-definitions" / "customer" => EndpointHelpers.withUserAndBankAndBodyCreated[AttributeDefinitionJsonV400, Any](req) { (_, _, postedData, cc) => createOrUpdateAttributeDefinitionImpl(bankIdStr, @@ -5928,7 +5929,7 @@ object Http4s400 { } } - lazy val createOrUpdateAccountAttributeDefinition: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val createOrUpdateAccountAttributeDefinition: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "banks" / bankIdStr / "attribute-definitions" / "account" => EndpointHelpers.withUserAndBankAndBodyCreated[AttributeDefinitionJsonV400, Any](req) { (_, _, postedData, cc) => createOrUpdateAttributeDefinitionImpl(bankIdStr, @@ -5936,7 +5937,7 @@ object Http4s400 { } } - lazy val createOrUpdateProductAttributeDefinition: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val createOrUpdateProductAttributeDefinition: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "banks" / bankIdStr / "attribute-definitions" / "product" => EndpointHelpers.withUserAndBankAndBodyCreated[AttributeDefinitionJsonV400, Any](req) { (_, _, postedData, cc) => createOrUpdateAttributeDefinitionImpl(bankIdStr, @@ -5944,7 +5945,7 @@ object Http4s400 { } } - lazy val createOrUpdateTransactionAttributeDefinition: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val createOrUpdateTransactionAttributeDefinition: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "banks" / bankIdStr / "attribute-definitions" / "transaction" => EndpointHelpers.withUserAndBankAndBodyCreated[AttributeDefinitionJsonV400, Any](req) { (_, _, postedData, cc) => createOrUpdateAttributeDefinitionImpl(bankIdStr, @@ -5952,7 +5953,7 @@ object Http4s400 { } } - lazy val createOrUpdateTransactionRequestAttributeDefinition: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val createOrUpdateTransactionRequestAttributeDefinition: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "banks" / bankIdStr / "attribute-definitions" / "transaction-request" => EndpointHelpers.withUserAndBankAndBodyCreated[AttributeDefinitionJsonV400, Any](req) { (_, _, postedData, cc) => createOrUpdateAttributeDefinitionImpl(bankIdStr, @@ -5960,7 +5961,7 @@ object Http4s400 { } } - lazy val createOrUpdateCardAttributeDefinition: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val createOrUpdateCardAttributeDefinition: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "banks" / bankIdStr / "attribute-definitions" / "card" => EndpointHelpers.withUserAndBankAndBodyCreated[AttributeDefinitionJsonV400, Any](req) { (_, _, postedData, cc) => createOrUpdateAttributeDefinitionImpl(bankIdStr, @@ -5968,7 +5969,7 @@ object Http4s400 { } } - lazy val createOrUpdateBankAttributeDefinition: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val createOrUpdateBankAttributeDefinition: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "banks" / bankIdStr / "attribute-definitions" / "bank" => EndpointHelpers.withUserAndBankAndBodyCreated[AttributeDefinitionJsonV400, Any](req) { (user, _, postedData, cc) => createOrUpdateAttributeDefinitionImpl(bankIdStr, @@ -6144,7 +6145,7 @@ object Http4s400 { // Batch 6 — ATM updates (PUT) and other simple PUTs // ═══════════════════════════════════════════════════════════════════════════ - lazy val updateAtmSupportedCurrencies: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val updateAtmSupportedCurrencies: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "banks" / _ / "atms" / atmIdStr / "supported-currencies" => EndpointHelpers.withUserAndBankAndBodyCreated[SupportedCurrenciesJson, Any](req) { (_, bank, postedData, cc) => for { @@ -6155,7 +6156,7 @@ object Http4s400 { } } - lazy val updateAtmSupportedLanguages: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val updateAtmSupportedLanguages: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "banks" / _ / "atms" / atmIdStr / "supported-languages" => EndpointHelpers.withUserAndBankAndBodyCreated[SupportedLanguagesJson, Any](req) { (_, bank, postedData, cc) => for { @@ -6166,7 +6167,7 @@ object Http4s400 { } } - lazy val updateAtmAccessibilityFeatures: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val updateAtmAccessibilityFeatures: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "banks" / _ / "atms" / atmIdStr / "accessibility-features" => EndpointHelpers.withUserAndBankAndBodyCreated[AccessibilityFeaturesJson, Any](req) { (_, bank, postedData, cc) => for { @@ -6177,7 +6178,7 @@ object Http4s400 { } } - lazy val updateAtmServices: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val updateAtmServices: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "banks" / _ / "atms" / atmIdStr / "services" => EndpointHelpers.withUserAndBankAndBodyCreated[AtmServicesJsonV400, Any](req) { (_, bank, postedData, cc) => for { @@ -6188,7 +6189,7 @@ object Http4s400 { } } - lazy val updateAtmNotes: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val updateAtmNotes: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "banks" / _ / "atms" / atmIdStr / "notes" => EndpointHelpers.withUserAndBankAndBodyCreated[AtmNotesJsonV400, Any](req) { (_, bank, postedData, cc) => for { @@ -6199,7 +6200,7 @@ object Http4s400 { } } - lazy val updateAtmLocationCategories: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val updateAtmLocationCategories: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "banks" / _ / "atms" / atmIdStr / "location-categories" => EndpointHelpers.withUserAndBankAndBodyCreated[AtmLocationCategoriesJsonV400, Any](req) { (_, bank, postedData, cc) => for { @@ -6210,7 +6211,7 @@ object Http4s400 { } } - lazy val updateAtm: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val updateAtm: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "banks" / _ / "atms" / atmIdStr => EndpointHelpers.withUserAndBankAndBodyCreated[AtmJsonV400, Any](req) { (_, bank, atmJsonV400, cc) => for { @@ -6345,7 +6346,7 @@ object Http4s400 { // Batch 5 — More simple endpoints // ═══════════════════════════════════════════════════════════════════════════ - lazy val getProductFee: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getProductFee: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "products" / _ / "fees" / productFeeId => EndpointHelpers.executeAndRespond(req) { cc => for { @@ -6354,7 +6355,7 @@ object Http4s400 { } } - lazy val getProductFees: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getProductFees: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / bankIdStr / "products" / productCodeStr / "fees" => EndpointHelpers.executeAndRespond(req) { cc => for { @@ -6364,7 +6365,7 @@ object Http4s400 { } } - lazy val getTransactionAttributes: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getTransactionAttributes: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "accounts" / _ / "transactions" / transactionIdStr / "attributes" => EndpointHelpers.withBankAccount(req) { (_, account, cc) => for { @@ -6376,7 +6377,7 @@ object Http4s400 { } } - lazy val getTransactionAttributeById: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getTransactionAttributeById: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "accounts" / _ / "transactions" / transactionIdStr / "attributes" / transactionAttributeId => EndpointHelpers.withBankAccount(req) { (_, account, cc) => for { @@ -6387,7 +6388,7 @@ object Http4s400 { } } - lazy val getTransactionRequestAttributes: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getTransactionRequestAttributes: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "accounts" / _ / "transaction-requests" / transactionRequestIdStr / "attributes" => EndpointHelpers.withBankAccount(req) { (_, account, cc) => for { @@ -6399,7 +6400,7 @@ object Http4s400 { } } - lazy val getTransactionRequestAttributeById: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getTransactionRequestAttributeById: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "accounts" / _ / "transaction-requests" / transactionRequestIdStr / "attributes" / transactionRequestAttributeId => EndpointHelpers.withBankAccount(req) { (_, _, cc) => for { @@ -6411,7 +6412,7 @@ object Http4s400 { } } - lazy val getTransactionRequestAttributeDefinition: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getTransactionRequestAttributeDefinition: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "attribute-definitions" / "transaction-request" => EndpointHelpers.withBank(req) { (_, cc) => getAttributeDefinitionImpl( @@ -6419,7 +6420,7 @@ object Http4s400 { } } - lazy val getTransactionRequest: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getTransactionRequest: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "accounts" / _ / _ / "transaction-requests" / transactionRequestIdStr => EndpointHelpers.withView(req) { (user, account, view, cc) => for { @@ -6429,7 +6430,7 @@ object Http4s400 { } } - lazy val getMyCorrelatedEntities: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getMyCorrelatedEntities: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "my" / "correlated-entities" => EndpointHelpers.withUser(req) { (user, cc) => for { @@ -6451,7 +6452,7 @@ object Http4s400 { } } - lazy val getCorrelatedUsersInfoByCustomerId: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getCorrelatedUsersInfoByCustomerId: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "customers" / customerIdStr / "correlated-users" => EndpointHelpers.withUserAndBank(req) { (_, _, cc) => for { @@ -6464,7 +6465,7 @@ object Http4s400 { } } - lazy val getAccountsMinimalByCustomerId: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getAccountsMinimalByCustomerId: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "customers" / customerIdStr / "accounts-minimal" => EndpointHelpers.withUser(req) { (user, cc) => for { @@ -6483,7 +6484,7 @@ object Http4s400 { } } - lazy val getCustomersByCustomerPhoneNumber: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getCustomersByCustomerPhoneNumber: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "banks" / _ / "search" / "customers" / "mobile-phone-number" => EndpointHelpers.withUserAndBankAndBody[PostCustomerPhoneNumberJsonV400, Any](req) { (_, bank, postedData, cc) => for { @@ -6493,7 +6494,7 @@ object Http4s400 { } } - lazy val getCustomersAtAnyBank: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getCustomersAtAnyBank: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "customers" => EndpointHelpers.withUser(req) { (_, cc) => val httpParams = req.headers.headers.toList.map(h => @@ -6506,7 +6507,7 @@ object Http4s400 { } } - lazy val getCustomersMinimalAtAnyBank: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getCustomersMinimalAtAnyBank: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "customers-minimal" => EndpointHelpers.withUser(req) { (_, cc) => for { @@ -6517,7 +6518,7 @@ object Http4s400 { } } - lazy val getUserInvitation: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getUserInvitation: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "user-invitations" / secretLink => EndpointHelpers.withUserAndBank(req) { (_, bank, cc) => for { @@ -6533,7 +6534,7 @@ object Http4s400 { } } - lazy val getUserInvitations: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getUserInvitations: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "user-invitations" => EndpointHelpers.withUserAndBank(req) { (_, bank, cc) => for { @@ -6891,7 +6892,7 @@ object Http4s400 { // Batch 4 — Consents, ApiCollections, and other simple endpoints // ═══════════════════════════════════════════════════════════════════════════ - lazy val getConsentInfosByBank: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getConsentInfosByBank: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "my" / "consent-infos" => EndpointHelpers.withUserAndBank(req) { (user, bank, _) => for { @@ -6906,7 +6907,7 @@ object Http4s400 { } } - lazy val getConsentInfos: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getConsentInfos: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "my" / "consent-infos" => EndpointHelpers.withUser(req) { (user, _) => for { @@ -6918,7 +6919,7 @@ object Http4s400 { } } - lazy val getMyApiCollectionByName: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getMyApiCollectionByName: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "my" / "api-collections" / "name" / apiCollectionName => EndpointHelpers.withUser(req) { (user, cc) => for { @@ -6928,7 +6929,7 @@ object Http4s400 { } } - lazy val getMyApiCollectionById: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getMyApiCollectionById: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "my" / "api-collections" / apiCollectionId => EndpointHelpers.withUser(req) { (_, cc) => for { @@ -6937,7 +6938,7 @@ object Http4s400 { } } - lazy val getSharableApiCollectionById: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getSharableApiCollectionById: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "api-collections" / "sharable" / apiCollectionId => EndpointHelpers.executeAndRespond(req) { cc => for { @@ -6949,7 +6950,7 @@ object Http4s400 { } } - lazy val getApiCollectionsForUser: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getApiCollectionsForUser: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "users" / userIdStr / "api-collections" => EndpointHelpers.withUser(req) { (_, cc) => for { @@ -6959,7 +6960,7 @@ object Http4s400 { } } - lazy val getFeaturedApiCollections: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getFeaturedApiCollections: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "api-collections" / "featured" => EndpointHelpers.executeAndRespond(req) { cc => for { @@ -6968,7 +6969,7 @@ object Http4s400 { } } - lazy val getMyApiCollections: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getMyApiCollections: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "my" / "api-collections" => EndpointHelpers.withUser(req) { (user, cc) => val params = req.uri.query.params @@ -6980,7 +6981,7 @@ object Http4s400 { } } - lazy val getMyApiCollectionEndpoint: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getMyApiCollectionEndpoint: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "my" / "api-collections" / apiCollectionName / "api-collection-endpoints" / operationId => EndpointHelpers.withUser(req) { (user, cc) => for { @@ -6992,7 +6993,7 @@ object Http4s400 { } } - lazy val getApiCollectionEndpoints: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getApiCollectionEndpoints: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "api-collections" / apiCollectionId / "api-collection-endpoints" => EndpointHelpers.withUser(req) { (_, cc) => for { @@ -7001,7 +7002,7 @@ object Http4s400 { } } - lazy val getMyApiCollectionEndpoints: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getMyApiCollectionEndpoints: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "my" / "api-collections" / apiCollectionName / "api-collection-endpoints" => EndpointHelpers.withUser(req) { (user, cc) => for { @@ -7012,7 +7013,7 @@ object Http4s400 { } } - lazy val getMyApiCollectionEndpointsById: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getMyApiCollectionEndpointsById: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "my" / "api-collection-ids" / apiCollectionId / "api-collection-endpoints" => EndpointHelpers.withUser(req) { (_, cc) => for { @@ -7022,7 +7023,7 @@ object Http4s400 { } } - lazy val deleteMyApiCollection: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val deleteMyApiCollection: Http4sRoute = Http4sRoute { case req @ DELETE -> `prefixPath` / "my" / "api-collections" / apiCollectionId => EndpointHelpers.withUserDelete(req) { (_, cc) => for { @@ -7032,7 +7033,7 @@ object Http4s400 { } } - lazy val deleteMyApiCollectionEndpoint: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val deleteMyApiCollectionEndpoint: Http4sRoute = Http4sRoute { case req @ DELETE -> `prefixPath` / "my" / "api-collections" / apiCollectionName / "api-collection-endpoints" / operationId => EndpointHelpers.withUserDelete(req) { (user, cc) => for { @@ -7046,7 +7047,7 @@ object Http4s400 { } } - lazy val deleteMyApiCollectionEndpointByOperationId: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val deleteMyApiCollectionEndpointByOperationId: Http4sRoute = Http4sRoute { case req @ DELETE -> `prefixPath` / "my" / "api-collection-ids" / apiCollectionId / "api-collection-endpoints" / operationId => EndpointHelpers.withUserDelete(req) { (_, cc) => for { @@ -7059,7 +7060,7 @@ object Http4s400 { } } - lazy val deleteMyApiCollectionEndpointById: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val deleteMyApiCollectionEndpointById: Http4sRoute = Http4sRoute { case req @ DELETE -> `prefixPath` / "my" / "api-collection-ids" / _ / "api-collection-endpoint-ids" / apiCollectionEndpointId => EndpointHelpers.withUserDelete(req) { (_, cc) => for { @@ -7398,7 +7399,7 @@ object Http4s400 { } yield Full(deleted) } - lazy val deleteTransactionAttributeDefinition: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val deleteTransactionAttributeDefinition: Http4sRoute = Http4sRoute { case req @ DELETE -> `prefixPath` / "banks" / _ / "attribute-definitions" / attributeDefinitionId / "transaction" => EndpointHelpers.withBank(req) { (_, cc) => deleteAttributeDefinitionImpl( @@ -7407,7 +7408,7 @@ object Http4s400 { } } - lazy val deleteCustomerAttributeDefinition: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val deleteCustomerAttributeDefinition: Http4sRoute = Http4sRoute { case req @ DELETE -> `prefixPath` / "banks" / _ / "attribute-definitions" / attributeDefinitionId / "customer" => EndpointHelpers.withBank(req) { (_, cc) => deleteAttributeDefinitionImpl( @@ -7416,7 +7417,7 @@ object Http4s400 { } } - lazy val deleteAccountAttributeDefinition: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val deleteAccountAttributeDefinition: Http4sRoute = Http4sRoute { case req @ DELETE -> `prefixPath` / "banks" / _ / "attribute-definitions" / attributeDefinitionId / "account" => EndpointHelpers.withBank(req) { (_, cc) => deleteAttributeDefinitionImpl( @@ -7425,7 +7426,7 @@ object Http4s400 { } } - lazy val deleteProductAttributeDefinition: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val deleteProductAttributeDefinition: Http4sRoute = Http4sRoute { case req @ DELETE -> `prefixPath` / "banks" / _ / "attribute-definitions" / attributeDefinitionId / "product" => EndpointHelpers.withBank(req) { (_, cc) => deleteAttributeDefinitionImpl( @@ -7434,7 +7435,7 @@ object Http4s400 { } } - lazy val deleteCardAttributeDefinition: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val deleteCardAttributeDefinition: Http4sRoute = Http4sRoute { case req @ DELETE -> `prefixPath` / "banks" / _ / "attribute-definitions" / attributeDefinitionId / "card" => EndpointHelpers.withBank(req) { (_, cc) => deleteAttributeDefinitionImpl( @@ -7443,7 +7444,7 @@ object Http4s400 { } } - lazy val deleteTransactionRequestAttributeDefinition: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val deleteTransactionRequestAttributeDefinition: Http4sRoute = Http4sRoute { case req @ DELETE -> `prefixPath` / "banks" / _ / "attribute-definitions" / attributeDefinitionId / "transaction-request" => EndpointHelpers.withBank(req) { (_, cc) => deleteAttributeDefinitionImpl( @@ -7452,7 +7453,7 @@ object Http4s400 { } } - lazy val deleteUser: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val deleteUser: Http4sRoute = Http4sRoute { case req @ DELETE -> `prefixPath` / "users" / userId => EndpointHelpers.withUser(req) { (_, cc) => for { @@ -7462,7 +7463,7 @@ object Http4s400 { } } - lazy val deleteUserCustomerLink: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val deleteUserCustomerLink: Http4sRoute = Http4sRoute { case req @ DELETE -> `prefixPath` / "banks" / _ / "user_customer_links" / userCustomerLinkId => EndpointHelpers.withUserAndBank(req) { (_, _, cc) => for { @@ -7472,7 +7473,7 @@ object Http4s400 { } } - lazy val deleteTransactionCascade: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val deleteTransactionCascade: Http4sRoute = Http4sRoute { case req @ DELETE -> `prefixPath` / "management" / "cascading" / "banks" / bankIdStr / "accounts" / accountIdStr / "transactions" / transactionIdStr => EndpointHelpers.withUser(req) { (_, cc) => for { @@ -7484,7 +7485,7 @@ object Http4s400 { } } - lazy val deleteAccountCascade: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val deleteAccountCascade: Http4sRoute = Http4sRoute { case req @ DELETE -> `prefixPath` / "management" / "cascading" / "banks" / _ / "accounts" / _ => EndpointHelpers.withBankAccount(req) { (_, account, cc) => for { @@ -7493,7 +7494,7 @@ object Http4s400 { } } - lazy val deleteBankCascade: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val deleteBankCascade: Http4sRoute = Http4sRoute { case req @ DELETE -> `prefixPath` / "management" / "cascading" / "banks" / _ => EndpointHelpers.withUserAndBank(req) { (_, bank, _) => for { @@ -7502,7 +7503,7 @@ object Http4s400 { } } - lazy val deleteProductCascade: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val deleteProductCascade: Http4sRoute = Http4sRoute { case req @ DELETE -> `prefixPath` / "management" / "cascading" / "banks" / _ / "products" / productCodeStr => EndpointHelpers.withUserAndBank(req) { (_, bank, cc) => for { @@ -7512,7 +7513,7 @@ object Http4s400 { } } - lazy val deleteCustomerCascade: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val deleteCustomerCascade: Http4sRoute = Http4sRoute { case req @ DELETE -> `prefixPath` / "management" / "cascading" / "banks" / _ / "customers" / customerIdStr => EndpointHelpers.withUserAndBank(req) { (_, _, cc) => for { @@ -7522,7 +7523,7 @@ object Http4s400 { } } - lazy val deleteSystemLevelEndpointTag: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val deleteSystemLevelEndpointTag: Http4sRoute = Http4sRoute { case req @ DELETE -> `prefixPath` / "management" / "endpoints" / _ / "tags" / endpointTagId => EndpointHelpers.withUserDelete(req) { (_, cc) => for { @@ -7532,7 +7533,7 @@ object Http4s400 { } } - lazy val deleteBankLevelEndpointTag: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val deleteBankLevelEndpointTag: Http4sRoute = Http4sRoute { case req @ DELETE -> `prefixPath` / "management" / "banks" / _ / "endpoints" / _ / "tags" / endpointTagId => EndpointHelpers.withUserAndBankDelete(req) { (_, _, cc) => for { @@ -7542,7 +7543,7 @@ object Http4s400 { } } - lazy val deleteAuthenticationTypeValidation: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val deleteAuthenticationTypeValidation: Http4sRoute = Http4sRoute { case req @ DELETE -> `prefixPath` / "management" / "authentication-type-validations" / operationId => EndpointHelpers.withUser(req) { (_, cc) => for { @@ -7553,7 +7554,7 @@ object Http4s400 { } } - lazy val deleteJsonSchemaValidation: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val deleteJsonSchemaValidation: Http4sRoute = Http4sRoute { case req @ DELETE -> `prefixPath` / "management" / "json-schema-validations" / operationId => EndpointHelpers.withUser(req) { (_, cc) => for { @@ -7564,7 +7565,7 @@ object Http4s400 { } } - lazy val deleteCustomerAttribute: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val deleteCustomerAttribute: Http4sRoute = Http4sRoute { case req @ DELETE -> `prefixPath` / "banks" / _ / "customers" / "attributes" / customerAttributeId => EndpointHelpers.withUserAndBankDelete(req) { (_, _, cc) => for { @@ -7573,7 +7574,7 @@ object Http4s400 { } } - lazy val deleteBankAttribute: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val deleteBankAttribute: Http4sRoute = Http4sRoute { case req @ DELETE -> `prefixPath` / "banks" / _ / "attributes" / bankAttributeId => EndpointHelpers.withUserAndBankDelete(req) { (_, _, cc) => for { @@ -7582,7 +7583,7 @@ object Http4s400 { } } - lazy val deleteAtm: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val deleteAtm: Http4sRoute = Http4sRoute { case req @ DELETE -> `prefixPath` / "banks" / _ / "atms" / atmIdStr => EndpointHelpers.withUserAndBankDelete(req) { (_, bank, cc) => for { @@ -7592,7 +7593,7 @@ object Http4s400 { } } - lazy val deleteProductFee: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val deleteProductFee: Http4sRoute = Http4sRoute { case req @ DELETE -> `prefixPath` / "banks" / _ / "products" / _ / "fees" / productFeeId => EndpointHelpers.withUserAndBankDelete(req) { (_, _, cc) => for { @@ -7602,7 +7603,7 @@ object Http4s400 { } } - lazy val deleteEndpointMapping: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val deleteEndpointMapping: Http4sRoute = Http4sRoute { case req @ DELETE -> `prefixPath` / "management" / "endpoint-mappings" / endpointMappingId => EndpointHelpers.withUser(req) { (_, cc) => for { @@ -7611,7 +7612,7 @@ object Http4s400 { } } - lazy val deleteBankLevelEndpointMapping: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val deleteBankLevelEndpointMapping: Http4sRoute = Http4sRoute { case req @ DELETE -> `prefixPath` / "management" / "banks" / _ / "endpoint-mappings" / endpointMappingId => EndpointHelpers.withUserAndBank(req) { (_, bank, cc) => for { @@ -8072,7 +8073,7 @@ object Http4s400 { // Batch 2 — more simple GETs // ═══════════════════════════════════════════════════════════════════════════ - lazy val getEntitlementsForBank: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getEntitlementsForBank: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / bankIdStr / "entitlements" => EndpointHelpers.withUser(req) { (_, cc) => for { @@ -8081,7 +8082,7 @@ object Http4s400 { } } - lazy val getMyPersonalUserAttributes: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getMyPersonalUserAttributes: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "my" / "user" / "attributes" => EndpointHelpers.withUser(req) { (user, cc) => for { @@ -8090,7 +8091,7 @@ object Http4s400 { } } - lazy val getUserWithAttributes: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getUserWithAttributes: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "users" / userId / "attributes" => EndpointHelpers.withUser(req) { (_, cc) => for { @@ -8100,7 +8101,7 @@ object Http4s400 { } } - lazy val getCustomerAttributes: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getCustomerAttributes: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / bankIdStr / "customers" / customerId / "attributes" => EndpointHelpers.withUser(req) { (_, cc) => for { @@ -8115,7 +8116,7 @@ object Http4s400 { } } - lazy val getCustomerAttributeById: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getCustomerAttributeById: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / bankIdStr / "customers" / customerId / "attributes" / customerAttributeId => EndpointHelpers.withUser(req) { (_, cc) => for { @@ -8137,7 +8138,7 @@ object Http4s400 { } yield org.json4s.Extraction.decompose(JSONFactory400.createAttributeDefinitionsJson(defs)) } - lazy val getProductAttributeDefinition: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getProductAttributeDefinition: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "attribute-definitions" / "product" => EndpointHelpers.withBank(req) { (_, cc) => getAttributeDefinitionImpl( @@ -8145,7 +8146,7 @@ object Http4s400 { } } - lazy val getCustomerAttributeDefinition: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getCustomerAttributeDefinition: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "attribute-definitions" / "customer" => EndpointHelpers.withBank(req) { (_, cc) => getAttributeDefinitionImpl( @@ -8153,7 +8154,7 @@ object Http4s400 { } } - lazy val getAccountAttributeDefinition: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getAccountAttributeDefinition: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "attribute-definitions" / "account" => EndpointHelpers.withBank(req) { (_, cc) => getAttributeDefinitionImpl( @@ -8161,7 +8162,7 @@ object Http4s400 { } } - lazy val getTransactionAttributeDefinition: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getTransactionAttributeDefinition: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "attribute-definitions" / "transaction" => EndpointHelpers.withBank(req) { (_, cc) => getAttributeDefinitionImpl( @@ -8169,7 +8170,7 @@ object Http4s400 { } } - lazy val getCardAttributeDefinition: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getCardAttributeDefinition: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "attribute-definitions" / "card" => EndpointHelpers.withBank(req) { (_, cc) => getAttributeDefinitionImpl( @@ -8177,7 +8178,7 @@ object Http4s400 { } } - lazy val getJsonSchemaValidation: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getJsonSchemaValidation: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "management" / "json-schema-validations" / operationId => EndpointHelpers.withUser(req) { (_, cc) => for { @@ -8186,7 +8187,7 @@ object Http4s400 { } } - lazy val getAllJsonSchemaValidations: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getAllJsonSchemaValidations: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "management" / "json-schema-validations" => EndpointHelpers.withUser(req) { (_, cc) => for { @@ -8206,7 +8207,7 @@ object Http4s400 { // from the /management/... variant's. Same underlying route. lazy val getAllJsonSchemaValidationsPublic = getAllJsonSchemaValidations - lazy val getAuthenticationTypeValidation: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getAuthenticationTypeValidation: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "management" / "authentication-type-validations" / operationId => EndpointHelpers.withUser(req) { (_, cc) => for { @@ -8215,7 +8216,7 @@ object Http4s400 { } } - lazy val getAllAuthenticationTypeValidations: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getAllAuthenticationTypeValidations: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "management" / "authentication-type-validations" => EndpointHelpers.withUser(req) { (_, cc) => for { @@ -8235,7 +8236,7 @@ object Http4s400 { // from the /management/... variant's. Same underlying route. lazy val getAllAuthenticationTypeValidationsPublic = getAllAuthenticationTypeValidations - lazy val getConnectorMethod: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getConnectorMethod: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "management" / "connector-methods" / connectorMethodId => EndpointHelpers.withUser(req) { (_, cc) => for { @@ -8244,7 +8245,7 @@ object Http4s400 { } } - lazy val getAllConnectorMethods: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getAllConnectorMethods: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "management" / "connector-methods" => EndpointHelpers.withUser(req) { (_, cc) => for { @@ -8253,7 +8254,7 @@ object Http4s400 { } } - lazy val getUserCustomerLinksByUserId: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getUserCustomerLinksByUserId: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "user_customer_links" / "users" / userId => EndpointHelpers.withUserAndBank(req) { (_, _, cc) => for { @@ -8263,7 +8264,7 @@ object Http4s400 { } } - lazy val getUserCustomerLinksByCustomerId: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getUserCustomerLinksByCustomerId: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "user_customer_links" / "customers" / customerId => EndpointHelpers.withUserAndBank(req) { (_, _, cc) => for { @@ -8273,7 +8274,7 @@ object Http4s400 { } } - lazy val getCustomerMessages: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getCustomerMessages: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "customers" / customerId / "messages" => EndpointHelpers.withUserAndBank(req) { (_, bank, cc) => for { @@ -8283,7 +8284,7 @@ object Http4s400 { } } - lazy val createCustomerMessage: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val createCustomerMessage: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "banks" / _ / "customers" / customerId / "messages" => EndpointHelpers.withUserAndBankAndBodyCreated[CreateMessageJsonV400, code.api.v1_2_1.SuccessMessage](req) { (_, bank, postedData, cc) => @@ -8989,7 +8990,7 @@ object Http4s400 { } } - lazy val createEndpointMapping: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val createEndpointMapping: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "management" / "endpoint-mappings" => EndpointHelpers.executeFutureCreated(req) { val cc = req.callContext @@ -8997,7 +8998,7 @@ object Http4s400 { } } - lazy val updateEndpointMapping: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val updateEndpointMapping: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "management" / "endpoint-mappings" / endpointMappingId => EndpointHelpers.executeFutureCreated(req) { val cc = req.callContext @@ -9005,7 +9006,7 @@ object Http4s400 { } } - lazy val createBankLevelEndpointMapping: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val createBankLevelEndpointMapping: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "management" / "banks" / bankIdStr / "endpoint-mappings" => EndpointHelpers.executeFutureCreated(req) { val cc = req.callContext @@ -9013,7 +9014,7 @@ object Http4s400 { } } - lazy val updateBankLevelEndpointMapping: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val updateBankLevelEndpointMapping: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "management" / "banks" / bankIdStr / "endpoint-mappings" / endpointMappingId => EndpointHelpers.executeFutureCreated(req) { val cc = req.callContext @@ -9096,7 +9097,7 @@ object Http4s400 { // Batch 14 — Endpoint Tags CRUD (create/update — system + bank level) // ═══════════════════════════════════════════════════════════════════════════ - lazy val createSystemLevelEndpointTag: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val createSystemLevelEndpointTag: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "management" / "endpoints" / operationId / "tags" => EndpointHelpers.executeFutureCreated(req) { val cc = req.callContext @@ -9123,7 +9124,7 @@ object Http4s400 { } } - lazy val updateSystemLevelEndpointTag: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val updateSystemLevelEndpointTag: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "management" / "endpoints" / operationId / "tags" / endpointTagId => EndpointHelpers.executeFutureCreated(req) { val cc = req.callContext @@ -9151,7 +9152,7 @@ object Http4s400 { } } - lazy val createBankLevelEndpointTag: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val createBankLevelEndpointTag: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "management" / "banks" / bankIdStr / "endpoints" / operationId / "tags" => EndpointHelpers.executeFutureCreated(req) { val cc = req.callContext @@ -9179,7 +9180,7 @@ object Http4s400 { } } - lazy val updateBankLevelEndpointTag: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val updateBankLevelEndpointTag: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "management" / "banks" / bankIdStr / "endpoints" / operationId / "tags" / endpointTagId => EndpointHelpers.executeFutureCreated(req) { val cc = req.callContext @@ -9297,7 +9298,7 @@ object Http4s400 { // Batch 15 — JSON Schema + Auth Type Validation + Connector Method // ═══════════════════════════════════════════════════════════════════════════ - lazy val createJsonSchemaValidation: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val createJsonSchemaValidation: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "management" / "json-schema-validations" / operationId => EndpointHelpers.executeFutureCreated(req) { val cc = req.callContext @@ -9317,7 +9318,7 @@ object Http4s400 { } } - lazy val updateJsonSchemaValidation: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val updateJsonSchemaValidation: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "management" / "json-schema-validations" / operationId => EndpointHelpers.executeAndRespond(req) { cc => val httpBody = cc.httpBody.getOrElse("") @@ -9338,7 +9339,7 @@ object Http4s400 { private lazy val allowedAuthTypes = AuthenticationType.values.filterNot(AuthenticationType.Anonymous.==) - lazy val createAuthenticationTypeValidation: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val createAuthenticationTypeValidation: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "management" / "authentication-type-validations" / operationId => EndpointHelpers.executeFutureCreated(req) { val cc = req.callContext @@ -9357,7 +9358,7 @@ object Http4s400 { } } - lazy val updateAuthenticationTypeValidation: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val updateAuthenticationTypeValidation: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "management" / "authentication-type-validations" / operationId => EndpointHelpers.executeAndRespond(req) { cc => val rawBody = cc.httpBody.getOrElse("") @@ -9375,7 +9376,7 @@ object Http4s400 { } } - lazy val createConnectorMethod: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val createConnectorMethod: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "management" / "connector-methods" => EndpointHelpers.executeFutureWithStatus(req) { val cc = req.callContext @@ -9408,7 +9409,7 @@ object Http4s400 { } } - lazy val updateConnectorMethod: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val updateConnectorMethod: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "management" / "connector-methods" / connectorMethodId => EndpointHelpers.executeFutureWithStatus(req) { val cc = req.callContext @@ -9700,7 +9701,7 @@ object Http4s400 { } yield result } - lazy val createDynamicResourceDoc: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val createDynamicResourceDoc: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "management" / "dynamic-resource-docs" => EndpointHelpers.executeFutureWithStatus(req) { val cc = req.callContext @@ -9708,7 +9709,7 @@ object Http4s400 { } } - lazy val updateDynamicResourceDoc: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val updateDynamicResourceDoc: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "management" / "dynamic-resource-docs" / dynamicResourceDocId => EndpointHelpers.executeFutureWithStatus(req) { val cc = req.callContext @@ -9716,14 +9717,14 @@ object Http4s400 { } } - lazy val deleteDynamicResourceDoc: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val deleteDynamicResourceDoc: Http4sRoute = Http4sRoute { case req @ DELETE -> `prefixPath` / "management" / "dynamic-resource-docs" / dynamicResourceDocId => EndpointHelpers.withUserAndStatus(req) { (_, cc) => deleteDynamicResourceDocImpl(None, dynamicResourceDocId, cc) } } - lazy val getDynamicResourceDoc: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getDynamicResourceDoc: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "management" / "dynamic-resource-docs" / dynamicResourceDocId => EndpointHelpers.withUser(req) { (_, cc) => for { @@ -9732,7 +9733,7 @@ object Http4s400 { } } - lazy val getAllDynamicResourceDocs: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getAllDynamicResourceDocs: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "management" / "dynamic-resource-docs" => EndpointHelpers.withUser(req) { (_, cc) => for { @@ -9741,7 +9742,7 @@ object Http4s400 { } } - lazy val createBankLevelDynamicResourceDoc: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val createBankLevelDynamicResourceDoc: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "management" / "banks" / bankIdStr / "dynamic-resource-docs" => EndpointHelpers.executeFutureWithStatus(req) { val cc = req.callContext @@ -9749,7 +9750,7 @@ object Http4s400 { } } - lazy val updateBankLevelDynamicResourceDoc: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val updateBankLevelDynamicResourceDoc: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "management" / "banks" / bankIdStr / "dynamic-resource-docs" / dynamicResourceDocId => EndpointHelpers.executeFutureWithStatus(req) { val cc = req.callContext @@ -9757,14 +9758,14 @@ object Http4s400 { } } - lazy val deleteBankLevelDynamicResourceDoc: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val deleteBankLevelDynamicResourceDoc: Http4sRoute = Http4sRoute { case req @ DELETE -> `prefixPath` / "management" / "banks" / bankIdStr / "dynamic-resource-docs" / dynamicResourceDocId => EndpointHelpers.withUserAndStatus(req) { (_, cc) => deleteDynamicResourceDocImpl(Some(bankIdStr), dynamicResourceDocId, cc) } } - lazy val getBankLevelDynamicResourceDoc: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getBankLevelDynamicResourceDoc: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "management" / "banks" / bankIdStr / "dynamic-resource-docs" / dynamicResourceDocId => EndpointHelpers.withUser(req) { (_, cc) => for { @@ -9773,7 +9774,7 @@ object Http4s400 { } } - lazy val getAllBankLevelDynamicResourceDocs: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getAllBankLevelDynamicResourceDocs: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "management" / "banks" / bankIdStr / "dynamic-resource-docs" => EndpointHelpers.withUser(req) { (_, cc) => for { @@ -10018,7 +10019,7 @@ object Http4s400 { } yield result } - lazy val createDynamicMessageDoc: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val createDynamicMessageDoc: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "management" / "dynamic-message-docs" => EndpointHelpers.executeFutureWithStatus(req) { val cc = req.callContext @@ -10026,7 +10027,7 @@ object Http4s400 { } } - lazy val updateDynamicMessageDoc: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val updateDynamicMessageDoc: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "management" / "dynamic-message-docs" / dynamicMessageDocId => EndpointHelpers.executeFutureWithStatus(req) { val cc = req.callContext @@ -10034,14 +10035,14 @@ object Http4s400 { } } - lazy val deleteDynamicMessageDoc: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val deleteDynamicMessageDoc: Http4sRoute = Http4sRoute { case req @ DELETE -> `prefixPath` / "management" / "dynamic-message-docs" / dynamicMessageDocId => EndpointHelpers.withUserAndStatus(req) { (_, cc) => deleteDynamicMessageDocImpl(None, dynamicMessageDocId, cc) } } - lazy val getDynamicMessageDoc: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getDynamicMessageDoc: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "management" / "dynamic-message-docs" / dynamicMessageDocId => EndpointHelpers.withUser(req) { (_, cc) => for { @@ -10050,7 +10051,7 @@ object Http4s400 { } } - lazy val getAllDynamicMessageDocs: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getAllDynamicMessageDocs: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "management" / "dynamic-message-docs" => EndpointHelpers.withUser(req) { (_, cc) => for { @@ -10059,7 +10060,7 @@ object Http4s400 { } } - lazy val createBankLevelDynamicMessageDoc: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val createBankLevelDynamicMessageDoc: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "management" / "banks" / bankIdStr / "dynamic-message-docs" => EndpointHelpers.executeFutureWithStatus(req) { val cc = req.callContext @@ -10067,7 +10068,7 @@ object Http4s400 { } } - lazy val updateBankLevelDynamicMessageDoc: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val updateBankLevelDynamicMessageDoc: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "management" / "banks" / bankIdStr / "dynamic-message-docs" / dynamicMessageDocId => EndpointHelpers.executeFutureWithStatus(req) { val cc = req.callContext @@ -10075,14 +10076,14 @@ object Http4s400 { } } - lazy val deleteBankLevelDynamicMessageDoc: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val deleteBankLevelDynamicMessageDoc: Http4sRoute = Http4sRoute { case req @ DELETE -> `prefixPath` / "management" / "banks" / bankIdStr / "dynamic-message-docs" / dynamicMessageDocId => EndpointHelpers.withUserAndStatus(req) { (_, cc) => deleteDynamicMessageDocImpl(Some(bankIdStr), dynamicMessageDocId, cc) } } - lazy val getBankLevelDynamicMessageDoc: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getBankLevelDynamicMessageDoc: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "management" / "banks" / bankIdStr / "dynamic-message-docs" / dynamicMessageDocId => EndpointHelpers.withUser(req) { (_, cc) => // Lift bug-compat: passes None for bankId; preserved verbatim. @@ -10092,7 +10093,7 @@ object Http4s400 { } } - lazy val getAllBankLevelDynamicMessageDocs: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getAllBankLevelDynamicMessageDocs: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "management" / "banks" / bankIdStr / "dynamic-message-docs" => EndpointHelpers.withUser(req) { (_, cc) => for { @@ -10272,7 +10273,7 @@ object Http4s400 { // Batch 18 — buildDynamicEndpointTemplate // ═══════════════════════════════════════════════════════════════════════════ - lazy val buildDynamicEndpointTemplate: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val buildDynamicEndpointTemplate: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "management" / "dynamic-resource-docs" / "endpoint-code" => EndpointHelpers.executeFutureCreated(req) { val cc = req.callContext @@ -10405,7 +10406,7 @@ object Http4s400 { // ─── addAccount ────────────────────────────────────────────────────────── - lazy val addAccount: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val addAccount: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "banks" / bankIdStr / "accounts" => EndpointHelpers.executeFutureCreated(req) { val cc = req.callContext @@ -10481,7 +10482,7 @@ object Http4s400 { // ─── createSettlementAccount ────────────────────────────────────────────── - lazy val createSettlementAccount: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val createSettlementAccount: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "banks" / bankIdStr / "settlement-accounts" => EndpointHelpers.executeFutureCreated(req) { val cc = req.callContext @@ -10559,7 +10560,7 @@ object Http4s400 { // ─── createConsumer ────────────────────────────────────────────────────── - lazy val createConsumer: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val createConsumer: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "management" / "consumers" => EndpointHelpers.withUser(req) { (u, cc) => val rawBody = cc.httpBody.getOrElse("") @@ -10594,7 +10595,7 @@ object Http4s400 { // ─── createCounterpartyForAnyAccount ────────────────────────────────────── - lazy val createCounterpartyForAnyAccount: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val createCounterpartyForAnyAccount: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "management" / "banks" / bankIdStr / "accounts" / accountIdStr / viewIdStr / "counterparties" => EndpointHelpers.withViewCreated[code.api.v4_0_0.CounterpartyWithMetadataJson400](req) { (user, _, _, cc) => val u = user @@ -10675,7 +10676,7 @@ object Http4s400 { // ─── createHistoricalTransactionAtBank ──────────────────────────────────── - lazy val createHistoricalTransactionAtBank: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val createHistoricalTransactionAtBank: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "banks" / bankIdStr / "management" / "historical" / "transactions" => EndpointHelpers.executeFutureCreated(req) { val cc = req.callContext @@ -10731,7 +10732,7 @@ object Http4s400 { // ─── createUserWithRoles ────────────────────────────────────────────────── - lazy val createUserWithRoles: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val createUserWithRoles: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "user-entitlements" => EndpointHelpers.executeFutureCreated(req) { implicit val cc: CallContext = req.callContext @@ -10762,7 +10763,7 @@ object Http4s400 { // ─── createUserWithAccountAccess ────────────────────────────────────────── - lazy val createUserWithAccountAccess: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val createUserWithAccountAccess: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "banks" / bankIdStr / "accounts" / accountIdStr / "user-account-access" => EndpointHelpers.executeFutureCreated(req) { implicit val cc: CallContext = req.callContext @@ -10801,7 +10802,7 @@ object Http4s400 { private val INVITATION_DEFAULT_EMAIL_TEXT = s"Dear $INVITATION_EMAIL_RECIPIENT_PLACEHOLDER, please activate your account: $INVITATION_ACTIVATE_ACCOUNT_PLACEHOLDER" private val INVITATION_DEFAULT_EMAIL_HTML = s"

Dear $INVITATION_EMAIL_RECIPIENT_PLACEHOLDER,

Please activate your account: Activate

" - lazy val createUserInvitation: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val createUserInvitation: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "banks" / bankIdStr / "user-invitation" => EndpointHelpers.executeFutureCreated(req) { val cc = req.callContext @@ -11114,283 +11115,289 @@ object Http4s400 { // ─── allRoutes ──────────────────────────────────────────────────────────── - private val allOwnRoutes: HttpRoutes[IO] = Kleisli[HttpF, Request[IO], Response[IO]] { req => - root.run(req) - .orElse(getMapperDatabaseInfo.run(req)) - .orElse(getLogoutLink.run(req)) - .orElse(getBanks.run(req)) - .orElse(getBank.run(req)) - .orElse(ibanChecker.run(req)) - .orElse(callsLimit.run(req)) - .orElse(createBank.run(req)) - .orElse(getAtms.run(req)) - .orElse(getAtm.run(req)) - .orElse(getProducts.run(req)) - .orElse(getProduct.run(req)) - .orElse(createAtm.run(req)) - .orElse(createProduct.run(req)) - .orElse(createProductAttribute.run(req)) - .orElse(updateProductAttribute.run(req)) - .orElse(getEntitlements.run(req)) - .orElse(getUserByUserId.run(req)) - .orElse(getUserByUsername.run(req)) - .orElse(getUsersByEmail.run(req)) - .orElse(getUsers.run(req)) - .orElse(getCustomersByAttributes.run(req)) - .orElse(createCustomer.run(req)) - .orElse(getBankAccountsBalancesForCurrentUser.run(req)) - .orElse(getCoreAccountById.run(req)) - .orElse(getPrivateAccountByIdFull.run(req)) - .orElse(getPrivateAccountsAtOneBank.run(req)) - .orElse(createUserCustomerLinks.run(req)) - .orElse(getSystemDynamicEntities.run(req)) - .orElse(getBankLevelDynamicEntities.run(req)) - .orElse(getMyDynamicEntities.run(req)) - .orElse(createSystemDynamicEntity.run(req)) - .orElse(createBankLevelDynamicEntity.run(req)) - .orElse(updateSystemDynamicEntity.run(req)) - .orElse(updateBankLevelDynamicEntity.run(req)) - .orElse(deleteSystemDynamicEntity.run(req)) - .orElse(deleteBankLevelDynamicEntity.run(req)) - .orElse(updateMyDynamicEntity.run(req)) - .orElse(deleteMyDynamicEntity.run(req)) - .orElse(createDynamicEndpoint.run(req)) - .orElse(createBankLevelDynamicEndpoint.run(req)) - .orElse(updateDynamicEndpointHost.run(req)) - .orElse(updateBankLevelDynamicEndpointHost.run(req)) - .orElse(getDynamicEndpoint.run(req)) - .orElse(getDynamicEndpoints.run(req)) - .orElse(getBankLevelDynamicEndpoint.run(req)) - .orElse(getBankLevelDynamicEndpoints.run(req)) - .orElse(deleteDynamicEndpoint.run(req)) - .orElse(deleteBankLevelDynamicEndpoint.run(req)) - .orElse(getMyDynamicEndpoints.run(req)) - .orElse(deleteMyDynamicEndpoint.run(req)) - .orElse(getProductAttribute.run(req)) - .orElse(getScopes.run(req)) - .orElse(addScope.run(req)) - .orElse(getConsents.run(req)) - .orElse(updateAccountLabel.run(req)) - .orElse(getExplicitCounterpartiesForAccount.run(req)) - .orElse(getExplicitCounterpartyById.run(req)) - .orElse(createExplicitCounterparty.run(req)) - .orElse(getFirehoseAccountsAtOneBank.run(req)) - .orElse(createTransactionRequest.run(req)) - .orElse(answerTransactionRequestChallenge.run(req)) - // Batch 1 — simple GETs - .orElse(getCallContext.run(req)) - .orElse(verifyRequestSignResponse.run(req)) - .orElse(getCurrentUserId.run(req)) - .orElse(getScannedApiVersions.run(req)) - .orElse(getMySpaces.run(req)) - .orElse(getBankAttributes.run(req)) - .orElse(getBankAttribute.run(req)) - .orElse(getSystemLevelEndpointTags.run(req)) - .orElse(getBankLevelEndpointTags.run(req)) - .orElse(getEndpointMapping.run(req)) - .orElse(getBankLevelEndpointMapping.run(req)) - .orElse(getAllEndpointMappings.run(req)) - .orElse(getAllBankLevelEndpointMappings.run(req)) - // Batch 2 — more GETs - .orElse(getEntitlementsForBank.run(req)) - .orElse(getMyPersonalUserAttributes.run(req)) - .orElse(getUserWithAttributes.run(req)) - .orElse(getCustomerAttributes.run(req)) - .orElse(getCustomerAttributeById.run(req)) - .orElse(getProductAttributeDefinition.run(req)) - .orElse(getCustomerAttributeDefinition.run(req)) - .orElse(getAccountAttributeDefinition.run(req)) - .orElse(getTransactionAttributeDefinition.run(req)) - .orElse(getCardAttributeDefinition.run(req)) - .orElse(getJsonSchemaValidation.run(req)) - .orElse(getAllJsonSchemaValidations.run(req)) - .orElse(getAuthenticationTypeValidation.run(req)) - .orElse(getAllAuthenticationTypeValidations.run(req)) - .orElse(getConnectorMethod.run(req)) - .orElse(getAllConnectorMethods.run(req)) - .orElse(getUserCustomerLinksByUserId.run(req)) - .orElse(getUserCustomerLinksByCustomerId.run(req)) - .orElse(getCustomerMessages.run(req)) - .orElse(createCustomerMessage.run(req)) - // Batch 3 — DELETEs - .orElse(deleteTransactionAttributeDefinition.run(req)) - .orElse(deleteCustomerAttributeDefinition.run(req)) - .orElse(deleteAccountAttributeDefinition.run(req)) - .orElse(deleteProductAttributeDefinition.run(req)) - .orElse(deleteCardAttributeDefinition.run(req)) - .orElse(deleteTransactionRequestAttributeDefinition.run(req)) - .orElse(deleteUser.run(req)) - .orElse(deleteUserCustomerLink.run(req)) - .orElse(deleteTransactionCascade.run(req)) - .orElse(deleteAccountCascade.run(req)) - .orElse(deleteBankCascade.run(req)) - .orElse(deleteProductCascade.run(req)) - .orElse(deleteCustomerCascade.run(req)) - .orElse(deleteSystemLevelEndpointTag.run(req)) - .orElse(deleteBankLevelEndpointTag.run(req)) - .orElse(deleteAuthenticationTypeValidation.run(req)) - .orElse(deleteJsonSchemaValidation.run(req)) - .orElse(deleteCustomerAttribute.run(req)) - .orElse(deleteBankAttribute.run(req)) - .orElse(deleteAtm.run(req)) - .orElse(deleteProductFee.run(req)) - .orElse(deleteEndpointMapping.run(req)) - .orElse(deleteBankLevelEndpointMapping.run(req)) - // Batch 4 — Consents, ApiCollections - .orElse(getConsentInfosByBank.run(req)) - .orElse(getConsentInfos.run(req)) - .orElse(getMyApiCollectionByName.run(req)) - .orElse(getMyApiCollectionById.run(req)) - .orElse(getSharableApiCollectionById.run(req)) - .orElse(getApiCollectionsForUser.run(req)) - .orElse(getFeaturedApiCollections.run(req)) - .orElse(getMyApiCollections.run(req)) - .orElse(getMyApiCollectionEndpoint.run(req)) - .orElse(getApiCollectionEndpoints.run(req)) - .orElse(getMyApiCollectionEndpoints.run(req)) - .orElse(getMyApiCollectionEndpointsById.run(req)) - .orElse(deleteMyApiCollection.run(req)) - .orElse(deleteMyApiCollectionEndpoint.run(req)) - .orElse(deleteMyApiCollectionEndpointByOperationId.run(req)) - .orElse(deleteMyApiCollectionEndpointById.run(req)) - // Batch 5 — more GETs - .orElse(getProductFee.run(req)) - .orElse(getProductFees.run(req)) - .orElse(getTransactionAttributes.run(req)) - .orElse(getTransactionAttributeById.run(req)) - .orElse(getTransactionRequestAttributes.run(req)) - .orElse(getTransactionRequestAttributeById.run(req)) - .orElse(getTransactionRequestAttributeDefinition.run(req)) - .orElse(getTransactionRequest.run(req)) - .orElse(getMyCorrelatedEntities.run(req)) - .orElse(getCorrelatedUsersInfoByCustomerId.run(req)) - .orElse(getAccountsMinimalByCustomerId.run(req)) - .orElse(getCustomersByCustomerPhoneNumber.run(req)) - .orElse(getCustomersAtAnyBank.run(req)) - .orElse(getCustomersMinimalAtAnyBank.run(req)) - .orElse(getUserInvitation.run(req)) - .orElse(getUserInvitations.run(req)) - // Batch 6 — ATM updates - .orElse(updateAtmSupportedCurrencies.run(req)) - .orElse(updateAtmSupportedLanguages.run(req)) - .orElse(updateAtmAccessibilityFeatures.run(req)) - .orElse(updateAtmServices.run(req)) - .orElse(updateAtmNotes.run(req)) - .orElse(updateAtmLocationCategories.run(req)) - .orElse(updateAtm.run(req)) - // Batch 7 — Attribute Definitions PUT - .orElse(createOrUpdateCustomerAttributeAttributeDefinition.run(req)) - .orElse(createOrUpdateAccountAttributeDefinition.run(req)) - .orElse(createOrUpdateProductAttributeDefinition.run(req)) - .orElse(createOrUpdateTransactionAttributeDefinition.run(req)) - .orElse(createOrUpdateTransactionRequestAttributeDefinition.run(req)) - .orElse(createOrUpdateCardAttributeDefinition.run(req)) - .orElse(createOrUpdateBankAttributeDefinition.run(req)) - // Batch 8 — Counterparty management - .orElse(getCounterpartiesForAnyAccount.run(req)) - .orElse(getCounterpartyByIdForAnyAccount.run(req)) - .orElse(getCounterpartyByNameForAnyAccount.run(req)) - // Batch 9 — Remaining v4 migrations - .orElse(createTransactionRequestCard.run(req)) - .orElse(deleteExplicitCounterparty.run(req)) - .orElse(deleteCounterpartyForAnyAccount.run(req)) - .orElse(deleteTagForViewOnAccount.run(req)) - .orElse(getTagsForViewOnAccount.run(req)) - .orElse(addTagForViewOnAccount.run(req)) - .orElse(getDoubleEntryTransaction.run(req)) - .orElse(getBalancingTransaction.run(req)) - .orElse(getBankAccountBalancesForCurrentUser.run(req)) - .orElse(getAccountByAccountRouting.run(req)) - .orElse(getAccountsByAccountRoutingRegex.run(req)) - .orElse(lockUser.run(req)) - .orElse(resetPasswordUrl.run(req)) - .orElse(getSettlementAccounts.run(req)) - // Batch 10 — Attribute create/update - .orElse(createBankAttribute.run(req)) - .orElse(updateBankAttribute.run(req)) - .orElse(createCustomerAttribute.run(req)) - .orElse(updateCustomerAttribute.run(req)) - .orElse(createTransactionAttribute.run(req)) - .orElse(updateTransactionAttribute.run(req)) - .orElse(createTransactionRequestAttribute.run(req)) - .orElse(updateTransactionRequestAttribute.run(req)) - .orElse(createProductFee.run(req)) - .orElse(updateProductFee.run(req)) - .orElse(createMyPersonalUserAttribute.run(req)) - .orElse(updateMyPersonalUserAttribute.run(req)) - // Batch 11 — account access, user invitations, consents, api collections - .orElse(getUserInvitationAnonymous.run(req)) - .orElse(grantUserAccessToView.run(req)) - .orElse(revokeUserAccessToView.run(req)) - .orElse(revokeGrantUserAccessToViews.run(req)) - .orElse(createMyApiCollection.run(req)) - .orElse(createMyApiCollectionEndpoint.run(req)) - .orElse(createMyApiCollectionEndpointById.run(req)) - .orElse(updateConsentStatus.run(req)) - .orElse(addConsentUser.run(req)) - // Batch 12 — direct debits, standing orders, webhooks, fast firehose - .orElse(createDirectDebit.run(req)) - .orElse(createDirectDebitManagement.run(req)) - .orElse(createStandingOrder.run(req)) - .orElse(createStandingOrderManagement.run(req)) - .orElse(createSystemAccountNotificationWebhook.run(req)) - .orElse(createBankAccountNotificationWebhook.run(req)) - .orElse(getFastFirehoseAccountsAtOneBank.run(req)) - // Batch 13 — Endpoint Mappings create/update - .orElse(createEndpointMapping.run(req)) - .orElse(updateEndpointMapping.run(req)) - .orElse(createBankLevelEndpointMapping.run(req)) - .orElse(updateBankLevelEndpointMapping.run(req)) - // Batch 14 — Endpoint Tags CRUD - .orElse(createSystemLevelEndpointTag.run(req)) - .orElse(updateSystemLevelEndpointTag.run(req)) - .orElse(createBankLevelEndpointTag.run(req)) - .orElse(updateBankLevelEndpointTag.run(req)) - // Batch 15 — JSON Schema + Auth Type Validation + Connector Method - .orElse(createJsonSchemaValidation.run(req)) - .orElse(updateJsonSchemaValidation.run(req)) - .orElse(createAuthenticationTypeValidation.run(req)) - .orElse(updateAuthenticationTypeValidation.run(req)) - .orElse(createConnectorMethod.run(req)) - .orElse(updateConnectorMethod.run(req)) - // Batch 16 — Dynamic Resource Doc CRUD - .orElse(createDynamicResourceDoc.run(req)) - .orElse(updateDynamicResourceDoc.run(req)) - .orElse(deleteDynamicResourceDoc.run(req)) - .orElse(getDynamicResourceDoc.run(req)) - .orElse(getAllDynamicResourceDocs.run(req)) - .orElse(createBankLevelDynamicResourceDoc.run(req)) - .orElse(updateBankLevelDynamicResourceDoc.run(req)) - .orElse(deleteBankLevelDynamicResourceDoc.run(req)) - .orElse(getBankLevelDynamicResourceDoc.run(req)) - .orElse(getAllBankLevelDynamicResourceDocs.run(req)) - // Batch 17 — Dynamic Message Doc CRUD - .orElse(createDynamicMessageDoc.run(req)) - .orElse(updateDynamicMessageDoc.run(req)) - .orElse(deleteDynamicMessageDoc.run(req)) - .orElse(getDynamicMessageDoc.run(req)) - .orElse(getAllDynamicMessageDocs.run(req)) - .orElse(createBankLevelDynamicMessageDoc.run(req)) - .orElse(updateBankLevelDynamicMessageDoc.run(req)) - .orElse(deleteBankLevelDynamicMessageDoc.run(req)) - .orElse(getBankLevelDynamicMessageDoc.run(req)) - .orElse(getAllBankLevelDynamicMessageDocs.run(req)) - // Batch 18 — buildDynamicEndpointTemplate - .orElse(buildDynamicEndpointTemplate.run(req)) - // Batch 19 — Complex authn (addAccount, createConsumer, createCounterpartyForAnyAccount, - // createHistoricalTransactionAtBank, createSettlementAccount, - // createUserInvitation, createUserWithAccountAccess, createUserWithRoles) - .orElse(addAccount.run(req)) - .orElse(createSettlementAccount.run(req)) - .orElse(createConsumer.run(req)) - .orElse(createCounterpartyForAnyAccount.run(req)) - .orElse(createHistoricalTransactionAtBank.run(req)) - .orElse(createUserWithRoles.run(req)) - .orElse(createUserWithAccountAccess.run(req)) - .orElse(createUserInvitation.run(req)) - } - - lazy val allRoutesWithMiddleware: HttpRoutes[IO] = ResourceDocMiddleware.apply(resourceDocs)(IdempotencyMiddleware(allOwnRoutes)) + // The routes in the order they are tried. ResourceDocMiddleware selects the doc of the first + // route that serves a request, so it is given the docs in this same order. + lazy val routesInOrder: List[Http4sHandler] = List( + root, + getMapperDatabaseInfo, + getLogoutLink, + getBanks, + getBank, + ibanChecker, + callsLimit, + createBank, + getAtms, + getAtm, + getProducts, + getProduct, + createAtm, + createProduct, + createProductAttribute, + updateProductAttribute, + getEntitlements, + getUserByUserId, + getUserByUsername, + getUsersByEmail, + getUsers, + getCustomersByAttributes, + createCustomer, + getBankAccountsBalancesForCurrentUser, + getCoreAccountById, + getPrivateAccountByIdFull, + getPrivateAccountsAtOneBank, + createUserCustomerLinks, + getSystemDynamicEntities, + getBankLevelDynamicEntities, + getMyDynamicEntities, + createSystemDynamicEntity, + createBankLevelDynamicEntity, + updateSystemDynamicEntity, + updateBankLevelDynamicEntity, + deleteSystemDynamicEntity, + deleteBankLevelDynamicEntity, + updateMyDynamicEntity, + deleteMyDynamicEntity, + createDynamicEndpoint, + createBankLevelDynamicEndpoint, + updateDynamicEndpointHost, + updateBankLevelDynamicEndpointHost, + getDynamicEndpoint, + getDynamicEndpoints, + getBankLevelDynamicEndpoint, + getBankLevelDynamicEndpoints, + deleteDynamicEndpoint, + deleteBankLevelDynamicEndpoint, + getMyDynamicEndpoints, + deleteMyDynamicEndpoint, + getProductAttribute, + getScopes, + addScope, + getConsents, + updateAccountLabel, + getExplicitCounterpartiesForAccount, + getExplicitCounterpartyById, + createExplicitCounterparty, + getFirehoseAccountsAtOneBank, + createTransactionRequest, + answerTransactionRequestChallenge, + // Batch 1 — simple GETs + getCallContext, + verifyRequestSignResponse, + getCurrentUserId, + getScannedApiVersions, + getMySpaces, + getBankAttributes, + getBankAttribute, + getSystemLevelEndpointTags, + getBankLevelEndpointTags, + getEndpointMapping, + getBankLevelEndpointMapping, + getAllEndpointMappings, + getAllBankLevelEndpointMappings, + // Batch 2 — more GETs + getEntitlementsForBank, + getMyPersonalUserAttributes, + getUserWithAttributes, + getCustomerAttributes, + getCustomerAttributeById, + getProductAttributeDefinition, + getCustomerAttributeDefinition, + getAccountAttributeDefinition, + getTransactionAttributeDefinition, + getCardAttributeDefinition, + getJsonSchemaValidation, + getAllJsonSchemaValidations, + getAuthenticationTypeValidation, + getAllAuthenticationTypeValidations, + getConnectorMethod, + getAllConnectorMethods, + getUserCustomerLinksByUserId, + getUserCustomerLinksByCustomerId, + getCustomerMessages, + createCustomerMessage, + // Batch 3 — DELETEs + deleteTransactionAttributeDefinition, + deleteCustomerAttributeDefinition, + deleteAccountAttributeDefinition, + deleteProductAttributeDefinition, + deleteCardAttributeDefinition, + deleteTransactionRequestAttributeDefinition, + deleteUser, + deleteUserCustomerLink, + deleteTransactionCascade, + deleteAccountCascade, + deleteBankCascade, + deleteProductCascade, + deleteCustomerCascade, + deleteSystemLevelEndpointTag, + deleteBankLevelEndpointTag, + deleteAuthenticationTypeValidation, + deleteJsonSchemaValidation, + deleteCustomerAttribute, + deleteBankAttribute, + deleteAtm, + deleteProductFee, + deleteEndpointMapping, + deleteBankLevelEndpointMapping, + // Batch 4 — Consents, ApiCollections + getConsentInfosByBank, + getConsentInfos, + getMyApiCollectionByName, + getMyApiCollectionById, + getSharableApiCollectionById, + getApiCollectionsForUser, + getFeaturedApiCollections, + getMyApiCollections, + getMyApiCollectionEndpoint, + getApiCollectionEndpoints, + getMyApiCollectionEndpoints, + getMyApiCollectionEndpointsById, + deleteMyApiCollection, + deleteMyApiCollectionEndpoint, + deleteMyApiCollectionEndpointByOperationId, + deleteMyApiCollectionEndpointById, + // Batch 5 — more GETs + getProductFee, + getProductFees, + getTransactionAttributes, + getTransactionAttributeById, + getTransactionRequestAttributes, + getTransactionRequestAttributeById, + getTransactionRequestAttributeDefinition, + getTransactionRequest, + getMyCorrelatedEntities, + getCorrelatedUsersInfoByCustomerId, + getAccountsMinimalByCustomerId, + getCustomersByCustomerPhoneNumber, + getCustomersAtAnyBank, + getCustomersMinimalAtAnyBank, + getUserInvitation, + getUserInvitations, + // Batch 6 — ATM updates + updateAtmSupportedCurrencies, + updateAtmSupportedLanguages, + updateAtmAccessibilityFeatures, + updateAtmServices, + updateAtmNotes, + updateAtmLocationCategories, + updateAtm, + // Batch 7 — Attribute Definitions PUT + createOrUpdateCustomerAttributeAttributeDefinition, + createOrUpdateAccountAttributeDefinition, + createOrUpdateProductAttributeDefinition, + createOrUpdateTransactionAttributeDefinition, + createOrUpdateTransactionRequestAttributeDefinition, + createOrUpdateCardAttributeDefinition, + createOrUpdateBankAttributeDefinition, + // Batch 8 — Counterparty management + getCounterpartiesForAnyAccount, + getCounterpartyByIdForAnyAccount, + getCounterpartyByNameForAnyAccount, + // Batch 9 — Remaining v4 migrations + createTransactionRequestCard, + deleteExplicitCounterparty, + deleteCounterpartyForAnyAccount, + deleteTagForViewOnAccount, + getTagsForViewOnAccount, + addTagForViewOnAccount, + getDoubleEntryTransaction, + getBalancingTransaction, + getBankAccountBalancesForCurrentUser, + getAccountByAccountRouting, + getAccountsByAccountRoutingRegex, + lockUser, + resetPasswordUrl, + getSettlementAccounts, + // Batch 10 — Attribute create/update + createBankAttribute, + updateBankAttribute, + createCustomerAttribute, + updateCustomerAttribute, + createTransactionAttribute, + updateTransactionAttribute, + createTransactionRequestAttribute, + updateTransactionRequestAttribute, + createProductFee, + updateProductFee, + createMyPersonalUserAttribute, + updateMyPersonalUserAttribute, + // Batch 11 — account access, user invitations, consents, api collections + getUserInvitationAnonymous, + grantUserAccessToView, + revokeUserAccessToView, + revokeGrantUserAccessToViews, + createMyApiCollection, + createMyApiCollectionEndpoint, + createMyApiCollectionEndpointById, + updateConsentStatus, + addConsentUser, + // Batch 12 — direct debits, standing orders, webhooks, fast firehose + createDirectDebit, + createDirectDebitManagement, + createStandingOrder, + createStandingOrderManagement, + createSystemAccountNotificationWebhook, + createBankAccountNotificationWebhook, + getFastFirehoseAccountsAtOneBank, + // Batch 13 — Endpoint Mappings create/update + createEndpointMapping, + updateEndpointMapping, + createBankLevelEndpointMapping, + updateBankLevelEndpointMapping, + // Batch 14 — Endpoint Tags CRUD + createSystemLevelEndpointTag, + updateSystemLevelEndpointTag, + createBankLevelEndpointTag, + updateBankLevelEndpointTag, + // Batch 15 — JSON Schema + Auth Type Validation + Connector Method + createJsonSchemaValidation, + updateJsonSchemaValidation, + createAuthenticationTypeValidation, + updateAuthenticationTypeValidation, + createConnectorMethod, + updateConnectorMethod, + // Batch 16 — Dynamic Resource Doc CRUD + createDynamicResourceDoc, + updateDynamicResourceDoc, + deleteDynamicResourceDoc, + getDynamicResourceDoc, + getAllDynamicResourceDocs, + createBankLevelDynamicResourceDoc, + updateBankLevelDynamicResourceDoc, + deleteBankLevelDynamicResourceDoc, + getBankLevelDynamicResourceDoc, + getAllBankLevelDynamicResourceDocs, + // Batch 17 — Dynamic Message Doc CRUD + createDynamicMessageDoc, + updateDynamicMessageDoc, + deleteDynamicMessageDoc, + getDynamicMessageDoc, + getAllDynamicMessageDocs, + createBankLevelDynamicMessageDoc, + updateBankLevelDynamicMessageDoc, + deleteBankLevelDynamicMessageDoc, + getBankLevelDynamicMessageDoc, + getAllBankLevelDynamicMessageDocs, + // Batch 18 — buildDynamicEndpointTemplate + buildDynamicEndpointTemplate, + // Batch 19 — Complex authn (addAccount, createConsumer, createCounterpartyForAnyAccount, + // createHistoricalTransactionAtBank, createSettlementAccount, + // createUserInvitation, createUserWithAccountAccess, createUserWithRoles) + addAccount, + createSettlementAccount, + createConsumer, + createCounterpartyForAnyAccount, + createHistoricalTransactionAtBank, + createUserWithRoles, + createUserWithAccountAccess, + createUserInvitation + ) + + lazy val orderedResourceDocs: ArrayBuffer[ResourceDoc] = ResourceDocMatcher.orderByRoutes(resourceDocs, routesInOrder) + + private lazy val allOwnRoutes: HttpRoutes[IO] = Http4sRoute.chain(routesInOrder) + + lazy val allRoutesWithMiddleware: HttpRoutes[IO] = ResourceDocMiddleware.apply(orderedResourceDocs, routes => IdempotencyMiddleware(routes))(IdempotencyMiddleware(allOwnRoutes)) // ─── nameOf-compatibility aliases ──────────────────────────────────────── // These vals have no Lift counterpart in Http4s400 but are referenced by From cd4eb113d87646bf0a706db77ce7507e875d87ae Mon Sep 17 00:00:00 2001 From: Hongwei Date: Wed, 7 Oct 2026 17:29:58 +0200 Subject: [PATCH 14/29] refactor: bind every v3.1.0 ResourceDoc to its route Declare each endpoint as an Http4sRoute and derive the route chain from one list, so the middleware selects the doc of the route that serves a request and runs only that route. The path-rewriting bridge asks the routes whether the version serves a path instead of matching the URL against doc templates. createConsent is shared by the EMAIL, SMS and IMPLICIT docs. The message-docs Swagger doc has no route here (Http4sResourceDocs serves it) and stays out of the middleware's docs. --- .../scala/code/api/v3_1_0/Http4s310.scala | 419 +++++++++--------- 1 file changed, 212 insertions(+), 207 deletions(-) diff --git a/obp-api/src/main/scala/code/api/v3_1_0/Http4s310.scala b/obp-api/src/main/scala/code/api/v3_1_0/Http4s310.scala index f293021eba..de306d8429 100644 --- a/obp-api/src/main/scala/code/api/v3_1_0/Http4s310.scala +++ b/obp-api/src/main/scala/code/api/v3_1_0/Http4s310.scala @@ -35,7 +35,7 @@ import code.api.ResourceDocs1_4_0.SwaggerDefinitionsJSON import code.api.ResourceDocs1_4_0.SwaggerDefinitionsJSON._ import code.api.util.Glossary import code.api.util.ExampleValue._ -import code.api.util.APIUtil.{EmptyBody, ResourceDoc, _} +import code.api.util.APIUtil.{Http4sHandler, Http4sRoute, EmptyBody, ResourceDoc, _} import code.api.util.ApiRole import code.api.util.ApiRole._ import code.api.util.ApiTag._ @@ -43,7 +43,7 @@ import code.api.util.ErrorMessages._ import code.api.util.CertificateUtil import code.api.util.{ApiTrigger, AttributeTypeDocs, Consent, Glossary, SecureRandomUtil} import code.api.util.http4s.Http4sRequestAttributes.{EndpointHelpers, RequestOps} -import code.api.util.http4s.ResourceDocMiddleware +import code.api.util.http4s.{ResourceDocMatcher, ResourceDocMiddleware} import code.api.util.http4s.IdempotencyMiddleware import code.api.util.newstyle.{BalanceNewStyle, ViewNewStyle} import code.api.util.{APIUtil, CallContext, CustomJsonFormats, NewStyle, OBPBankId, RateLimitingUtil} @@ -155,7 +155,7 @@ object Http4s310 { // ─── root ───────────────────────────────────────────────────────────────── - val root: HttpRoutes[IO] = HttpRoutes.of[IO] { + val root: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` => EndpointHelpers.executeAndRespond(req) { _ => Future.successful(JSONFactory.getApiInfoJSON(ApiVersion.v3_1_0, versionStatus)) @@ -187,7 +187,7 @@ object Http4s310 { // ─── getCheckbookOrders ─────────────────────────────────────────────────── - val getCheckbookOrders: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getCheckbookOrders: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "accounts" / _ / _ / "checkbook" / "orders" => EndpointHelpers.withView(req) { (_, account, _, cc) => for { @@ -212,7 +212,7 @@ object Http4s310 { // ─── getStatusOfCreditCardOrder ─────────────────────────────────────────── - val getStatusOfCreditCardOrder: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getStatusOfCreditCardOrder: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "accounts" / _ / _ / "credit_cards" / "orders" => EndpointHelpers.withView(req) { (_, account, _, cc) => for { @@ -244,7 +244,7 @@ object Http4s310 { // ─── getTopAPIs ─────────────────────────────────────────────────────────── - val getTopAPIs: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getTopAPIs: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "management" / "metrics" / "top-apis" => EndpointHelpers.withUser(req) { (_, cc) => val httpParams = req.headers.headers.toList.map(h => HTTPParam(h.name.toString, h.value)) ::: @@ -318,7 +318,7 @@ object Http4s310 { // ─── getMetricsTopConsumers ─────────────────────────────────────────────── - val getMetricsTopConsumers: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getMetricsTopConsumers: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "management" / "metrics" / "top-consumers" => EndpointHelpers.withUser(req) { (_, cc) => val httpParams = req.headers.headers.toList.map(h => HTTPParam(h.name.toString, h.value)) ::: @@ -397,7 +397,7 @@ object Http4s310 { // Firehose pattern: prop check (→400) before role check (→403) before bank lookup (→404). // Uses non-standard ALL_CAPS template var FIREHOSE_BANK_ID so middleware skips bank validation. - val getFirehoseCustomers: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getFirehoseCustomers: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / bankIdStr / "firehose" / "customers" => EndpointHelpers.withUser(req) { (user, cc) => val roles = ApiRole.canUseCustomerFirehose :: canUseCustomerFirehoseAtAnyBank :: Nil @@ -456,7 +456,7 @@ object Http4s310 { // ─── getBadLoginStatus ──────────────────────────────────────────────────── - val getBadLoginStatus: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getBadLoginStatus: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "users" / username / "lock-status" => EndpointHelpers.withUser(req) { (user, cc) => for { @@ -493,7 +493,7 @@ object Http4s310 { // ─── getCallsLimit ──────────────────────────────────────────────────────── - val getCallsLimit: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getCallsLimit: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "management" / "consumers" / consumerIdStr / "consumer" / "call-limits" => EndpointHelpers.withUser(req) { (user, cc) => for { @@ -526,7 +526,7 @@ object Http4s310 { // ─── getConsumer ────────────────────────────────────────────────────────── - val getConsumer: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getConsumer: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "management" / "consumers" / consumerIdStr => EndpointHelpers.withUser(req) { (user, cc) => for { @@ -557,7 +557,7 @@ object Http4s310 { // ─── getConsumersForCurrentUser ────────────────────────────────────────── - val getConsumersForCurrentUser: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getConsumersForCurrentUser: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "management" / "users" / "current" / "consumers" => EndpointHelpers.withUser(req) { (user, _) => for { @@ -585,7 +585,7 @@ object Http4s310 { // ─── getConsumers ──────────────────────────────────────────────────────── - val getConsumers: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getConsumers: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "management" / "consumers" => EndpointHelpers.withUser(req) { (user, cc) => for { @@ -622,7 +622,7 @@ object Http4s310 { // ─── getAccountWebhooks ────────────────────────────────────────────────── - val getAccountWebhooks: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getAccountWebhooks: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "management" / "banks" / _ / "account-web-hooks" => EndpointHelpers.withUserAndBank(req) { (user, bank, cc) => for { @@ -663,7 +663,7 @@ object Http4s310 { // ─── config ─────────────────────────────────────────────────────────────── - val config: HttpRoutes[IO] = HttpRoutes.of[IO] { + val config: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "config" => EndpointHelpers.withUser(req) { (user, cc) => for { @@ -694,7 +694,7 @@ object Http4s310 { // ─── getAdapterInfo ─────────────────────────────────────────────────────── - val getAdapterInfo: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getAdapterInfo: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "adapter" => EndpointHelpers.withUser(req) { (user, cc) => for { @@ -726,7 +726,7 @@ object Http4s310 { // ─── getRateLimitingInfo ────────────────────────────────────────────────── // Anonymous endpoint — no auth required. - val getRateLimitingInfo: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getRateLimitingInfo: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "rate-limiting" => EndpointHelpers.executeAndRespond(req) { cc => for { @@ -766,7 +766,7 @@ object Http4s310 { // ─── getCustomerByCustomerId ────────────────────────────────────────────── - val getCustomerByCustomerId: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getCustomerByCustomerId: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "customers" / customerIdStr => EndpointHelpers.withUserAndBank(req) { (user, bank, cc) => for { @@ -805,7 +805,7 @@ object Http4s310 { // ─── getUserAuthContexts ───────────────────────────────────────────────── - val getUserAuthContexts: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getUserAuthContexts: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "users" / userIdStr / "auth-context" => EndpointHelpers.withUser(req) { (user, cc) => for { @@ -838,7 +838,7 @@ object Http4s310 { // ─── getTaxResidence ───────────────────────────────────────────────────── - val getTaxResidence: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getTaxResidence: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "customers" / customerIdStr / "tax-residences" => EndpointHelpers.withUserAndBank(req) { (user, bank, cc) => for { @@ -871,7 +871,7 @@ object Http4s310 { // ─── getAllEntitlements ────────────────────────────────────────────────── - val getAllEntitlements: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getAllEntitlements: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "entitlements" => EndpointHelpers.withUser(req) { (user, cc) => val roleName = req.uri.query.params.getOrElse("role", "") @@ -911,7 +911,7 @@ object Http4s310 { // ─── getCustomerAddresses ──────────────────────────────────────────────── - val getCustomerAddresses: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getCustomerAddresses: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "customers" / customerIdStr / "addresses" => EndpointHelpers.withUserAndBank(req) { (user, bank, cc) => for { @@ -944,7 +944,7 @@ object Http4s310 { // ─── getProductAttribute ───────────────────────────────────────────────── - val getProductAttribute: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getProductAttribute: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / bankIdStr / "products" / _ / "attributes" / productAttributeIdStr => EndpointHelpers.withUser(req) { (user, cc) => for { @@ -980,7 +980,7 @@ object Http4s310 { // ─── getAccountApplications ────────────────────────────────────────────── - val getAccountApplications: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getAccountApplications: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "account-applications" => EndpointHelpers.withUserAndBank(req) { (user, _, cc) => for { @@ -1014,7 +1014,7 @@ object Http4s310 { // ─── getAccountApplication ─────────────────────────────────────────────── - val getAccountApplication: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getAccountApplication: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "account-applications" / accountApplicationIdStr => EndpointHelpers.withUserAndBank(req) { (_, _, cc) => for { @@ -1049,7 +1049,7 @@ object Http4s310 { // ─── getMeetings ───────────────────────────────────────────────────────── - val getMeetings: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getMeetings: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "meetings" => EndpointHelpers.withUserAndBank(req) { (user, bank, cc) => for { @@ -1082,7 +1082,7 @@ object Http4s310 { // ─── getMeeting ────────────────────────────────────────────────────────── - val getMeeting: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getMeeting: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "meetings" / meetingIdStr => EndpointHelpers.withUserAndBank(req) { (user, bank, cc) => for { @@ -1116,7 +1116,7 @@ object Http4s310 { // ─── getServerJWK ──────────────────────────────────────────────────────── - val getServerJWK: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getServerJWK: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "certs" => EndpointHelpers.executeAndRespond(req) { _ => Future.successful(com.openbankproject.commons.util.JsonAliases.parse(CertificateUtil.convertRSAPublicKeyToAnRSAJWK())) @@ -1143,7 +1143,7 @@ object Http4s310 { // ─── getOAuth2ServerJWKsURIs ───────────────────────────────────────────── - val getOAuth2ServerJWKsURIs: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getOAuth2ServerJWKsURIs: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "jwks-uris" => EndpointHelpers.executeAndRespond(req) { _ => Future.successful(JSONFactory310.getOAuth2ServerJwksUrisJson()) @@ -1170,7 +1170,7 @@ object Http4s310 { // ─── getMethodRoutings ─────────────────────────────────────────────────── - val getMethodRoutings: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getMethodRoutings: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "management" / "method_routings" => EndpointHelpers.withUser(req) { (user, cc) => val methodNameParam = req.uri.query.params.get("method_name").map(Full(_)).getOrElse(net.liftweb.common.Empty) @@ -1242,7 +1242,7 @@ object Http4s310 { // (non-standard ALL_CAPS) to make middleware skip view validation; we look up via // ViewNewStyle.systemView inline. - val getSystemView: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getSystemView: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "system-views" / viewIdStr => EndpointHelpers.withUser(req) { (user, cc) => for { @@ -1277,7 +1277,7 @@ object Http4s310 { // ─── getCardsForBank ───────────────────────────────────────────────────── - val getCardsForBank: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getCardsForBank: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "management" / "banks" / _ / "cards" => EndpointHelpers.withUserAndBank(req) { (user, bank, cc) => val httpParams = req.headers.headers.toList.map(h => HTTPParam(h.name.toString, h.value)) ::: @@ -1316,7 +1316,7 @@ object Http4s310 { // ─── getCardForBank ────────────────────────────────────────────────────── - val getCardForBank: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getCardForBank: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "management" / "banks" / _ / "cards" / cardIdStr => EndpointHelpers.withUserAndBank(req) { (user, bank, cc) => for { @@ -1354,7 +1354,7 @@ object Http4s310 { // ─── getBankAccountsBalances ───────────────────────────────────────────── - val getBankAccountsBalances: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getBankAccountsBalances: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "balances" => EndpointHelpers.withUserAndBank(req) { (user, bank, cc) => for { @@ -1376,7 +1376,7 @@ object Http4s310 { // ─── checkFundsAvailable ───────────────────────────────────────────────── - val checkFundsAvailable: HttpRoutes[IO] = HttpRoutes.of[IO] { + val checkFundsAvailable: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "accounts" / _ / _ / "funds-available" => EndpointHelpers.withView(req) { (user, account, view, cc) => val amountKey = "amount" @@ -1437,7 +1437,7 @@ object Http4s310 { // ─── getTransactionByIdForBankAccount ──────────────────────────────────── - val getTransactionByIdForBankAccount: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getTransactionByIdForBankAccount: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "accounts" / _ / _ / "transactions" / transactionIdStr / "transaction" => EndpointHelpers.withView(req) { (user, account, view, cc) => for { @@ -1482,7 +1482,7 @@ object Http4s310 { // ─── getTransactionRequests ────────────────────────────────────────────── - val getTransactionRequests: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getTransactionRequests: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "accounts" / _ / viewIdStr / "transaction-requests" => EndpointHelpers.withBankAccount(req) { (user, account, cc) => for { @@ -1542,7 +1542,7 @@ object Http4s310 { // Conditional auth: middleware uses `userAuthenticationMessage(!getProductsIsPublic)` // in description to drive needsAuthentication. When public, anonymous is OK. - val getProduct: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getProduct: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / bankIdStr / "products" / productCodeStr => EndpointHelpers.executeAndRespond(req) { cc => for { @@ -1584,7 +1584,7 @@ object Http4s310 { // ─── getProductTree ────────────────────────────────────────────────────── - val getProductTree: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getProductTree: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / bankIdStr / "product-tree" / productCodeStr => EndpointHelpers.executeAndRespond(req) { cc => for { @@ -1630,7 +1630,7 @@ object Http4s310 { // ─── getProducts ───────────────────────────────────────────────────────── - val getProducts: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getProducts: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / bankIdStr / "products" => EndpointHelpers.executeAndRespond(req) { cc => val params = req.uri.query.multiParams.toList.map { case (k, vs) => GetProductsParam(k, vs.toList) } @@ -1669,7 +1669,7 @@ object Http4s310 { // ─── getProductCollection ──────────────────────────────────────────────── - val getProductCollection: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getProductCollection: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "product-collections" / collectionCodeStr => EndpointHelpers.withUserAndBank(req) { (_, bank, cc) => for { @@ -1698,7 +1698,7 @@ object Http4s310 { // ─── getConsents ───────────────────────────────────────────────────────── - val getConsents: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getConsents: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "my" / "consents" => EndpointHelpers.withUserAndBank(req) { (user, bank, _) => val params = req.uri.query.params @@ -1749,7 +1749,7 @@ object Http4s310 { // ─── getPrivateAccountByIdFull ─────────────────────────────────────────── - val getPrivateAccountByIdFull: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getPrivateAccountByIdFull: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "accounts" / _ / _ / "account" => EndpointHelpers.withView(req) { (user, account, view, cc) => for { @@ -1797,7 +1797,7 @@ object Http4s310 { // ─── getWebUiProps ─────────────────────────────────────────────────────── - val getWebUiProps: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getWebUiProps: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "management" / "webui_props" => EndpointHelpers.withUser(req) { (user, cc) => val activeRaw = req.uri.query.params.getOrElse("active", "false") @@ -1872,7 +1872,7 @@ object Http4s310 { // ─── deleteUserAuthContexts ────────────────────────────────────────────── - val deleteUserAuthContexts: HttpRoutes[IO] = HttpRoutes.of[IO] { + val deleteUserAuthContexts: Http4sRoute = Http4sRoute { case req @ DELETE -> `prefixPath` / "users" / userIdStr / "auth-context" => EndpointHelpers.withUser(req) { (user, cc) => for { @@ -1905,7 +1905,7 @@ object Http4s310 { // ─── deleteUserAuthContextById ────────────────────────────────────────── - val deleteUserAuthContextById: HttpRoutes[IO] = HttpRoutes.of[IO] { + val deleteUserAuthContextById: Http4sRoute = Http4sRoute { case req @ DELETE -> `prefixPath` / "users" / userIdStr / "auth-context" / userAuthContextIdStr => EndpointHelpers.withUser(req) { (user, cc) => for { @@ -1938,7 +1938,7 @@ object Http4s310 { // ─── deleteTaxResidence ────────────────────────────────────────────────── - val deleteTaxResidence: HttpRoutes[IO] = HttpRoutes.of[IO] { + val deleteTaxResidence: Http4sRoute = Http4sRoute { case req @ DELETE -> `prefixPath` / "banks" / _ / "customers" / customerIdStr / "tax_residencies" / taxResidenceIdStr => EndpointHelpers.withUserAndBank(req) { (user, bank, cc) => for { @@ -1971,7 +1971,7 @@ object Http4s310 { // ─── deleteCustomerAddress ─────────────────────────────────────────────── - val deleteCustomerAddress: HttpRoutes[IO] = HttpRoutes.of[IO] { + val deleteCustomerAddress: Http4sRoute = Http4sRoute { case req @ DELETE -> `prefixPath` / "banks" / _ / "customers" / customerIdStr / "addresses" / customerAddressIdStr => EndpointHelpers.withUserAndBank(req) { (user, bank, cc) => for { @@ -2005,7 +2005,7 @@ object Http4s310 { // ─── deleteProductAttribute ────────────────────────────────────────────── // Note: this DELETE returns 204 (matches original v3.1.0 behavior). - val deleteProductAttribute: HttpRoutes[IO] = HttpRoutes.of[IO] { + val deleteProductAttribute: Http4sRoute = Http4sRoute { case req @ DELETE -> `prefixPath` / "banks" / bankIdStr / "products" / _ / "attributes" / productAttributeIdStr => EndpointHelpers.withUserDelete(req) { (user, cc) => for { @@ -2045,7 +2045,7 @@ object Http4s310 { // ─── deleteBranch ──────────────────────────────────────────────────────── - val deleteBranch: HttpRoutes[IO] = HttpRoutes.of[IO] { + val deleteBranch: Http4sRoute = Http4sRoute { case req @ DELETE -> `prefixPath` / "banks" / _ / "branches" / branchIdStr => EndpointHelpers.withUserAndBank(req) { (user, bank, cc) => val allowedEntitlements = canDeleteBranch :: canDeleteBranchAtAnyBank :: Nil @@ -2080,7 +2080,7 @@ object Http4s310 { // ─── deleteSystemView ──────────────────────────────────────────────────── - val deleteSystemView: HttpRoutes[IO] = HttpRoutes.of[IO] { + val deleteSystemView: Http4sRoute = Http4sRoute { case req @ DELETE -> `prefixPath` / "system-views" / viewIdStr => EndpointHelpers.withUser(req) { (user, cc) => for { @@ -2103,7 +2103,7 @@ object Http4s310 { // ─── deleteMethodRouting ───────────────────────────────────────────────── - val deleteMethodRouting: HttpRoutes[IO] = HttpRoutes.of[IO] { + val deleteMethodRouting: Http4sRoute = Http4sRoute { case req @ DELETE -> `prefixPath` / "management" / "method_routings" / methodRoutingIdStr => EndpointHelpers.withUser(req) { (user, cc) => for { @@ -2137,7 +2137,7 @@ object Http4s310 { // ─── deleteCardForBank ─────────────────────────────────────────────────── // Note: original v3.1.0 returns 204 — use withUserAndBankDelete. - val deleteCardForBank: HttpRoutes[IO] = HttpRoutes.of[IO] { + val deleteCardForBank: Http4sRoute = Http4sRoute { case req @ DELETE -> `prefixPath` / "management" / "banks" / _ / "cards" / cardIdStr => EndpointHelpers.withUserAndBankDelete(req) { (user, bank, cc) => for { @@ -2167,7 +2167,7 @@ object Http4s310 { // ─── deleteWebUiProps ──────────────────────────────────────────────────── - val deleteWebUiProps: HttpRoutes[IO] = HttpRoutes.of[IO] { + val deleteWebUiProps: Http4sRoute = Http4sRoute { case req @ DELETE -> `prefixPath` / "management" / "webui_props" / webUiPropsIdStr => EndpointHelpers.withUser(req) { (user, cc) => for { @@ -2202,7 +2202,7 @@ object Http4s310 { // ─── revokeConsent ─────────────────────────────────────────────────────── // Routed as GET in Lift — keep matching shape. - val revokeConsent: HttpRoutes[IO] = HttpRoutes.of[IO] { + val revokeConsent: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "my" / "consents" / consentIdStr / "revoke" => EndpointHelpers.withUserAndBank(req) { (user, _, cc) => for { @@ -2246,7 +2246,7 @@ object Http4s310 { // ─── createTaxResidence (POST) ─────────────────────────────────────────── - val createTaxResidence: HttpRoutes[IO] = HttpRoutes.of[IO] { + val createTaxResidence: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "banks" / _ / "customers" / customerIdStr / "tax-residence" => EndpointHelpers.withUserAndBankAndBodyCreated[PostTaxResidenceJsonV310, Any](req) { (user, bank, postedData, cc) => for { @@ -2280,7 +2280,7 @@ object Http4s310 { // ─── createCustomerAddress (POST) ──────────────────────────────────────── - val createCustomerAddress: HttpRoutes[IO] = HttpRoutes.of[IO] { + val createCustomerAddress: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "banks" / _ / "customers" / customerIdStr / "address" => EndpointHelpers.withUserAndBankAndBodyCreated[PostCustomerAddressJsonV310, Any](req) { (user, bank, postedData, cc) => for { @@ -2320,7 +2320,7 @@ object Http4s310 { // ─── updateCustomerAddress (PUT) ───────────────────────────────────────── - val updateCustomerAddress: HttpRoutes[IO] = HttpRoutes.of[IO] { + val updateCustomerAddress: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "banks" / _ / "customers" / customerIdStr / "addresses" / customerAddressIdStr => EndpointHelpers.withUserAndBankAndBody[PostCustomerAddressJsonV310, Any](req) { (user, bank, postedData, cc) => for { @@ -2360,7 +2360,7 @@ object Http4s310 { // ─── createUserAuthContext (POST) ──────────────────────────────────────── - val createUserAuthContext: HttpRoutes[IO] = HttpRoutes.of[IO] { + val createUserAuthContext: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "users" / userIdStr / "auth-context" => EndpointHelpers.withUserAndBodyCreated[PostUserAuthContextJson, Any](req) { (user, postedData, cc) => for { @@ -2392,7 +2392,7 @@ object Http4s310 { // ─── createProductAttribute (POST) ─────────────────────────────────────── - val createProductAttribute: HttpRoutes[IO] = HttpRoutes.of[IO] { + val createProductAttribute: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "banks" / bankIdStr / "products" / productCodeStr / "attribute" => EndpointHelpers.withUserAndBodyCreated[ProductAttributeJson, Any](req) { (user, postedData, cc) => for { @@ -2459,7 +2459,7 @@ object Http4s310 { | |This functionality is work in progress! Please note that only implemented trigger is: ${ApiTrigger.onBalanceChange}""" - val createAccountWebhook: HttpRoutes[IO] = HttpRoutes.of[IO] { + val createAccountWebhook: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "banks" / _ / "account-web-hooks" => EndpointHelpers.withUserAndBankAndBodyCreated[AccountWebhookPostJson, Any](req) { (user, bank, postJson, cc) => for { @@ -2504,7 +2504,7 @@ object Http4s310 { // ─── unlockUser (PUT) ──────────────────────────────────────────────────── - val unlockUser: HttpRoutes[IO] = HttpRoutes.of[IO] { + val unlockUser: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "users" / username / "lock-status" => EndpointHelpers.withUser(req) { (user, cc) => for { @@ -2546,7 +2546,7 @@ object Http4s310 { // ─── callsLimit (PUT) ──────────────────────────────────────────────────── - val callsLimit: HttpRoutes[IO] = HttpRoutes.of[IO] { + val callsLimit: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "management" / "consumers" / consumerIdStr / "consumer" / "call-limits" => EndpointHelpers.withUserAndBody[CallLimitPostJson, Any](req) { (user, postJson, cc) => for { @@ -2601,7 +2601,7 @@ object Http4s310 { // ─── enableDisableAccountWebhook (PUT) ─────────────────────────────────── - val enableDisableAccountWebhook: HttpRoutes[IO] = HttpRoutes.of[IO] { + val enableDisableAccountWebhook: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "banks" / _ / "account-web-hooks" => EndpointHelpers.withUserAndBankAndBody[AccountWebhookPutJson, Any](req) { (user, bank, putJson, cc) => for { @@ -2641,7 +2641,7 @@ object Http4s310 { // ─── enableDisableConsumers (PUT) ──────────────────────────────────────── - val enableDisableConsumers: HttpRoutes[IO] = HttpRoutes.of[IO] { + val enableDisableConsumers: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "management" / "consumers" / consumerIdStr => EndpointHelpers.withUserAndBody[PutEnabledJSON, Any](req) { (user, putData, cc) => for { @@ -2671,7 +2671,7 @@ object Http4s310 { // ─── updateSystemView (PUT) ────────────────────────────────────────────── - val updateSystemView: HttpRoutes[IO] = HttpRoutes.of[IO] { + val updateSystemView: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "system-views" / viewIdStr => EndpointHelpers.withUserAndBody[UpdateViewJSON, Any](req) { (user, updateJson, cc) => for { @@ -2707,7 +2707,7 @@ object Http4s310 { // ─── updateProductAttribute (PUT) ──────────────────────────────────────── - val updateProductAttribute: HttpRoutes[IO] = HttpRoutes.of[IO] { + val updateProductAttribute: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "banks" / bankIdStr / "products" / productCodeStr / "attributes" / productAttributeIdStr => EndpointHelpers.withUserAndBody[ProductAttributeJson, Any](req) { (user, postedData, cc) => for { @@ -2751,7 +2751,7 @@ object Http4s310 { // ─── updateCustomerEmail (PUT) ─────────────────────────────────────────── - val updateCustomerEmail: HttpRoutes[IO] = HttpRoutes.of[IO] { + val updateCustomerEmail: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "banks" / _ / "customers" / customerIdStr / "email" => EndpointHelpers.withUserAndBankAndBody[PutUpdateCustomerEmailJsonV310, Any](req) { (user, bank, putData, cc) => for { @@ -2785,7 +2785,7 @@ object Http4s310 { // ─── updateCustomerNumber (PUT) ────────────────────────────────────────── - val updateCustomerNumber: HttpRoutes[IO] = HttpRoutes.of[IO] { + val updateCustomerNumber: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "banks" / _ / "customers" / customerIdStr / "number" => EndpointHelpers.withUserAndBankAndBody[PutUpdateCustomerNumberJsonV310, Any](req) { (user, bank, putData, cc) => for { @@ -2823,7 +2823,7 @@ object Http4s310 { // ─── updateCustomerMobileNumber (PUT) ──────────────────────────────────── - val updateCustomerMobileNumber: HttpRoutes[IO] = HttpRoutes.of[IO] { + val updateCustomerMobileNumber: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "banks" / _ / "customers" / customerIdStr / "mobile-number" => EndpointHelpers.withUserAndBankAndBody[PutUpdateCustomerMobilePhoneNumberJsonV310, Any](req) { (user, bank, putData, cc) => for { @@ -2857,7 +2857,7 @@ object Http4s310 { // ─── updateCustomerIdentity (PUT) ──────────────────────────────────────── - val updateCustomerIdentity: HttpRoutes[IO] = HttpRoutes.of[IO] { + val updateCustomerIdentity: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "banks" / _ / "customers" / customerIdStr / "identity" => EndpointHelpers.withUserAndBankAndBody[PutUpdateCustomerIdentityJsonV310, Any](req) { (user, bank, putData, cc) => for { @@ -2895,7 +2895,7 @@ object Http4s310 { // ─── updateCustomerCreditLimit (PUT) ───────────────────────────────────── - val updateCustomerCreditLimit: HttpRoutes[IO] = HttpRoutes.of[IO] { + val updateCustomerCreditLimit: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "banks" / _ / "customers" / customerIdStr / "credit-limit" => EndpointHelpers.withUserAndBankAndBody[PutUpdateCustomerCreditLimitJsonV310, Any](req) { (user, bank, putData, cc) => for { @@ -2929,7 +2929,7 @@ object Http4s310 { // ─── updateCustomerCreditRatingAndSource (PUT) ─────────────────────────── - val updateCustomerCreditRatingAndSource: HttpRoutes[IO] = HttpRoutes.of[IO] { + val updateCustomerCreditRatingAndSource: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "banks" / _ / "customers" / customerIdStr / "credit-rating-and-source" => EndpointHelpers.withUserAndBankAndBody[PutUpdateCustomerCreditRatingAndSourceJsonV310, Any](req) { (user, bank, putData, cc) => for { @@ -2964,7 +2964,7 @@ object Http4s310 { // ─── updateCustomerBranch (PUT) ────────────────────────────────────────── - val updateCustomerBranch: HttpRoutes[IO] = HttpRoutes.of[IO] { + val updateCustomerBranch: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "banks" / _ / "customers" / customerIdStr / "branch" => EndpointHelpers.withUserAndBankAndBody[PutUpdateCustomerBranchJsonV310, Any](req) { (user, bank, putData, cc) => for { @@ -3000,7 +3000,7 @@ object Http4s310 { // ─── updateCustomerData (PUT) ──────────────────────────────────────────── - val updateCustomerData: HttpRoutes[IO] = HttpRoutes.of[IO] { + val updateCustomerData: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "banks" / _ / "customers" / customerIdStr / "data" => EndpointHelpers.withUserAndBankAndBody[PutUpdateCustomerDataJsonV310, Any](req) { (user, bank, putData, cc) => for { @@ -3045,7 +3045,7 @@ object Http4s310 { // APPLICANT (the application's user) becomes its holder. The Lift implementation // (and its verbatim port) made the logged-in approver the holder — fixed 2026-09. - val updateAccountApplicationStatus: HttpRoutes[IO] = HttpRoutes.of[IO] { + val updateAccountApplicationStatus: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "banks" / _ / "account-applications" / accountApplicationIdStr => EndpointHelpers.withUserAndBankAndBody[AccountApplicationUpdateStatusJson, Any](req) { (user, bank, putJson, cc) => for { @@ -3112,7 +3112,7 @@ object Http4s310 { // ─── createCustomer (POST) ─────────────────────────────────────────────── - val createCustomer: HttpRoutes[IO] = HttpRoutes.of[IO] { + val createCustomer: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "banks" / _ / "customers" => EndpointHelpers.withUserAndBankAndBodyCreated[PostCustomerJsonV310, Any](req) { (user, bank, postedData, cc) => for { @@ -3173,7 +3173,7 @@ object Http4s310 { // ─── getCustomerByCustomerNumber (POST → 200) ──────────────────────────── - val getCustomerByCustomerNumber: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getCustomerByCustomerNumber: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "banks" / _ / "customers" / "customer-number" => EndpointHelpers.withUserAndBankAndBody[PostCustomerNumberJsonV310, Any](req) { (user, bank, postedData, cc) => for { @@ -3207,7 +3207,7 @@ object Http4s310 { // ─── createAccountApplication (POST) ───────────────────────────────────── - val createAccountApplication: HttpRoutes[IO] = HttpRoutes.of[IO] { + val createAccountApplication: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "banks" / _ / "account-applications" => EndpointHelpers.withUserAndBankAndBodyCreated[AccountApplicationJson, Any](req) { (_, bank, postedData, cc) => for { @@ -3255,7 +3255,7 @@ object Http4s310 { // ─── createAccountAttribute (POST) ─────────────────────────────────────── - val createAccountAttribute: HttpRoutes[IO] = HttpRoutes.of[IO] { + val createAccountAttribute: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "banks" / bankIdStr / "accounts" / accountIdStr / "products" / productCodeStr / "attribute" => EndpointHelpers.withUserAndBodyCreated[AccountAttributeJson, Any](req) { (user, postedData, cc) => for { @@ -3313,7 +3313,7 @@ object Http4s310 { // ─── updateAccountAttribute (PUT) ──────────────────────────────────────── - val updateAccountAttribute: HttpRoutes[IO] = HttpRoutes.of[IO] { + val updateAccountAttribute: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "banks" / bankIdStr / "accounts" / accountIdStr / "products" / productCodeStr / "attributes" / accountAttributeIdStr => EndpointHelpers.withUserAndBodyCreated[AccountAttributeJson, Any](req) { (user, postedData, cc) => for { @@ -3370,7 +3370,7 @@ object Http4s310 { // ─── createMeeting (POST) ──────────────────────────────────────────────── - val createMeeting: HttpRoutes[IO] = HttpRoutes.of[IO] { + val createMeeting: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "banks" / _ / "meetings" => // Manual body parsing to preserve Lift's error message format: // "$InvalidJsonFormat The Json body should be the $CreateMeetingJson " @@ -3424,7 +3424,7 @@ object Http4s310 { // ─── createSystemView (POST) ───────────────────────────────────────────── - val createSystemView: HttpRoutes[IO] = HttpRoutes.of[IO] { + val createSystemView: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "system-views" => EndpointHelpers.withUserAndBodyCreated[CreateViewJsonV300, Any](req) { (user, createViewJson, cc) => for { @@ -3474,7 +3474,7 @@ object Http4s310 { // ─── createProductCollection (PUT — "Create or Update") ────────────────── - val createProductCollection: HttpRoutes[IO] = HttpRoutes.of[IO] { + val createProductCollection: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "banks" / _ / "product-collections" / collectionCodeStr => EndpointHelpers.withUserAndBankAndBodyCreated[PutProductCollectionsV310, Any](req) { (user, bank, product, cc) => for { @@ -3530,7 +3530,7 @@ object Http4s310 { // ─── addCardForBank (POST) ─────────────────────────────────────────────── - val addCardForBank: HttpRoutes[IO] = HttpRoutes.of[IO] { + val addCardForBank: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "management" / "banks" / _ / "cards" => EndpointHelpers.withUserAndBankAndBodyCreated[CreatePhysicalCardJsonV310, Any](req) { (user, bank, postJson, cc) => for { @@ -3610,7 +3610,7 @@ object Http4s310 { // ─── updatedCardForBank (PUT) ──────────────────────────────────────────── - val updatedCardForBank: HttpRoutes[IO] = HttpRoutes.of[IO] { + val updatedCardForBank: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "management" / "banks" / _ / "cards" / cardIdStr => EndpointHelpers.withUserAndBankAndBody[UpdatePhysicalCardJsonV310, Any](req) { (user, bank, postJson, cc) => for { @@ -3680,7 +3680,7 @@ object Http4s310 { // ─── createCardAttribute (POST) ────────────────────────────────────────── - val createCardAttribute: HttpRoutes[IO] = HttpRoutes.of[IO] { + val createCardAttribute: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "management" / "banks" / bankIdStr / "cards" / cardIdStr / "attribute" => EndpointHelpers.executeFutureWithBodyCreated[CardAttributeJson, Any](req) { (postedData, cc) => for { @@ -3734,7 +3734,7 @@ object Http4s310 { // ─── updateCardAttribute (PUT) ─────────────────────────────────────────── - val updateCardAttribute: HttpRoutes[IO] = HttpRoutes.of[IO] { + val updateCardAttribute: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "management" / "banks" / bankIdStr / "cards" / cardIdStr / "attributes" / cardAttributeIdStr => EndpointHelpers.executeFutureWithBody[CardAttributeJson, Any](req) { (postedData, cc) => for { @@ -3787,7 +3787,7 @@ object Http4s310 { // ─── createWebUiProps (POST) ───────────────────────────────────────────── - val createWebUiProps: HttpRoutes[IO] = HttpRoutes.of[IO] { + val createWebUiProps: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "management" / "webui_props" => EndpointHelpers.withUserAndBodyCreated[WebUiPropsCommons, Any](req) { (user, postedData, cc) => for { @@ -3859,7 +3859,7 @@ object Http4s310 { // ─── createUserAuthContextUpdateRequest (POST) ─────────────────────────── - val createUserAuthContextUpdateRequest: HttpRoutes[IO] = HttpRoutes.of[IO] { + val createUserAuthContextUpdateRequest: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "banks" / _ / "users" / "current" / "auth-context-updates" / scaMethod => EndpointHelpers.withUserAndBankAndBodyCreated[PostUserAuthContextJson, Any](req) { (user, bank, postedData, cc) => for { @@ -3900,7 +3900,7 @@ object Http4s310 { // ─── answerUserAuthContextUpdateChallenge (POST → 200) ─────────────────── - val answerUserAuthContextUpdateChallenge: HttpRoutes[IO] = HttpRoutes.of[IO] { + val answerUserAuthContextUpdateChallenge: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "banks" / bankIdStr / "users" / "current" / "auth-context-updates" / authContextUpdateIdStr / "challenge" => EndpointHelpers.executeFutureWithBody[PostUserAuthContextUpdateJsonV310, Any](req) { (postBody, cc) => for { @@ -3942,7 +3942,7 @@ object Http4s310 { // ─── refreshUser (POST) ────────────────────────────────────────────────── - val refreshUser: HttpRoutes[IO] = HttpRoutes.of[IO] { + val refreshUser: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "users" / userIdStr / "refresh" => // Lift returns 201 (CREATED) for this POST — middleware has already validated auth // and the canRefreshUser role, so we use executeFutureCreated to preserve the status. @@ -3981,7 +3981,7 @@ object Http4s310 { // ─── createProduct (PUT — "Create or Update") ──────────────────────────── - val createProduct: HttpRoutes[IO] = HttpRoutes.of[IO] { + val createProduct: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "banks" / bankIdStr / "products" / productCodeStr => EndpointHelpers.withUserAndBankAndBodyCreated[PostPutProductJsonV310, Any](req) { (user, bank, product, cc) => for { @@ -4041,7 +4041,7 @@ object Http4s310 { // ─── createMethodRouting (POST) ────────────────────────────────────────── - val createMethodRouting: HttpRoutes[IO] = HttpRoutes.of[IO] { + val createMethodRouting: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "management" / "method_routings" => EndpointHelpers.withUserAndBodyCreated[MethodRoutingCommons, Any](req) { (user, raw, cc) => for { @@ -4140,7 +4140,7 @@ object Http4s310 { // ─── updateMethodRouting (PUT) ─────────────────────────────────────────── - val updateMethodRouting: HttpRoutes[IO] = HttpRoutes.of[IO] { + val updateMethodRouting: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "management" / "method_routings" / methodRoutingIdStr => EndpointHelpers.withUserAndBody[MethodRoutingCommons, Any](req) { (user, raw, cc) => for { @@ -4233,7 +4233,7 @@ object Http4s310 { // ─── updateAccount (PUT) ───────────────────────────────────────────────── - val updateAccount: HttpRoutes[IO] = HttpRoutes.of[IO] { + val updateAccount: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "management" / "banks" / _ / "accounts" / accountIdStr => EndpointHelpers.withUserAndBankAndBody[UpdateAccountRequestJsonV310, Any](req) { (user, bank, body, cc) => for { @@ -4291,7 +4291,7 @@ object Http4s310 { // (403 on missing). booleanToFuture is used to enforce 403, matching CLAUDE.md // guidance for conditional roles. - val createAccount: HttpRoutes[IO] = HttpRoutes.of[IO] { + val createAccount: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "banks" / bankIdStr / "accounts" / accountIdStr => EndpointHelpers.withUserAndBankAndBodyCreated[CreateAccountRequestJsonV310, Any](req) { (user, bank, body, cc) => for { @@ -4380,7 +4380,7 @@ object Http4s310 { // ─── createConsent (POST) ──────────────────────────────────────────────── - val createConsent: HttpRoutes[IO] = HttpRoutes.of[IO] { + val createConsent: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "banks" / bankIdStr / "my" / "consents" / scaMethod => EndpointHelpers.withUserAndBodyCreated[PostConsentBodyCommonJson, Any](req) { (user, consentJson, cc) => val raw = cc.httpBody.getOrElse("") @@ -4500,10 +4500,9 @@ object Http4s310 { // Lift registered three separate endpoints with concrete SCA-method URL segments. // Preserve those names so FrozenClassTest (STABLE API contract) stays green. - val createConsentEmail: HttpRoutes[IO] = createConsent - val createConsentSms: HttpRoutes[IO] = createConsent - val createConsentImplicit: HttpRoutes[IO] = createConsent - + val createConsentEmail: Http4sRoute = createConsent + val createConsentSms: Http4sRoute = createConsent + val createConsentImplicit: Http4sRoute = createConsent resourceDocs += ResourceDoc( implementedInApiVersion, nameOf(createConsentEmail), @@ -4753,7 +4752,7 @@ object Http4s310 { // ─── answerConsentChallenge (POST → 201) ───────────────────────────────── - val answerConsentChallenge: HttpRoutes[IO] = HttpRoutes.of[IO] { + val answerConsentChallenge: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "banks" / _ / "consents" / consentIdStr / "challenge" => EndpointHelpers.withUserAndBankAndBodyCreated[PostConsentChallengeJsonV310, Any](req) { (_, _, body, cc) => for { @@ -4797,7 +4796,7 @@ object Http4s310 { // ─── getObpConnectorLoopback ───────────────────────────────────────────── - val getObpConnectorLoopback: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getObpConnectorLoopback: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "connector" / "loopback" => EndpointHelpers.executeAndRespond(req) { cc => for { @@ -4864,7 +4863,7 @@ object Http4s310 { // ─── saveHistoricalTransaction (POST) ──────────────────────────────────── - val saveHistoricalTransaction: HttpRoutes[IO] = HttpRoutes.of[IO] { + val saveHistoricalTransaction: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "management" / "historical" / "transactions" => EndpointHelpers.withUserAndBodyCreated[PostHistoricalTransactionJson, Any](req) { (user, body, cc) => for { @@ -5028,109 +5027,115 @@ object Http4s310 { // ─── allRoutes ──────────────────────────────────────────────────────────── - private val allOwnRoutes: HttpRoutes[IO] = Kleisli[HttpF, Request[IO], Response[IO]] { req => - root.run(req) - .orElse(getCheckbookOrders.run(req)) - .orElse(getStatusOfCreditCardOrder.run(req)) - .orElse(getTopAPIs.run(req)) - .orElse(getMetricsTopConsumers.run(req)) - .orElse(getFirehoseCustomers.run(req)) - .orElse(getBadLoginStatus.run(req)) - .orElse(getCallsLimit.run(req)) - .orElse(getConsumer.run(req)) - .orElse(getConsumersForCurrentUser.run(req)) - .orElse(getConsumers.run(req)) - .orElse(getAccountWebhooks.run(req)) - .orElse(config.run(req)) - .orElse(getAdapterInfo.run(req)) - .orElse(getRateLimitingInfo.run(req)) - .orElse(getCustomerByCustomerId.run(req)) - .orElse(getUserAuthContexts.run(req)) - .orElse(getTaxResidence.run(req)) - .orElse(getAllEntitlements.run(req)) - .orElse(getCustomerAddresses.run(req)) - .orElse(getProductAttribute.run(req)) - .orElse(getAccountApplications.run(req)) - .orElse(getAccountApplication.run(req)) - .orElse(getMeetings.run(req)) - .orElse(getMeeting.run(req)) - .orElse(getServerJWK.run(req)) - .orElse(getOAuth2ServerJWKsURIs.run(req)) - .orElse(getMethodRoutings.run(req)) - .orElse(getSystemView.run(req)) - .orElse(getCardsForBank.run(req)) - .orElse(getCardForBank.run(req)) - .orElse(getBankAccountsBalances.run(req)) - .orElse(checkFundsAvailable.run(req)) - .orElse(getTransactionByIdForBankAccount.run(req)) - .orElse(getTransactionRequests.run(req)) - .orElse(getProduct.run(req)) - .orElse(getProductTree.run(req)) - .orElse(getProducts.run(req)) - .orElse(getProductCollection.run(req)) - .orElse(getConsents.run(req)) - .orElse(getPrivateAccountByIdFull.run(req)) - .orElse(getWebUiProps.run(req)) - .orElse(deleteUserAuthContexts.run(req)) - .orElse(deleteUserAuthContextById.run(req)) - .orElse(deleteTaxResidence.run(req)) - .orElse(deleteCustomerAddress.run(req)) - .orElse(deleteProductAttribute.run(req)) - .orElse(deleteBranch.run(req)) - .orElse(deleteSystemView.run(req)) - .orElse(deleteMethodRouting.run(req)) - .orElse(deleteCardForBank.run(req)) - .orElse(deleteWebUiProps.run(req)) - .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)) - .orElse(unlockUser.run(req)) - .orElse(callsLimit.run(req)) - .orElse(enableDisableAccountWebhook.run(req)) - .orElse(enableDisableConsumers.run(req)) - .orElse(updateSystemView.run(req)) - .orElse(updateProductAttribute.run(req)) - .orElse(updateCustomerEmail.run(req)) - .orElse(updateCustomerNumber.run(req)) - .orElse(updateCustomerMobileNumber.run(req)) - .orElse(updateCustomerIdentity.run(req)) - .orElse(updateCustomerCreditLimit.run(req)) - .orElse(updateCustomerCreditRatingAndSource.run(req)) - .orElse(updateCustomerBranch.run(req)) - .orElse(updateCustomerData.run(req)) - .orElse(updateAccountApplicationStatus.run(req)) - .orElse(createCustomer.run(req)) - .orElse(getCustomerByCustomerNumber.run(req)) - .orElse(createAccountApplication.run(req)) - .orElse(createAccountAttribute.run(req)) - .orElse(updateAccountAttribute.run(req)) - .orElse(createMeeting.run(req)) - .orElse(createSystemView.run(req)) - .orElse(createProductCollection.run(req)) - .orElse(addCardForBank.run(req)) - .orElse(updatedCardForBank.run(req)) - .orElse(createCardAttribute.run(req)) - .orElse(updateCardAttribute.run(req)) - .orElse(createWebUiProps.run(req)) - .orElse(createUserAuthContextUpdateRequest.run(req)) - .orElse(answerUserAuthContextUpdateChallenge.run(req)) - .orElse(refreshUser.run(req)) - .orElse(createProduct.run(req)) - .orElse(createMethodRouting.run(req)) - .orElse(updateMethodRouting.run(req)) - .orElse(updateAccount.run(req)) - .orElse(createAccount.run(req)) - .orElse(createConsent.run(req)) - .orElse(answerConsentChallenge.run(req)) - .orElse(saveHistoricalTransaction.run(req)) - .orElse(getObpConnectorLoopback.run(req)) - } + // The routes in the order they are tried. ResourceDocMiddleware selects the doc of the first + // route that serves a request, so it is given the docs in this same order. + lazy val routesInOrder: List[Http4sHandler] = List( + root, + getCheckbookOrders, + getStatusOfCreditCardOrder, + getTopAPIs, + getMetricsTopConsumers, + getFirehoseCustomers, + getBadLoginStatus, + getCallsLimit, + getConsumer, + getConsumersForCurrentUser, + getConsumers, + getAccountWebhooks, + config, + getAdapterInfo, + getRateLimitingInfo, + getCustomerByCustomerId, + getUserAuthContexts, + getTaxResidence, + getAllEntitlements, + getCustomerAddresses, + getProductAttribute, + getAccountApplications, + getAccountApplication, + getMeetings, + getMeeting, + getServerJWK, + getOAuth2ServerJWKsURIs, + getMethodRoutings, + getSystemView, + getCardsForBank, + getCardForBank, + getBankAccountsBalances, + checkFundsAvailable, + getTransactionByIdForBankAccount, + getTransactionRequests, + getProduct, + getProductTree, + getProducts, + getProductCollection, + getConsents, + getPrivateAccountByIdFull, + getWebUiProps, + deleteUserAuthContexts, + deleteUserAuthContextById, + deleteTaxResidence, + deleteCustomerAddress, + deleteProductAttribute, + deleteBranch, + deleteSystemView, + deleteMethodRouting, + deleteCardForBank, + deleteWebUiProps, + revokeConsent, + createTaxResidence, + createCustomerAddress, + updateCustomerAddress, + createUserAuthContext, + createProductAttribute, + createAccountWebhook, + unlockUser, + callsLimit, + enableDisableAccountWebhook, + enableDisableConsumers, + updateSystemView, + updateProductAttribute, + updateCustomerEmail, + updateCustomerNumber, + updateCustomerMobileNumber, + updateCustomerIdentity, + updateCustomerCreditLimit, + updateCustomerCreditRatingAndSource, + updateCustomerBranch, + updateCustomerData, + updateAccountApplicationStatus, + createCustomer, + getCustomerByCustomerNumber, + createAccountApplication, + createAccountAttribute, + updateAccountAttribute, + createMeeting, + createSystemView, + createProductCollection, + addCardForBank, + updatedCardForBank, + createCardAttribute, + updateCardAttribute, + createWebUiProps, + createUserAuthContextUpdateRequest, + answerUserAuthContextUpdateChallenge, + refreshUser, + createProduct, + createMethodRouting, + updateMethodRouting, + updateAccount, + createAccount, + createConsent, + answerConsentChallenge, + saveHistoricalTransaction, + getObpConnectorLoopback + ) + + lazy val orderedResourceDocs: ArrayBuffer[ResourceDoc] = ResourceDocMatcher.orderByRoutes(resourceDocs, routesInOrder) + + private lazy val allOwnRoutes: HttpRoutes[IO] = Http4sRoute.chain(routesInOrder) - val allRoutesWithMiddleware: HttpRoutes[IO] = ResourceDocMiddleware.apply(resourceDocs)(IdempotencyMiddleware(allOwnRoutes)) + lazy val allRoutesWithMiddleware: HttpRoutes[IO] = ResourceDocMiddleware.apply(orderedResourceDocs, routes => IdempotencyMiddleware(routes))(IdempotencyMiddleware(allOwnRoutes)) // ─── path-rewriting bridge: /obp/v3.1.0/… → /obp/v3.0.0/… ────────────── From ce5a375dd14ca01a0cbe3017fe890f920330db42 Mon Sep 17 00:00:00 2001 From: Hongwei Date: Wed, 7 Oct 2026 17:29:58 +0200 Subject: [PATCH 15/29] refactor: bind every v3.0.0 ResourceDoc to its route Declare each endpoint as an Http4sRoute and derive the route chain from one list, so the middleware selects the doc of the route that serves a request and runs only that route. The path-rewriting bridge asks the routes whether the version serves a path instead of matching the URL against doc templates. --- .../scala/code/api/v3_0_0/Http4s300.scala | 204 +++++++++--------- 1 file changed, 105 insertions(+), 99 deletions(-) diff --git a/obp-api/src/main/scala/code/api/v3_0_0/Http4s300.scala b/obp-api/src/main/scala/code/api/v3_0_0/Http4s300.scala index 0a4c35341b..026df0536f 100644 --- a/obp-api/src/main/scala/code/api/v3_0_0/Http4s300.scala +++ b/obp-api/src/main/scala/code/api/v3_0_0/Http4s300.scala @@ -35,13 +35,13 @@ import code.api.Constant._ import code.api.ResourceDocs1_4_0.SwaggerDefinitionsJSON import code.api.ResourceDocs1_4_0.SwaggerDefinitionsJSON._ import code.api.v2_0_0.AccountsHelper._ -import code.api.util.APIUtil.{EmptyBody, ResourceDoc, _} +import code.api.util.APIUtil.{Http4sHandler, Http4sRoute, EmptyBody, ResourceDoc, _} import code.api.util.{ApiRole, FutureUtil, Glossary} import code.api.util.ApiRole._ import code.api.util.ApiTag._ import code.api.util.ErrorMessages._ import code.api.util.http4s.Http4sRequestAttributes.{EndpointHelpers, RequestOps} -import code.api.util.http4s.ResourceDocMiddleware +import code.api.util.http4s.{ResourceDocMatcher, ResourceDocMiddleware} import code.api.util.http4s.IdempotencyMiddleware import code.api.util.newstyle.ViewNewStyle import code.api.util.{APIUtil, CallContext, CustomJsonFormats, NewStyle} @@ -90,7 +90,7 @@ object Http4s300 { // ─── root ───────────────────────────────────────────────────────────────── - val root: HttpRoutes[IO] = HttpRoutes.of[IO] { + val root: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` => EndpointHelpers.executeAndRespond(req) { _ => Future.successful(JSONFactory.getApiInfoJSON(ApiVersion.v3_0_0, versionStatus)) @@ -122,7 +122,7 @@ object Http4s300 { // ─── getViewsForBankAccount ─────────────────────────────────────────────── - val getViewsForBankAccount: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getViewsForBankAccount: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "accounts" / _ / "views" => EndpointHelpers.withBankAccount(req) { (user, account, cc) => for { @@ -178,7 +178,7 @@ object Http4s300 { // ─── createViewForBankAccount ───────────────────────────────────────────── - val createViewForBankAccount: HttpRoutes[IO] = HttpRoutes.of[IO] { + val createViewForBankAccount: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "banks" / _ / "accounts" / accountIdStr / "views" => EndpointHelpers.withUserAndBankCreated(req) { (user, bank, cc) => for { @@ -241,7 +241,7 @@ object Http4s300 { // ─── updateViewForBankAccount ───────────────────────────────────────────── - val updateViewForBankAccount: HttpRoutes[IO] = HttpRoutes.of[IO] { + val updateViewForBankAccount: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "banks" / _ / "accounts" / _ / "views" / viewIdStr => EndpointHelpers.withBankAccount(req) { (user, account, cc) => updateViewImpl300(user, account, ViewId(viewIdStr), cc.httpBody.getOrElse(""), cc) @@ -295,7 +295,7 @@ object Http4s300 { // ─── getPermissionForUserForBankAccount ─────────────────────────────────── - val getPermissionForUserForBankAccount: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getPermissionForUserForBankAccount: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "accounts" / _ / "permissions" / providerStr / providerIdStr => EndpointHelpers.withBankAccount(req) { (user, account, cc) => for { @@ -338,7 +338,7 @@ object Http4s300 { // ─── getPrivateAccountById ──────────────────────────────────────────────── - val getPrivateAccountById: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getPrivateAccountById: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "accounts" / _ / _ / "account" => EndpointHelpers.withView(req) { (user, account, view, cc) => for { @@ -379,7 +379,7 @@ object Http4s300 { // ─── getPublicAccountById ───────────────────────────────────────────────── - val getPublicAccountById: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getPublicAccountById: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "public" / "accounts" / _ / _ / "account" => EndpointHelpers.executeAndRespond(req) { cc => for { @@ -425,7 +425,7 @@ object Http4s300 { // ─── getCoreAccountById ─────────────────────────────────────────────────── - val getCoreAccountById: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getCoreAccountById: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "my" / "banks" / _ / "accounts" / _ / "account" => EndpointHelpers.withBankAccount(req) { (user, account, cc) => for { @@ -470,7 +470,7 @@ object Http4s300 { // ─── corePrivateAccountsAllBanks ────────────────────────────────────────── - val corePrivateAccountsAllBanks: HttpRoutes[IO] = HttpRoutes.of[IO] { + val corePrivateAccountsAllBanks: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "my" / "accounts" => EndpointHelpers.withUser(req) { (user, cc) => for { @@ -507,7 +507,7 @@ object Http4s300 { // does NOT resolve the bank/view (validateBank checks pathParams("BANK_ID") exactly). // Order: prop check (400) → role check (403) → bank lookup (404) — matches test expectations. - val getFirehoseAccountsAtOneBank: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getFirehoseAccountsAtOneBank: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / bankIdStr / "firehose" / "accounts" / "views" / viewIdStr => EndpointHelpers.withUser(req) { (user, cc) => val roles = ApiRole.canUseAccountFirehose :: canUseAccountFirehoseAtAnyBank :: Nil @@ -595,7 +595,7 @@ object Http4s300 { // Uses non-standard FIREHOSE_* vars so middleware skips bank/account/view validation. // Order: prop check (400) → role check (403) → bank/account/view lookups — matches tests. - val getFirehoseTransactionsForBankAccount: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getFirehoseTransactionsForBankAccount: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / bankIdStr / "firehose" / "accounts" / accountIdStr / "views" / viewIdStr / "transactions" => EndpointHelpers.withUser(req) { (user, cc) => val roles = ApiRole.canUseAccountFirehose :: canUseAccountFirehoseAtAnyBank :: Nil @@ -664,7 +664,7 @@ object Http4s300 { // ─── getCoreTransactionsForBankAccount ──────────────────────────────────── - val getCoreTransactionsForBankAccount: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getCoreTransactionsForBankAccount: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "my" / "banks" / _ / "accounts" / _ / "transactions" => EndpointHelpers.withBankAccount(req) { (user, account, cc) => for { @@ -709,7 +709,7 @@ object Http4s300 { // ─── getTransactionsForBankAccount ──────────────────────────────────────── - val getTransactionsForBankAccount: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getTransactionsForBankAccount: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "accounts" / _ / _ / "transactions" => EndpointHelpers.withView(req) { (user, account, view, cc) => for { @@ -757,7 +757,7 @@ object Http4s300 { private val esw = new elasticsearchWarehouse - val dataWarehouseSearch: HttpRoutes[IO] = HttpRoutes.of[IO] { + val dataWarehouseSearch: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "search" / "warehouse" / indexStr => implicit val cc: CallContext = req.callContext val io = for { @@ -831,7 +831,7 @@ object Http4s300 { // ─── dataWarehouseStatistics ────────────────────────────────────────────── - val dataWarehouseStatistics: HttpRoutes[IO] = HttpRoutes.of[IO] { + val dataWarehouseStatistics: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "search" / "warehouse" / "statistics" / indexStr / fieldStr => implicit val cc: CallContext = req.callContext val io = for { @@ -906,7 +906,7 @@ object Http4s300 { // ─── getUser (by email) ─────────────────────────────────────────────────── - val getUser: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getUser: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "users" / "email" / emailStr / "terminator" => EndpointHelpers.withUser(req) { (user, cc) => for { @@ -938,7 +938,7 @@ object Http4s300 { // ─── getUserByUserId ────────────────────────────────────────────────────── - val getUserByUserId: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getUserByUserId: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "users" / "user_id" / userIdStr => EndpointHelpers.withUser(req) { (user, cc) => for { @@ -973,7 +973,7 @@ object Http4s300 { // ─── getUserByUsername ──────────────────────────────────────────────────── - val getUserByUsername: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getUserByUsername: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "users" / "username" / usernameStr => EndpointHelpers.withUser(req) { (user, cc) => for { @@ -1009,7 +1009,7 @@ object Http4s300 { // ─── getAdapterInfoForBank ──────────────────────────────────────────────── - val getAdapterInfoForBank: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getAdapterInfoForBank: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "adapter" => EndpointHelpers.withUserAndBank(req) { (user, bank, cc) => for { @@ -1040,7 +1040,7 @@ object Http4s300 { // ─── createBranch ───────────────────────────────────────────────────────── - val createBranch: HttpRoutes[IO] = HttpRoutes.of[IO] { + val createBranch: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "banks" / _ / "branches" => EndpointHelpers.withUserAndBankAndBodyCreated[BranchJsonV300, BranchJsonV300](req) { (user, bank, body, cc) => for { @@ -1078,7 +1078,7 @@ object Http4s300 { // ─── updateBranch ───────────────────────────────────────────────────────── - val updateBranch: HttpRoutes[IO] = HttpRoutes.of[IO] { + val updateBranch: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "banks" / _ / "branches" / branchIdStr => EndpointHelpers.withUserAndBankAndBodyCreated[PostBranchJsonV300, BranchJsonV300](req) { (user, bank, body, cc) => for { @@ -1120,7 +1120,7 @@ object Http4s300 { // ─── createAtm ──────────────────────────────────────────────────────────── - val createAtm: HttpRoutes[IO] = HttpRoutes.of[IO] { + val createAtm: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "banks" / _ / "atms" => EndpointHelpers.withUserAndBankAndBodyCreated[AtmJsonV300, AtmJsonV300](req) { (user, bank, body, cc) => for { @@ -1160,7 +1160,7 @@ object Http4s300 { private val getBranchesIsPublic = APIUtil.getPropsAsBoolValue("apiOptions.getBranchesIsPublic", true) - val getBranch: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getBranch: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "branches" / branchIdStr => EndpointHelpers.withBank(req) { (bank, cc) => for { @@ -1219,7 +1219,7 @@ object Http4s300 { } } - val getBranches: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getBranches: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "branches" => EndpointHelpers.withBank(req) { (bank, cc) => val qp = req.uri.query.params @@ -1315,7 +1315,7 @@ object Http4s300 { private val getAtmsIsPublic = APIUtil.getPropsAsBoolValue("apiOptions.getAtmsIsPublic", true) - val getAtm: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getAtm: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "atms" / atmIdStr => EndpointHelpers.withBank(req) { (bank, cc) => for { @@ -1352,7 +1352,7 @@ object Http4s300 { // ─── getAtms ────────────────────────────────────────────────────────────── - val getAtms: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getAtms: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "atms" => EndpointHelpers.withBank(req) { (bank, cc) => val qp = req.uri.query.params @@ -1415,7 +1415,7 @@ object Http4s300 { // ─── getUsers ───────────────────────────────────────────────────────────── - val getUsers: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getUsers: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "users" => EndpointHelpers.withUser(req) { (user, cc) => for { @@ -1453,7 +1453,7 @@ object Http4s300 { // ─── getCustomersForUser ────────────────────────────────────────────────── - val getCustomersForUser: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getCustomersForUser: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "users" / "current" / "customers" => EndpointHelpers.withUser(req) { (user, cc) => for { @@ -1487,7 +1487,7 @@ object Http4s300 { // ─── getCurrentUser ─────────────────────────────────────────────────────── - val getCurrentUser: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getCurrentUser: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "users" / "current" => EndpointHelpers.withUser(req) { (user, cc) => for { @@ -1519,7 +1519,7 @@ object Http4s300 { // ─── privateAccountsAtOneBank ───────────────────────────────────────────── - val privateAccountsAtOneBank: HttpRoutes[IO] = HttpRoutes.of[IO] { + val privateAccountsAtOneBank: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "accounts" / "private" => EndpointHelpers.withUserAndBank(req) { (user, bank, cc) => for { @@ -1554,7 +1554,7 @@ object Http4s300 { // ─── getPrivateAccountIdsbyBankId ───────────────────────────────────────── - val getPrivateAccountIdsbyBankId: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getPrivateAccountIdsbyBankId: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "accounts" / "account_ids" / "private" => EndpointHelpers.withUserAndBank(req) { (user, bank, cc) => for { @@ -1593,7 +1593,7 @@ object Http4s300 { // ─── getOtherAccountsForBankAccount ─────────────────────────────────────── - val getOtherAccountsForBankAccount: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getOtherAccountsForBankAccount: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "accounts" / _ / _ / "other_accounts" => EndpointHelpers.withView(req) { (user, account, view, cc) => for { @@ -1622,7 +1622,7 @@ object Http4s300 { // ─── getOtherAccountByIdForBankAccount ──────────────────────────────────── - val getOtherAccountByIdForBankAccount: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getOtherAccountByIdForBankAccount: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "accounts" / _ / _ / "other_accounts" / otherAccountIdStr => EndpointHelpers.withView(req) { (user, account, view, cc) => for { @@ -1651,7 +1651,7 @@ object Http4s300 { // ─── addEntitlementRequest ──────────────────────────────────────────────── - val addEntitlementRequest: HttpRoutes[IO] = HttpRoutes.of[IO] { + val addEntitlementRequest: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "entitlement-requests" => EndpointHelpers.withUserAndBodyCreated[CreateEntitlementRequestJSON, EntitlementRequestJSON](req) { (user, body, cc) => for { @@ -1710,7 +1710,7 @@ object Http4s300 { // ─── getAllEntitlementRequests ───────────────────────────────────────────── - val getAllEntitlementRequests: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getAllEntitlementRequests: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "entitlement-requests" => EndpointHelpers.withUser(req) { (user, cc) => val allowedEntitlements = canGetEntitlementRequestsAtAnyBank :: Nil @@ -1747,7 +1747,7 @@ object Http4s300 { // ─── getEntitlementRequests ─────────────────────────────────────────────── - val getEntitlementRequests: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getEntitlementRequests: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "users" / userIdStr / "entitlement-requests" => EndpointHelpers.withUser(req) { (user, cc) => val allowedEntitlements = canGetEntitlementRequestsAtAnyBank :: Nil @@ -1783,7 +1783,7 @@ object Http4s300 { // ─── getEntitlementRequestsForCurrentUser ───────────────────────────────── - val getEntitlementRequestsForCurrentUser: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getEntitlementRequestsForCurrentUser: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "my" / "entitlement-requests" => EndpointHelpers.withUser(req) { (user, cc) => for { @@ -1816,7 +1816,7 @@ object Http4s300 { // ─── deleteEntitlementRequest ───────────────────────────────────────────── - val deleteEntitlementRequest: HttpRoutes[IO] = HttpRoutes.of[IO] { + val deleteEntitlementRequest: Http4sRoute = Http4sRoute { case req @ DELETE -> `prefixPath` / "entitlement-requests" / entitlementRequestIdStr => EndpointHelpers.withUser(req) { (user, cc) => val allowedEntitlements = canDeleteEntitlementRequestsAtOneBank :: canDeleteEntitlementRequestsAtAnyBank :: Nil @@ -1854,7 +1854,7 @@ object Http4s300 { // ─── getEntitlementsForCurrentUser ──────────────────────────────────────── - val getEntitlementsForCurrentUser: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getEntitlementsForCurrentUser: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "my" / "entitlements" => EndpointHelpers.withUser(req) { (user, cc) => for { @@ -1915,7 +1915,7 @@ object Http4s300 { } } - val getApiGlossary: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getApiGlossary: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "api" / "glossary" => EndpointHelpers.executeAndRespond(req) { cc => for { @@ -1958,7 +1958,7 @@ object Http4s300 { // ─── getAccountsHeld ────────────────────────────────────────────────────── - val getAccountsHeld: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getAccountsHeld: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "accounts-held" => EndpointHelpers.withUserAndBank(req) { (user, bank, cc) => for { @@ -1996,7 +1996,7 @@ object Http4s300 { // ─── getAggregateMetrics ────────────────────────────────────────────────── - val getAggregateMetrics: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getAggregateMetrics: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "management" / "aggregate-metrics" => EndpointHelpers.withUser(req) { (user, cc) => for { @@ -2069,7 +2069,7 @@ object Http4s300 { // ─── addScope ───────────────────────────────────────────────────────────── - val addScope: HttpRoutes[IO] = HttpRoutes.of[IO] { + val addScope: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "consumers" / consumerIdStr / "scopes" => EndpointHelpers.withUserAndBodyCreated[CreateScopeJson, ScopeJson](req) { (user, body, cc) => for { @@ -2125,7 +2125,7 @@ object Http4s300 { // ─── deleteScope ────────────────────────────────────────────────────────── - val deleteScope: HttpRoutes[IO] = HttpRoutes.of[IO] { + val deleteScope: Http4sRoute = Http4sRoute { case req @ DELETE -> `prefixPath` / "consumers" / consumerIdStr / "scope" / scopeIdStr => EndpointHelpers.withUser(req) { (user, cc) => for { @@ -2171,7 +2171,7 @@ object Http4s300 { // ─── getScopes ──────────────────────────────────────────────────────────── - val getScopes: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getScopes: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "consumers" / consumerIdStr / "scopes" => EndpointHelpers.withUser(req) { (user, cc) => for { @@ -2208,7 +2208,7 @@ object Http4s300 { // ─── getBanks ───────────────────────────────────────────────────────────── - val getBanks: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getBanks: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" => EndpointHelpers.executeAndRespond(req) { cc => for { @@ -2243,7 +2243,7 @@ object Http4s300 { // ─── bankById ───────────────────────────────────────────────────────────── - val bankById: HttpRoutes[IO] = HttpRoutes.of[IO] { + val bankById: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ => EndpointHelpers.withBank(req) { (bank, cc) => Future.successful(code.api.v4_0_0.JSONFactory400.createBankJSON400(bank)) @@ -2287,57 +2287,63 @@ object Http4s300 { // ─── allRoutes ──────────────────────────────────────────────────────────── - private val allOwnRoutes: HttpRoutes[IO] = Kleisli[HttpF, Request[IO], Response[IO]] { req => - root.run(req) - .orElse(getViewsForBankAccount.run(req)) - .orElse(createViewForBankAccount.run(req)) - .orElse(updateViewForBankAccount.run(req)) - .orElse(getPermissionForUserForBankAccount.run(req)) - .orElse(getPrivateAccountById.run(req)) - .orElse(getPublicAccountById.run(req)) - .orElse(getCoreAccountById.run(req)) - .orElse(corePrivateAccountsAllBanks.run(req)) - .orElse(getFirehoseAccountsAtOneBank.run(req)) - .orElse(getFirehoseTransactionsForBankAccount.run(req)) - .orElse(getCoreTransactionsForBankAccount.run(req)) - .orElse(getTransactionsForBankAccount.run(req)) - .orElse(dataWarehouseSearch.run(req)) - .orElse(dataWarehouseStatistics.run(req)) - .orElse(getUser.run(req)) - .orElse(getUserByUserId.run(req)) - .orElse(getUserByUsername.run(req)) - .orElse(getAdapterInfoForBank.run(req)) - .orElse(createBranch.run(req)) - .orElse(updateBranch.run(req)) - .orElse(createAtm.run(req)) - .orElse(getBranch.run(req)) - .orElse(getBranches.run(req)) - .orElse(getAtm.run(req)) - .orElse(getAtms.run(req)) - .orElse(getUsers.run(req)) - .orElse(getCustomersForUser.run(req)) - .orElse(getCurrentUser.run(req)) - .orElse(privateAccountsAtOneBank.run(req)) - .orElse(getPrivateAccountIdsbyBankId.run(req)) - .orElse(getOtherAccountsForBankAccount.run(req)) - .orElse(getOtherAccountByIdForBankAccount.run(req)) - .orElse(addEntitlementRequest.run(req)) - .orElse(getAllEntitlementRequests.run(req)) - .orElse(getEntitlementRequests.run(req)) - .orElse(getEntitlementRequestsForCurrentUser.run(req)) - .orElse(deleteEntitlementRequest.run(req)) - .orElse(getEntitlementsForCurrentUser.run(req)) - .orElse(getApiGlossary.run(req)) - .orElse(getAccountsHeld.run(req)) - .orElse(getAggregateMetrics.run(req)) - .orElse(addScope.run(req)) - .orElse(deleteScope.run(req)) - .orElse(getScopes.run(req)) - .orElse(getBanks.run(req)) - .orElse(bankById.run(req)) - } + // The routes in the order they are tried. ResourceDocMiddleware selects the doc of the first + // route that serves a request, so it is given the docs in this same order. + lazy val routesInOrder: List[Http4sHandler] = List( + root, + getViewsForBankAccount, + createViewForBankAccount, + updateViewForBankAccount, + getPermissionForUserForBankAccount, + getPrivateAccountById, + getPublicAccountById, + getCoreAccountById, + corePrivateAccountsAllBanks, + getFirehoseAccountsAtOneBank, + getFirehoseTransactionsForBankAccount, + getCoreTransactionsForBankAccount, + getTransactionsForBankAccount, + dataWarehouseSearch, + dataWarehouseStatistics, + getUser, + getUserByUserId, + getUserByUsername, + getAdapterInfoForBank, + createBranch, + updateBranch, + createAtm, + getBranch, + getBranches, + getAtm, + getAtms, + getUsers, + getCustomersForUser, + getCurrentUser, + privateAccountsAtOneBank, + getPrivateAccountIdsbyBankId, + getOtherAccountsForBankAccount, + getOtherAccountByIdForBankAccount, + addEntitlementRequest, + getAllEntitlementRequests, + getEntitlementRequests, + getEntitlementRequestsForCurrentUser, + deleteEntitlementRequest, + getEntitlementsForCurrentUser, + getApiGlossary, + getAccountsHeld, + getAggregateMetrics, + addScope, + deleteScope, + getScopes, + getBanks, + bankById + ) + + lazy val orderedResourceDocs: ArrayBuffer[ResourceDoc] = ResourceDocMatcher.orderByRoutes(resourceDocs, routesInOrder) + + private lazy val allOwnRoutes: HttpRoutes[IO] = Http4sRoute.chain(routesInOrder) - val allRoutesWithMiddleware: HttpRoutes[IO] = ResourceDocMiddleware.apply(resourceDocs)(IdempotencyMiddleware(allOwnRoutes)) + lazy val allRoutesWithMiddleware: HttpRoutes[IO] = ResourceDocMiddleware.apply(orderedResourceDocs, routes => IdempotencyMiddleware(routes))(IdempotencyMiddleware(allOwnRoutes)) // ─── path-rewriting bridge: /obp/v3.0.0/… → /obp/v2.2.0/… ────────────── From 26017fa5ec4bdbc216ed2a9ff96641f90728ba7c Mon Sep 17 00:00:00 2001 From: Hongwei Date: Wed, 7 Oct 2026 17:29:58 +0200 Subject: [PATCH 16/29] refactor: bind every v1.2.1 to v2.2.0 ResourceDoc to its route Declare each endpoint of v1.2.1, v1.3.0, v1.4.0, v2.0.0, v2.1.0 and v2.2.0 as an Http4sRoute and derive the route chain from one list. bankById keeps running before the middleware so an unknown bank answers 400, not 464, and is listed as outside the chain. --- .../scala/code/api/v1_2_1/Http4s121.scala | 301 +++++++++--------- .../scala/code/api/v1_3_0/Http4s130.scala | 28 +- .../scala/code/api/v1_4_0/Http4s140.scala | 56 ++-- .../scala/code/api/v2_0_0/Http4s200.scala | 166 +++++----- .../scala/code/api/v2_1_0/Http4s210.scala | 118 +++---- .../scala/code/api/v2_2_0/Http4s220.scala | 90 +++--- 6 files changed, 397 insertions(+), 362 deletions(-) diff --git a/obp-api/src/main/scala/code/api/v1_2_1/Http4s121.scala b/obp-api/src/main/scala/code/api/v1_2_1/Http4s121.scala index d203aff336..06e15f6574 100644 --- a/obp-api/src/main/scala/code/api/v1_2_1/Http4s121.scala +++ b/obp-api/src/main/scala/code/api/v1_2_1/Http4s121.scala @@ -32,13 +32,13 @@ import cats.data.{Kleisli, OptionT} import cats.effect._ import code.api.Constant._ import code.api.ResourceDocs1_4_0.SwaggerDefinitionsJSON._ -import code.api.util.APIUtil.{EmptyBody, ResourceDoc, _} +import code.api.util.APIUtil.{Http4sHandler, Http4sRoute, EmptyBody, ResourceDoc, _} import code.api.util.ApiTag._ import code.api.util.ErrorMessages._ import code.api.util.http4s.Http4sRequestAttributes.{EndpointHelpers, RequestOps, callContextKey} import code.api.util.http4s.Http4sCallContextBuilder import code.api.util.http4s.IdempotencyMiddleware -import code.api.util.http4s.ResourceDocMiddleware +import code.api.util.http4s.{ResourceDocMatcher, ResourceDocMiddleware} import code.api.util.newstyle.ViewNewStyle import code.api.util.{CallContext, CustomJsonFormats, NewStyle} import code.bankconnectors.Connector @@ -149,7 +149,7 @@ object Http4s121 { // ─── root ─────────────────────────────────────────────────────────────── - val root: HttpRoutes[IO] = HttpRoutes.of[IO] { + val root: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "root" => EndpointHelpers.executeAndRespond(req) { cc => Future.successful(JSONFactory.getApiInfoJSON(ApiVersion.v1_2_1, "STABLE")) @@ -176,7 +176,7 @@ object Http4s121 { // ─── getBanks ──────────────────────────────────────────────────────────── - val getBanks: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getBanks: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" => EndpointHelpers.executeAndRespond(req) { cc => for { @@ -212,7 +212,7 @@ object Http4s121 { // bankById runs outside ResourceDocMiddleware so it can return 400 (not 464) for unknown bank, // preserving the v1.2.1 Lift behavior. Builds its own CallContext via Http4sCallContextBuilder. - val bankById: HttpRoutes[IO] = HttpRoutes.of[IO] { + val bankById: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / bankId => Http4sCallContextBuilder.fromRequest(req, implementedInApiVersion.apiShortVersion).flatMap { cc => val reqWithCc = req.withAttribute(callContextKey, cc) @@ -245,7 +245,7 @@ object Http4s121 { // ─── getPrivateAccountsAllBanks ────────────────────────────────────────── - val getPrivateAccountsAllBanks: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getPrivateAccountsAllBanks: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "accounts" => EndpointHelpers.withUser(req) { (user, cc) => Future { @@ -276,7 +276,7 @@ object Http4s121 { // ─── privateAccountsAllBanks ───────────────────────────────────────────── - val privateAccountsAllBanks: HttpRoutes[IO] = HttpRoutes.of[IO] { + val privateAccountsAllBanks: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "accounts" / "private" => EndpointHelpers.withUser(req) { (user, cc) => Future { @@ -306,7 +306,7 @@ object Http4s121 { // ─── publicAccountsAllBanks ────────────────────────────────────────────── - val publicAccountsAllBanks: HttpRoutes[IO] = HttpRoutes.of[IO] { + val publicAccountsAllBanks: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "accounts" / "public" => EndpointHelpers.executeAndRespond(req) { cc => Future { @@ -338,7 +338,7 @@ object Http4s121 { // ─── getPrivateAccountsAtOneBank ───────────────────────────────────────── - val getPrivateAccountsAtOneBank: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getPrivateAccountsAtOneBank: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "accounts" => EndpointHelpers.withUserAndBank(req) { (user, bank, cc) => Future { @@ -369,7 +369,7 @@ object Http4s121 { // ─── privateAccountsAtOneBank ──────────────────────────────────────────── - val privateAccountsAtOneBank: HttpRoutes[IO] = HttpRoutes.of[IO] { + val privateAccountsAtOneBank: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "accounts" / "private" => EndpointHelpers.withUserAndBank(req) { (user, bank, cc) => Future { @@ -400,7 +400,7 @@ object Http4s121 { // ─── publicAccountsAtOneBank ───────────────────────────────────────────── - val publicAccountsAtOneBank: HttpRoutes[IO] = HttpRoutes.of[IO] { + val publicAccountsAtOneBank: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "accounts" / "public" => EndpointHelpers.withBank(req) { (bank, cc) => Future { @@ -429,7 +429,7 @@ object Http4s121 { // ─── accountById ───────────────────────────────────────────────────────── - val accountById: HttpRoutes[IO] = HttpRoutes.of[IO] { + val accountById: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "accounts" / _ / _ / "account" => EndpointHelpers.withView(req) { (user, account, view, cc) => for { @@ -475,7 +475,7 @@ object Http4s121 { // ─── updateAccountLabel ────────────────────────────────────────────────── - val updateAccountLabel: HttpRoutes[IO] = HttpRoutes.of[IO] { + val updateAccountLabel: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "banks" / bankId / "accounts" / accountId => EndpointHelpers.executeFuture(req) { val cc = req.callContext @@ -521,7 +521,7 @@ object Http4s121 { // ─── getViewsForBankAccount ─────────────────────────────────────────────── - val getViewsForBankAccount: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getViewsForBankAccount: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "accounts" / _ / "views" => EndpointHelpers.withBankAccount(req) { (user, account, cc) => for { @@ -577,7 +577,7 @@ object Http4s121 { // ─── createViewForBankAccount ───────────────────────────────────────────── - val createViewForBankAccount: HttpRoutes[IO] = HttpRoutes.of[IO] { + val createViewForBankAccount: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "banks" / bankId / "accounts" / accountId / "views" => EndpointHelpers.executeFutureCreated(req) { val cc = req.callContext @@ -642,7 +642,7 @@ object Http4s121 { // ─── updateViewForBankAccount ───────────────────────────────────────────── - val updateViewForBankAccount: HttpRoutes[IO] = HttpRoutes.of[IO] { + val updateViewForBankAccount: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "banks" / bankId / "accounts" / accountId / "views" / viewId => EndpointHelpers.executeFutureWithBody[UpdateViewJsonV121, ViewJSONV121](req) { (updateJsonV121, cc) => for { @@ -701,7 +701,7 @@ object Http4s121 { // ─── deleteViewForBankAccount ───────────────────────────────────────────── - val deleteViewForBankAccount: HttpRoutes[IO] = HttpRoutes.of[IO] { + val deleteViewForBankAccount: Http4sRoute = Http4sRoute { case req @ DELETE -> `prefixPath` / "banks" / bankId / "accounts" / accountId / "views" / viewId => EndpointHelpers.withUserDelete(req) { (user, cc) => for { @@ -736,7 +736,7 @@ object Http4s121 { // ─── getPermissionsForBankAccount ───────────────────────────────────────── - val getPermissionsForBankAccount: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getPermissionsForBankAccount: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "accounts" / _ / "permissions" => EndpointHelpers.withBankAccount(req) { (user, account, cc) => val permissionBox = Views.views.vend.permission(BankIdAccountId(account.bankId, account.accountId), user) @@ -769,7 +769,7 @@ object Http4s121 { // ─── getPermissionForUserForBankAccount ─────────────────────────────────── - val getPermissionForUserForBankAccount: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getPermissionForUserForBankAccount: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "accounts" / _ / "permissions" / provider / providerId => EndpointHelpers.withBankAccount(req) { (loggedInUser, account, cc) => val loggedInUserPermissionBox = Views.views.vend.permission(BankIdAccountId(account.bankId, account.accountId), loggedInUser) @@ -809,7 +809,7 @@ object Http4s121 { // ─── addPermissionForUserForBankAccountForMultipleViews ─────────────────── - val addPermissionForUserForBankAccountForMultipleViews: HttpRoutes[IO] = HttpRoutes.of[IO] { + val addPermissionForUserForBankAccountForMultipleViews: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "banks" / bankId / "accounts" / accountId / "permissions" / provider / providerId / "views" => EndpointHelpers.executeFutureCreated(req) { val cc = req.callContext @@ -855,7 +855,7 @@ object Http4s121 { // ─── addPermissionForUserForBankAccountForOneView ───────────────────────── - val addPermissionForUserForBankAccountForOneView: HttpRoutes[IO] = HttpRoutes.of[IO] { + val addPermissionForUserForBankAccountForOneView: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "banks" / bankId / "accounts" / accountId / "permissions" / provider / providerId / "views" / viewId => EndpointHelpers.executeFutureCreated(req) { val cc = req.callContext @@ -894,7 +894,7 @@ object Http4s121 { // ─── removePermissionForUserForBankAccountForOneView ────────────────────── - val removePermissionForUserForBankAccountForOneView: HttpRoutes[IO] = HttpRoutes.of[IO] { + val removePermissionForUserForBankAccountForOneView: Http4sRoute = Http4sRoute { case req @ DELETE -> `prefixPath` / "banks" / bankId / "accounts" / accountId / "permissions" / provider / providerId / "views" / viewId => EndpointHelpers.withUserDelete(req) { (user, cc) => for { @@ -925,7 +925,7 @@ object Http4s121 { // ─── removePermissionForUserForBankAccountForAllViews ──────────────────── - val removePermissionForUserForBankAccountForAllViews: HttpRoutes[IO] = HttpRoutes.of[IO] { + val removePermissionForUserForBankAccountForAllViews: Http4sRoute = Http4sRoute { case req @ DELETE -> `prefixPath` / "banks" / bankId / "accounts" / accountId / "permissions" / provider / providerId / "views" => EndpointHelpers.withUserDelete(req) { (user, cc) => for { @@ -956,7 +956,7 @@ object Http4s121 { // ─── getOtherAccountsForBankAccount ────────────────────────────────────── - val getOtherAccountsForBankAccount: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getOtherAccountsForBankAccount: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "accounts" / _ / _ / "other_accounts" => EndpointHelpers.withView(req) { (user, account, view, cc) => for { @@ -983,7 +983,7 @@ object Http4s121 { // ─── getOtherAccountByIdForBankAccount ─────────────────────────────────── - val getOtherAccountByIdForBankAccount: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getOtherAccountByIdForBankAccount: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "accounts" / _ / _ / "other_accounts" / otherAccountId => EndpointHelpers.withView(req) { (user, account, view, cc) => for { @@ -1012,7 +1012,7 @@ object Http4s121 { // ─── getOtherAccountMetadata ────────────────────────────────────────────── - val getOtherAccountMetadata: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getOtherAccountMetadata: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "accounts" / _ / _ / "other_accounts" / otherAccountId / "metadata" => EndpointHelpers.withView(req) { (user, account, view, cc) => for { @@ -1043,7 +1043,7 @@ object Http4s121 { // ─── getCounterpartyPublicAlias ─────────────────────────────────────────── - val getCounterpartyPublicAlias: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getCounterpartyPublicAlias: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "accounts" / _ / _ / "other_accounts" / otherAccountId / "public_alias" => EndpointHelpers.withView(req) { (user, account, view, cc) => for { @@ -1074,7 +1074,7 @@ object Http4s121 { // ─── addCounterpartyPublicAlias ─────────────────────────────────────────── - val addCounterpartyPublicAlias: HttpRoutes[IO] = HttpRoutes.of[IO] { + val addCounterpartyPublicAlias: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "banks" / _ / "accounts" / _ / _ / "other_accounts" / otherAccountId / "public_alias" => EndpointHelpers.withViewCreated(req) { (user, account, view, cc) => val bodyStr = cc.httpBody.getOrElse("") @@ -1118,7 +1118,7 @@ object Http4s121 { // ─── updateCounterpartyPublicAlias ──────────────────────────────────────── - val updateCounterpartyPublicAlias: HttpRoutes[IO] = HttpRoutes.of[IO] { + val updateCounterpartyPublicAlias: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "banks" / _ / "accounts" / _ / _ / "other_accounts" / otherAccountId / "public_alias" => EndpointHelpers.withView(req) { (user, account, view, cc) => val bodyStr = cc.httpBody.getOrElse("") @@ -1148,7 +1148,7 @@ object Http4s121 { // ─── deleteCounterpartyPublicAlias ──────────────────────────────────────── - val deleteCounterpartyPublicAlias: HttpRoutes[IO] = HttpRoutes.of[IO] { + val deleteCounterpartyPublicAlias: Http4sRoute = Http4sRoute { case req @ DELETE -> `prefixPath` / "banks" / _ / "accounts" / _ / _ / "other_accounts" / otherAccountId / "public_alias" => EndpointHelpers.withUserDelete(req) { (user, cc) => for { @@ -1185,7 +1185,7 @@ object Http4s121 { // ─── getOtherAccountPrivateAlias ────────────────────────────────────────── - val getOtherAccountPrivateAlias: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getOtherAccountPrivateAlias: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "accounts" / _ / _ / "other_accounts" / otherAccountId / "private_alias" => EndpointHelpers.withView(req) { (user, account, view, cc) => for { @@ -1211,7 +1211,7 @@ object Http4s121 { // ─── addOtherAccountPrivateAlias ────────────────────────────────────────── - val addOtherAccountPrivateAlias: HttpRoutes[IO] = HttpRoutes.of[IO] { + val addOtherAccountPrivateAlias: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "banks" / _ / "accounts" / _ / _ / "other_accounts" / otherAccountId / "private_alias" => EndpointHelpers.withViewCreated(req) { (user, account, view, cc) => val bodyStr = cc.httpBody.getOrElse("") @@ -1241,7 +1241,7 @@ object Http4s121 { // ─── updateCounterpartyPrivateAlias ─────────────────────────────────────── - val updateCounterpartyPrivateAlias: HttpRoutes[IO] = HttpRoutes.of[IO] { + val updateCounterpartyPrivateAlias: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "banks" / _ / "accounts" / _ / _ / "other_accounts" / otherAccountId / "private_alias" => EndpointHelpers.withView(req) { (user, account, view, cc) => val bodyStr = cc.httpBody.getOrElse("") @@ -1271,7 +1271,7 @@ object Http4s121 { // ─── deleteCounterpartyPrivateAlias ─────────────────────────────────────── - val deleteCounterpartyPrivateAlias: HttpRoutes[IO] = HttpRoutes.of[IO] { + val deleteCounterpartyPrivateAlias: Http4sRoute = Http4sRoute { case req @ DELETE -> `prefixPath` / "banks" / _ / "accounts" / _ / _ / "other_accounts" / otherAccountId / "private_alias" => EndpointHelpers.withUserDelete(req) { (user, cc) => for { @@ -1301,7 +1301,7 @@ object Http4s121 { // ─── addCounterpartyMoreInfo ────────────────────────────────────────────── - val addCounterpartyMoreInfo: HttpRoutes[IO] = HttpRoutes.of[IO] { + val addCounterpartyMoreInfo: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "banks" / _ / "accounts" / _ / _ / "other_accounts" / otherAccountId / "metadata" / "more_info" => EndpointHelpers.withViewCreated(req) { (user, account, view, cc) => val bodyStr = cc.httpBody.getOrElse("") @@ -1339,7 +1339,7 @@ object Http4s121 { // ─── updateCounterpartyMoreInfo ─────────────────────────────────────────── - val updateCounterpartyMoreInfo: HttpRoutes[IO] = HttpRoutes.of[IO] { + val updateCounterpartyMoreInfo: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "banks" / _ / "accounts" / _ / _ / "other_accounts" / otherAccountId / "metadata" / "more_info" => EndpointHelpers.withView(req) { (user, account, view, cc) => val bodyStr = cc.httpBody.getOrElse("") @@ -1369,7 +1369,7 @@ object Http4s121 { // ─── deleteCounterpartyMoreInfo ─────────────────────────────────────────── - val deleteCounterpartyMoreInfo: HttpRoutes[IO] = HttpRoutes.of[IO] { + val deleteCounterpartyMoreInfo: Http4sRoute = Http4sRoute { case req @ DELETE -> `prefixPath` / "banks" / _ / "accounts" / _ / _ / "other_accounts" / otherAccountId / "metadata" / "more_info" => EndpointHelpers.withUserDelete(req) { (user, cc) => for { @@ -1396,7 +1396,7 @@ object Http4s121 { // ─── addCounterpartyUrl ─────────────────────────────────────────────────── - val addCounterpartyUrl: HttpRoutes[IO] = HttpRoutes.of[IO] { + val addCounterpartyUrl: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "banks" / _ / "accounts" / _ / _ / "other_accounts" / otherAccountId / "metadata" / "url" => EndpointHelpers.withViewCreated(req) { (user, account, view, cc) => val bodyStr = cc.httpBody.getOrElse("") @@ -1423,7 +1423,7 @@ object Http4s121 { // ─── updateCounterpartyUrl ──────────────────────────────────────────────── - val updateCounterpartyUrl: HttpRoutes[IO] = HttpRoutes.of[IO] { + val updateCounterpartyUrl: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "banks" / _ / "accounts" / _ / _ / "other_accounts" / otherAccountId / "metadata" / "url" => EndpointHelpers.withView(req) { (user, account, view, cc) => val bodyStr = cc.httpBody.getOrElse("") @@ -1450,7 +1450,7 @@ object Http4s121 { // ─── deleteCounterpartyUrl ──────────────────────────────────────────────── - val deleteCounterpartyUrl: HttpRoutes[IO] = HttpRoutes.of[IO] { + val deleteCounterpartyUrl: Http4sRoute = Http4sRoute { case req @ DELETE -> `prefixPath` / "banks" / _ / "accounts" / _ / _ / "other_accounts" / otherAccountId / "metadata" / "url" => EndpointHelpers.withUserDelete(req) { (user, cc) => for { @@ -1477,7 +1477,7 @@ object Http4s121 { // ─── addCounterpartyImageUrl ────────────────────────────────────────────── - val addCounterpartyImageUrl: HttpRoutes[IO] = HttpRoutes.of[IO] { + val addCounterpartyImageUrl: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "banks" / _ / "accounts" / _ / _ / "other_accounts" / otherAccountId / "metadata" / "image_url" => EndpointHelpers.withViewCreated(req) { (user, account, view, cc) => val bodyStr = cc.httpBody.getOrElse("") @@ -1504,7 +1504,7 @@ object Http4s121 { // ─── updateCounterpartyImageUrl ─────────────────────────────────────────── - val updateCounterpartyImageUrl: HttpRoutes[IO] = HttpRoutes.of[IO] { + val updateCounterpartyImageUrl: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "banks" / _ / "accounts" / _ / _ / "other_accounts" / otherAccountId / "metadata" / "image_url" => EndpointHelpers.withView(req) { (user, account, view, cc) => val bodyStr = cc.httpBody.getOrElse("") @@ -1538,7 +1538,7 @@ object Http4s121 { // ─── deleteCounterpartyImageUrl ─────────────────────────────────────────── - val deleteCounterpartyImageUrl: HttpRoutes[IO] = HttpRoutes.of[IO] { + val deleteCounterpartyImageUrl: Http4sRoute = Http4sRoute { case req @ DELETE -> `prefixPath` / "banks" / _ / "accounts" / _ / _ / "other_accounts" / otherAccountId / "metadata" / "image_url" => EndpointHelpers.withUserDelete(req) { (user, cc) => for { @@ -1567,7 +1567,7 @@ object Http4s121 { // ─── addCounterpartyOpenCorporatesUrl ───────────────────────────────────── - val addCounterpartyOpenCorporatesUrl: HttpRoutes[IO] = HttpRoutes.of[IO] { + val addCounterpartyOpenCorporatesUrl: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "banks" / _ / "accounts" / _ / _ / "other_accounts" / otherAccountId / "metadata" / "open_corporates_url" => EndpointHelpers.withViewCreated(req) { (user, account, view, cc) => val bodyStr = cc.httpBody.getOrElse("") @@ -1601,7 +1601,7 @@ object Http4s121 { // ─── updateCounterpartyOpenCorporatesUrl ────────────────────────────────── - val updateCounterpartyOpenCorporatesUrl: HttpRoutes[IO] = HttpRoutes.of[IO] { + val updateCounterpartyOpenCorporatesUrl: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "banks" / _ / "accounts" / _ / _ / "other_accounts" / otherAccountId / "metadata" / "open_corporates_url" => EndpointHelpers.withView(req) { (user, account, view, cc) => val bodyStr = cc.httpBody.getOrElse("") @@ -1628,7 +1628,7 @@ object Http4s121 { // ─── deleteCounterpartyOpenCorporatesUrl ────────────────────────────────── - val deleteCounterpartyOpenCorporatesUrl: HttpRoutes[IO] = HttpRoutes.of[IO] { + val deleteCounterpartyOpenCorporatesUrl: Http4sRoute = Http4sRoute { case req @ DELETE -> `prefixPath` / "banks" / _ / "accounts" / _ / _ / "other_accounts" / otherAccountId / "metadata" / "open_corporates_url" => EndpointHelpers.withUserDelete(req) { (user, cc) => for { @@ -1655,7 +1655,7 @@ object Http4s121 { // ─── addCounterpartyCorporateLocation ──────────────────────────────────── - val addCounterpartyCorporateLocation: HttpRoutes[IO] = HttpRoutes.of[IO] { + val addCounterpartyCorporateLocation: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "banks" / _ / "accounts" / _ / _ / "other_accounts" / otherAccountId / "metadata" / "corporate_location" => EndpointHelpers.withViewCreated(req) { (user, account, view, cc) => val bodyStr = cc.httpBody.getOrElse("") @@ -1683,7 +1683,7 @@ object Http4s121 { // ─── updateCounterpartyCorporateLocation ────────────────────────────────── - val updateCounterpartyCorporateLocation: HttpRoutes[IO] = HttpRoutes.of[IO] { + val updateCounterpartyCorporateLocation: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "banks" / _ / "accounts" / _ / _ / "other_accounts" / otherAccountId / "metadata" / "corporate_location" => EndpointHelpers.withView(req) { (user, account, view, cc) => val bodyStr = cc.httpBody.getOrElse("") @@ -1711,7 +1711,7 @@ object Http4s121 { // ─── deleteCounterpartyCorporateLocation ────────────────────────────────── - val deleteCounterpartyCorporateLocation: HttpRoutes[IO] = HttpRoutes.of[IO] { + val deleteCounterpartyCorporateLocation: Http4sRoute = Http4sRoute { case req @ DELETE -> `prefixPath` / "banks" / _ / "accounts" / _ / _ / "other_accounts" / otherAccountId / "metadata" / "corporate_location" => EndpointHelpers.withUserDelete(req) { (user, cc) => for { @@ -1738,7 +1738,7 @@ object Http4s121 { // ─── addCounterpartyPhysicalLocation ────────────────────────────────────── - val addCounterpartyPhysicalLocation: HttpRoutes[IO] = HttpRoutes.of[IO] { + val addCounterpartyPhysicalLocation: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "banks" / _ / "accounts" / _ / _ / "other_accounts" / otherAccountId / "metadata" / "physical_location" => EndpointHelpers.withViewCreated(req) { (user, account, view, cc) => val bodyStr = cc.httpBody.getOrElse("") @@ -1766,7 +1766,7 @@ object Http4s121 { // ─── updateCounterpartyPhysicalLocation ─────────────────────────────────── - val updateCounterpartyPhysicalLocation: HttpRoutes[IO] = HttpRoutes.of[IO] { + val updateCounterpartyPhysicalLocation: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "banks" / _ / "accounts" / _ / _ / "other_accounts" / otherAccountId / "metadata" / "physical_location" => EndpointHelpers.withView(req) { (user, account, view, cc) => val bodyStr = cc.httpBody.getOrElse("") @@ -1794,7 +1794,7 @@ object Http4s121 { // ─── deleteCounterpartyPhysicalLocation ─────────────────────────────────── - val deleteCounterpartyPhysicalLocation: HttpRoutes[IO] = HttpRoutes.of[IO] { + val deleteCounterpartyPhysicalLocation: Http4sRoute = Http4sRoute { case req @ DELETE -> `prefixPath` / "banks" / _ / "accounts" / _ / _ / "other_accounts" / otherAccountId / "metadata" / "physical_location" => EndpointHelpers.withUserDelete(req) { (user, cc) => for { @@ -1821,7 +1821,7 @@ object Http4s121 { // ─── getTransactionsForBankAccount ─────────────────────────────────────── - val getTransactionsForBankAccount: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getTransactionsForBankAccount: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / bankId / "accounts" / accountId / viewId / "transactions" => EndpointHelpers.executeAndRespond(req) { cc => val httpParams: List[HTTPParam] = req.headers.headers.toList.map(h => HTTPParam(h.name.toString, h.value)) @@ -1857,7 +1857,7 @@ object Http4s121 { // ─── getTransactionByIdForBankAccount ───────────────────────────────────── - val getTransactionByIdForBankAccount: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getTransactionByIdForBankAccount: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / bankId / "accounts" / accountId / viewId / "transactions" / transactionId / "transaction" => EndpointHelpers.executeAndRespond(req) { cc => for { @@ -1890,7 +1890,7 @@ object Http4s121 { // ─── getTransactionNarrative ────────────────────────────────────────────── - val getTransactionNarrative: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getTransactionNarrative: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "accounts" / _ / _ / "transactions" / transactionId / "metadata" / "narrative" => EndpointHelpers.withView(req) { (user, account, view, cc) => for { @@ -1922,7 +1922,7 @@ object Http4s121 { // ─── addTransactionNarrative ────────────────────────────────────────────── - val addTransactionNarrative: HttpRoutes[IO] = HttpRoutes.of[IO] { + val addTransactionNarrative: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "banks" / _ / "accounts" / _ / _ / "transactions" / transactionId / "metadata" / "narrative" => EndpointHelpers.executeFutureCreated(req) { val cc = req.callContext @@ -1985,7 +1985,7 @@ object Http4s121 { // ─── updateTransactionNarrative ─────────────────────────────────────────── - val updateTransactionNarrative: HttpRoutes[IO] = HttpRoutes.of[IO] { + val updateTransactionNarrative: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "banks" / _ / "accounts" / _ / _ / "transactions" / transactionId / "metadata" / "narrative" => EndpointHelpers.executeFuture(req) { val cc = req.callContext @@ -2038,7 +2038,7 @@ object Http4s121 { // ─── deleteTransactionNarrative ─────────────────────────────────────────── - val deleteTransactionNarrative: HttpRoutes[IO] = HttpRoutes.of[IO] { + val deleteTransactionNarrative: Http4sRoute = Http4sRoute { case req @ DELETE -> `prefixPath` / "banks" / _ / "accounts" / _ / _ / "transactions" / transactionId / "metadata" / "narrative" => EndpointHelpers.withUserDelete(req) { (user, cc) => val bankAccount = cc.bankAccount.get @@ -2067,7 +2067,7 @@ object Http4s121 { // ─── getCommentsForViewOnTransaction ───────────────────────────────────── - val getCommentsForViewOnTransaction: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getCommentsForViewOnTransaction: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "accounts" / _ / _ / "transactions" / transactionId / "metadata" / "comments" => EndpointHelpers.withView(req) { (user, account, view, cc) => for { @@ -2094,7 +2094,7 @@ object Http4s121 { // ─── addCommentForViewOnTransaction ─────────────────────────────────────── - val addCommentForViewOnTransaction: HttpRoutes[IO] = HttpRoutes.of[IO] { + val addCommentForViewOnTransaction: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "banks" / _ / "accounts" / _ / _ / "transactions" / transactionId / "metadata" / "comments" => EndpointHelpers.executeFutureCreated(req) { val cc = req.callContext @@ -2143,7 +2143,7 @@ object Http4s121 { // ─── deleteCommentForViewOnTransaction ──────────────────────────────────── - val deleteCommentForViewOnTransaction: HttpRoutes[IO] = HttpRoutes.of[IO] { + val deleteCommentForViewOnTransaction: Http4sRoute = Http4sRoute { case req @ DELETE -> `prefixPath` / "banks" / _ / "accounts" / _ / _ / "transactions" / transactionId / "metadata" / "comments" / commentId => EndpointHelpers.withUserDelete(req) { (user, cc) => val bankAccount = cc.bankAccount.get @@ -2173,7 +2173,7 @@ object Http4s121 { // ─── getTagsForViewOnTransaction ───────────────────────────────────────── - val getTagsForViewOnTransaction: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getTagsForViewOnTransaction: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "accounts" / _ / _ / "transactions" / transactionId / "metadata" / "tags" => EndpointHelpers.withView(req) { (user, account, view, cc) => for { @@ -2205,7 +2205,7 @@ object Http4s121 { // ─── addTagForViewOnTransaction ─────────────────────────────────────────── - val addTagForViewOnTransaction: HttpRoutes[IO] = HttpRoutes.of[IO] { + val addTagForViewOnTransaction: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "banks" / _ / "accounts" / _ / _ / "transactions" / transactionId / "metadata" / "tags" => EndpointHelpers.executeFutureCreated(req) { val cc = req.callContext @@ -2254,7 +2254,7 @@ object Http4s121 { // ─── deleteTagForViewOnTransaction ──────────────────────────────────────── - val deleteTagForViewOnTransaction: HttpRoutes[IO] = HttpRoutes.of[IO] { + val deleteTagForViewOnTransaction: Http4sRoute = Http4sRoute { case req @ DELETE -> `prefixPath` / "banks" / _ / "accounts" / _ / _ / "transactions" / transactionId / "metadata" / "tags" / tagId => EndpointHelpers.withUserDelete(req) { (user, cc) => val bankAccount = cc.bankAccount.get @@ -2289,7 +2289,7 @@ object Http4s121 { // ─── getImagesForViewOnTransaction ──────────────────────────────────────── - val getImagesForViewOnTransaction: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getImagesForViewOnTransaction: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "accounts" / _ / _ / "transactions" / transactionId / "metadata" / "images" => EndpointHelpers.withView(req) { (user, account, view, cc) => for { @@ -2315,7 +2315,7 @@ object Http4s121 { // ─── addImageForViewOnTransaction ───────────────────────────────────────── - val addImageForViewOnTransaction: HttpRoutes[IO] = HttpRoutes.of[IO] { + val addImageForViewOnTransaction: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "banks" / _ / "accounts" / _ / _ / "transactions" / transactionId / "metadata" / "images" => EndpointHelpers.executeFutureCreated(req) { val cc = req.callContext @@ -2373,7 +2373,7 @@ object Http4s121 { // ─── deleteImageForViewOnTransaction ───────────────────────────────────── - val deleteImageForViewOnTransaction: HttpRoutes[IO] = HttpRoutes.of[IO] { + val deleteImageForViewOnTransaction: Http4sRoute = Http4sRoute { case req @ DELETE -> `prefixPath` / "banks" / _ / "accounts" / _ / _ / "transactions" / transactionId / "metadata" / "images" / imageId => EndpointHelpers.withUserDelete(req) { (user, cc) => val bankAccount = cc.bankAccount.get @@ -2411,7 +2411,7 @@ object Http4s121 { // ─── getWhereTagForViewOnTransaction ───────────────────────────────────── - val getWhereTagForViewOnTransaction: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getWhereTagForViewOnTransaction: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "accounts" / _ / _ / "transactions" / transactionId / "metadata" / "where" => EndpointHelpers.withView(req) { (user, account, view, cc) => for { @@ -2447,7 +2447,7 @@ object Http4s121 { // ─── addWhereTagForViewOnTransaction ───────────────────────────────────── - val addWhereTagForViewOnTransaction: HttpRoutes[IO] = HttpRoutes.of[IO] { + val addWhereTagForViewOnTransaction: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "banks" / _ / "accounts" / _ / _ / "transactions" / transactionId / "metadata" / "where" => EndpointHelpers.executeFutureCreated(req) { val cc = req.callContext @@ -2499,7 +2499,7 @@ object Http4s121 { // ─── updateWhereTagForViewOnTransaction ─────────────────────────────────── - val updateWhereTagForViewOnTransaction: HttpRoutes[IO] = HttpRoutes.of[IO] { + val updateWhereTagForViewOnTransaction: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "banks" / _ / "accounts" / _ / _ / "transactions" / transactionId / "metadata" / "where" => EndpointHelpers.executeFuture(req) { val cc = req.callContext @@ -2551,7 +2551,7 @@ object Http4s121 { // ─── deleteWhereTagForViewOnTransaction ─────────────────────────────────── - val deleteWhereTagForViewOnTransaction: HttpRoutes[IO] = HttpRoutes.of[IO] { + val deleteWhereTagForViewOnTransaction: Http4sRoute = Http4sRoute { case req @ DELETE -> `prefixPath` / "banks" / _ / "accounts" / _ / _ / "transactions" / transactionId / "metadata" / "where" => EndpointHelpers.withUserDelete(req) { (user, cc) => val bankAccount = cc.bankAccount.get @@ -2591,7 +2591,7 @@ object Http4s121 { // ─── getOtherAccountForTransaction ─────────────────────────────────────── - val getOtherAccountForTransaction: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getOtherAccountForTransaction: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "accounts" / _ / _ / "transactions" / transactionId / "other_account" => EndpointHelpers.withView(req) { (user, account, view, cc) => for { @@ -2622,82 +2622,87 @@ object Http4s121 { // ─── allRoutes ──────────────────────────────────────────────────────────── - val allRoutes: HttpRoutes[IO] = - Kleisli[HttpF, Request[IO], Response[IO]] { req: Request[IO] => - root(req) - .orElse(getBanks(req)) - // bankById is intentionally absent — it runs outside middleware (see allRoutesWithMiddleware) - .orElse(getPrivateAccountsAllBanks(req)) - .orElse(privateAccountsAllBanks(req)) - .orElse(publicAccountsAllBanks(req)) - .orElse(getPrivateAccountsAtOneBank(req)) - .orElse(privateAccountsAtOneBank(req)) - .orElse(publicAccountsAtOneBank(req)) - .orElse(accountById(req)) - .orElse(updateAccountLabel(req)) - .orElse(getViewsForBankAccount(req)) - .orElse(createViewForBankAccount(req)) - .orElse(updateViewForBankAccount(req)) - .orElse(deleteViewForBankAccount(req)) - .orElse(getPermissionsForBankAccount(req)) - .orElse(getPermissionForUserForBankAccount(req)) - .orElse(addPermissionForUserForBankAccountForMultipleViews(req)) - .orElse(addPermissionForUserForBankAccountForOneView(req)) - .orElse(removePermissionForUserForBankAccountForOneView(req)) - .orElse(removePermissionForUserForBankAccountForAllViews(req)) - .orElse(getOtherAccountsForBankAccount(req)) - .orElse(getOtherAccountByIdForBankAccount(req)) - .orElse(getOtherAccountMetadata(req)) - .orElse(getCounterpartyPublicAlias(req)) - .orElse(addCounterpartyPublicAlias(req)) - .orElse(updateCounterpartyPublicAlias(req)) - .orElse(deleteCounterpartyPublicAlias(req)) - .orElse(getOtherAccountPrivateAlias(req)) - .orElse(addOtherAccountPrivateAlias(req)) - .orElse(updateCounterpartyPrivateAlias(req)) - .orElse(deleteCounterpartyPrivateAlias(req)) - .orElse(addCounterpartyMoreInfo(req)) - .orElse(updateCounterpartyMoreInfo(req)) - .orElse(deleteCounterpartyMoreInfo(req)) - .orElse(addCounterpartyUrl(req)) - .orElse(updateCounterpartyUrl(req)) - .orElse(deleteCounterpartyUrl(req)) - .orElse(addCounterpartyImageUrl(req)) - .orElse(updateCounterpartyImageUrl(req)) - .orElse(deleteCounterpartyImageUrl(req)) - .orElse(addCounterpartyOpenCorporatesUrl(req)) - .orElse(updateCounterpartyOpenCorporatesUrl(req)) - .orElse(deleteCounterpartyOpenCorporatesUrl(req)) - .orElse(addCounterpartyCorporateLocation(req)) - .orElse(updateCounterpartyCorporateLocation(req)) - .orElse(deleteCounterpartyCorporateLocation(req)) - .orElse(addCounterpartyPhysicalLocation(req)) - .orElse(updateCounterpartyPhysicalLocation(req)) - .orElse(deleteCounterpartyPhysicalLocation(req)) - .orElse(getTransactionsForBankAccount(req)) - .orElse(getTransactionByIdForBankAccount(req)) - .orElse(getTransactionNarrative(req)) - .orElse(addTransactionNarrative(req)) - .orElse(updateTransactionNarrative(req)) - .orElse(deleteTransactionNarrative(req)) - .orElse(getCommentsForViewOnTransaction(req)) - .orElse(addCommentForViewOnTransaction(req)) - .orElse(deleteCommentForViewOnTransaction(req)) - .orElse(getTagsForViewOnTransaction(req)) - .orElse(addTagForViewOnTransaction(req)) - .orElse(deleteTagForViewOnTransaction(req)) - .orElse(getImagesForViewOnTransaction(req)) - .orElse(addImageForViewOnTransaction(req)) - .orElse(deleteImageForViewOnTransaction(req)) - .orElse(getWhereTagForViewOnTransaction(req)) - .orElse(addWhereTagForViewOnTransaction(req)) - .orElse(updateWhereTagForViewOnTransaction(req)) - .orElse(deleteWhereTagForViewOnTransaction(req)) - .orElse(getOtherAccountForTransaction(req)) - } - - val allRoutesWithMiddleware: HttpRoutes[IO] = { - val middlewareWrapped = ResourceDocMiddleware.apply(resourceDocs)(IdempotencyMiddleware(allRoutes)) + // The routes in the order they are tried. ResourceDocMiddleware selects the doc of the first + // route that serves a request, so it is given the docs in this same order. + lazy val routesInOrder: List[Http4sHandler] = List( + root, + getBanks, + // bankById is intentionally absent — it runs outside middleware (see allRoutesWithMiddleware) + getPrivateAccountsAllBanks, + privateAccountsAllBanks, + publicAccountsAllBanks, + getPrivateAccountsAtOneBank, + privateAccountsAtOneBank, + publicAccountsAtOneBank, + accountById, + updateAccountLabel, + getViewsForBankAccount, + createViewForBankAccount, + updateViewForBankAccount, + deleteViewForBankAccount, + getPermissionsForBankAccount, + getPermissionForUserForBankAccount, + addPermissionForUserForBankAccountForMultipleViews, + addPermissionForUserForBankAccountForOneView, + removePermissionForUserForBankAccountForOneView, + removePermissionForUserForBankAccountForAllViews, + getOtherAccountsForBankAccount, + getOtherAccountByIdForBankAccount, + getOtherAccountMetadata, + getCounterpartyPublicAlias, + addCounterpartyPublicAlias, + updateCounterpartyPublicAlias, + deleteCounterpartyPublicAlias, + getOtherAccountPrivateAlias, + addOtherAccountPrivateAlias, + updateCounterpartyPrivateAlias, + deleteCounterpartyPrivateAlias, + addCounterpartyMoreInfo, + updateCounterpartyMoreInfo, + deleteCounterpartyMoreInfo, + addCounterpartyUrl, + updateCounterpartyUrl, + deleteCounterpartyUrl, + addCounterpartyImageUrl, + updateCounterpartyImageUrl, + deleteCounterpartyImageUrl, + addCounterpartyOpenCorporatesUrl, + updateCounterpartyOpenCorporatesUrl, + deleteCounterpartyOpenCorporatesUrl, + addCounterpartyCorporateLocation, + updateCounterpartyCorporateLocation, + deleteCounterpartyCorporateLocation, + addCounterpartyPhysicalLocation, + updateCounterpartyPhysicalLocation, + deleteCounterpartyPhysicalLocation, + getTransactionsForBankAccount, + getTransactionByIdForBankAccount, + getTransactionNarrative, + addTransactionNarrative, + updateTransactionNarrative, + deleteTransactionNarrative, + getCommentsForViewOnTransaction, + addCommentForViewOnTransaction, + deleteCommentForViewOnTransaction, + getTagsForViewOnTransaction, + addTagForViewOnTransaction, + deleteTagForViewOnTransaction, + getImagesForViewOnTransaction, + addImageForViewOnTransaction, + deleteImageForViewOnTransaction, + getWhereTagForViewOnTransaction, + addWhereTagForViewOnTransaction, + updateWhereTagForViewOnTransaction, + deleteWhereTagForViewOnTransaction, + getOtherAccountForTransaction + ) + + lazy val orderedResourceDocs: ArrayBuffer[ResourceDoc] = ResourceDocMatcher.orderByRoutes(resourceDocs, routesInOrder, outsideTheChain = List(bankById)) + + lazy val allRoutes: HttpRoutes[IO] = Http4sRoute.chain(routesInOrder) + + lazy val allRoutesWithMiddleware: HttpRoutes[IO] = { + val middlewareWrapped = ResourceDocMiddleware.apply(orderedResourceDocs, routes => IdempotencyMiddleware(routes))(IdempotencyMiddleware(allRoutes)) // bankById runs before middleware so it can return 400 (not 404) for unknown bank Kleisli[HttpF, Request[IO], Response[IO]] { req => bankById.run(req).orElse(middlewareWrapped.run(req)) diff --git a/obp-api/src/main/scala/code/api/v1_3_0/Http4s130.scala b/obp-api/src/main/scala/code/api/v1_3_0/Http4s130.scala index eaa7e49dde..5ab9911b6c 100644 --- a/obp-api/src/main/scala/code/api/v1_3_0/Http4s130.scala +++ b/obp-api/src/main/scala/code/api/v1_3_0/Http4s130.scala @@ -31,11 +31,11 @@ import cats.data.{Kleisli, OptionT} import cats.effect._ import code.api.Constant._ import code.api.ResourceDocs1_4_0.SwaggerDefinitionsJSON._ -import code.api.util.APIUtil.{EmptyBody, ResourceDoc, _} +import code.api.util.APIUtil.{Http4sHandler, Http4sRoute, EmptyBody, ResourceDoc, _} import code.api.util.ApiTag._ import code.api.util.ErrorMessages._ import code.api.util.http4s.Http4sRequestAttributes.{EndpointHelpers, RequestOps} -import code.api.util.http4s.ResourceDocMiddleware +import code.api.util.http4s.{ResourceDocMatcher, ResourceDocMiddleware} import code.api.util.http4s.IdempotencyMiddleware import code.api.util.NewStyle import code.api.v1_2_1.JSONFactory @@ -62,7 +62,7 @@ object Http4s130 { // ─── root ──────────────────────────────────────────────────────────────── - val root: HttpRoutes[IO] = HttpRoutes.of[IO] { + val root: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` => EndpointHelpers.executeAndRespond(req) { _ => Future.successful(JSONFactory.getApiInfoJSON(ApiVersion.v1_3_0, versionStatus)) @@ -94,7 +94,7 @@ object Http4s130 { // ─── getCards ──────────────────────────────────────────────────────────── - val getCards: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getCards: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "cards" => EndpointHelpers.withUser(req) { (u, cc) => NewStyle.function.getPhysicalCardsForUser(u, Some(cc)).map { @@ -120,7 +120,7 @@ object Http4s130 { // ─── getCardsForBank ───────────────────────────────────────────────────── - val getCardsForBank: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getCardsForBank: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "cards" => EndpointHelpers.withUserAndBank(req) { (u, bank, cc) => for { @@ -148,13 +148,19 @@ object Http4s130 { // ─── allRoutes ─────────────────────────────────────────────────────────── - private val allOwnRoutes: HttpRoutes[IO] = Kleisli[HttpF, Request[IO], Response[IO]] { req => - root.run(req) - .orElse(getCards.run(req)) - .orElse(getCardsForBank.run(req)) - } + // The routes in the order they are tried. ResourceDocMiddleware selects the doc of the first + // route that serves a request, so it is given the docs in this same order. + lazy val routesInOrder: List[Http4sHandler] = List( + root, + getCards, + getCardsForBank + ) + + lazy val orderedResourceDocs: ArrayBuffer[ResourceDoc] = ResourceDocMatcher.orderByRoutes(resourceDocs, routesInOrder) + + private lazy val allOwnRoutes: HttpRoutes[IO] = Http4sRoute.chain(routesInOrder) - val allRoutesWithMiddleware: HttpRoutes[IO] = ResourceDocMiddleware.apply(resourceDocs)(IdempotencyMiddleware(allOwnRoutes)) + lazy val allRoutesWithMiddleware: HttpRoutes[IO] = ResourceDocMiddleware.apply(orderedResourceDocs, routes => IdempotencyMiddleware(routes))(IdempotencyMiddleware(allOwnRoutes)) // ─── path-rewriting bridge: /obp/v1.3.0/… → /obp/v1.2.1/… ───────────── // Delegates to Http4s121 so all inherited v1.2.1 endpoints are served diff --git a/obp-api/src/main/scala/code/api/v1_4_0/Http4s140.scala b/obp-api/src/main/scala/code/api/v1_4_0/Http4s140.scala index f6b69009a1..e444b47381 100644 --- a/obp-api/src/main/scala/code/api/v1_4_0/Http4s140.scala +++ b/obp-api/src/main/scala/code/api/v1_4_0/Http4s140.scala @@ -31,12 +31,12 @@ import cats.data.{Kleisli, OptionT} import cats.effect._ import code.api.Constant._ import code.api.ResourceDocs1_4_0.SwaggerDefinitionsJSON._ -import code.api.util.APIUtil.{EmptyBody, ResourceDoc, _} +import code.api.util.APIUtil.{Http4sHandler, Http4sRoute, EmptyBody, ResourceDoc, _} import code.api.util.ApiRole._ import code.api.util.ApiTag._ import code.api.util.ErrorMessages._ import code.api.util.http4s.Http4sRequestAttributes.{EndpointHelpers, RequestOps} -import code.api.util.http4s.ResourceDocMiddleware +import code.api.util.http4s.{ResourceDocMatcher, ResourceDocMiddleware} import code.api.util.http4s.IdempotencyMiddleware import code.api.util.{APIUtil, NewStyle} import code.api.v1_2_1.{JSONFactory, SuccessMessage} @@ -69,7 +69,7 @@ object Http4s140 { // ─── root ───────────────────────────────────────────────────────────────── - val root: HttpRoutes[IO] = HttpRoutes.of[IO] { + val root: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` => EndpointHelpers.executeAndRespond(req) { _ => Future.successful(JSONFactory.getApiInfoJSON(ApiVersion.v1_4_0, versionStatus)) @@ -101,7 +101,7 @@ object Http4s140 { // ─── getCustomer ────────────────────────────────────────────────────────── - val getCustomer: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getCustomer: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "customer" => EndpointHelpers.withUserAndBank(req) { (user, bank, cc) => for { @@ -134,7 +134,7 @@ object Http4s140 { // ─── getCustomersMessages ───────────────────────────────────────────────── - val getCustomersMessages: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getCustomersMessages: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "customer" / "messages" => EndpointHelpers.withUserAndBank(req) { (user, bank, cc) => Future { @@ -164,7 +164,7 @@ object Http4s140 { // ─── addCustomerMessage ─────────────────────────────────────────────────── - val addCustomerMessage: HttpRoutes[IO] = HttpRoutes.of[IO] { + val addCustomerMessage: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "banks" / _ / "customer" / customerId / "messages" => EndpointHelpers.withUserAndBankAndBodyCreated[JSONFactory1_4_0.AddCustomerMessageJson, SuccessMessage](req) { (_, bank, body, cc) => for { @@ -195,7 +195,7 @@ object Http4s140 { private val getBranchesIsPublic = APIUtil.getPropsAsBoolValue("apiOptions.getBranchesIsPublic", true) - val getBranches: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getBranches: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "branches" => EndpointHelpers.withBank(req) { (bank, cc) => for { @@ -239,7 +239,7 @@ object Http4s140 { private val getAtmsIsPublic = APIUtil.getPropsAsBoolValue("apiOptions.getAtmsIsPublic", true) - val getAtms: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getAtms: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "atms" => EndpointHelpers.withBank(req) { (bank, cc) => for { @@ -280,7 +280,7 @@ object Http4s140 { private val getProductsIsPublic = APIUtil.getPropsAsBoolValue("apiOptions.getProductsIsPublic", true) - val getProducts: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getProducts: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "products" => EndpointHelpers.withBank(req) { (bank, cc) => Future { @@ -319,7 +319,7 @@ object Http4s140 { // ─── getCrmEvents ───────────────────────────────────────────────────────── - val getCrmEvents: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getCrmEvents: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "crm-events" => EndpointHelpers.withUserAndBank(req) { (_, bank, cc) => NewStyle.function.getCrmEvents(bank.bankId, Some(cc)) @@ -344,7 +344,7 @@ object Http4s140 { // ─── getTransactionRequestTypes ─────────────────────────────────────────── - val getTransactionRequestTypes: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getTransactionRequestTypes: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "accounts" / _ / _ / "transaction-request-types" => EndpointHelpers.withView(req) { (user, fromAccount, view, cc) => for { @@ -408,7 +408,7 @@ object Http4s140 { // ─── addCustomer ───────────────────────────────────────────────────────── - val addCustomer: HttpRoutes[IO] = HttpRoutes.of[IO] { + val addCustomer: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "banks" / _ / "customer" => EndpointHelpers.withUserAndBankAndBody[code.api.v2_0_0.CreateCustomerJson, JSONFactory1_4_0.CustomerJsonV140](req) { (user, bank, body, cc) => for { @@ -499,20 +499,26 @@ object Http4s140 { // ─── allRoutes ──────────────────────────────────────────────────────────── - private val allOwnRoutes: HttpRoutes[IO] = Kleisli[HttpF, Request[IO], Response[IO]] { req => - root.run(req) - .orElse(getCustomer.run(req)) - .orElse(getCustomersMessages.run(req)) - .orElse(addCustomerMessage.run(req)) - .orElse(getBranches.run(req)) - .orElse(getAtms.run(req)) - .orElse(getProducts.run(req)) - .orElse(getCrmEvents.run(req)) - .orElse(getTransactionRequestTypes.run(req)) - .orElse(addCustomer.run(req)) - } + // The routes in the order they are tried. ResourceDocMiddleware selects the doc of the first + // route that serves a request, so it is given the docs in this same order. + lazy val routesInOrder: List[Http4sHandler] = List( + root, + getCustomer, + getCustomersMessages, + addCustomerMessage, + getBranches, + getAtms, + getProducts, + getCrmEvents, + getTransactionRequestTypes, + addCustomer + ) + + lazy val orderedResourceDocs: ArrayBuffer[ResourceDoc] = ResourceDocMatcher.orderByRoutes(resourceDocs, routesInOrder) + + private lazy val allOwnRoutes: HttpRoutes[IO] = Http4sRoute.chain(routesInOrder) - val allRoutesWithMiddleware: HttpRoutes[IO] = ResourceDocMiddleware.apply(resourceDocs)(IdempotencyMiddleware(allOwnRoutes)) + lazy val allRoutesWithMiddleware: HttpRoutes[IO] = ResourceDocMiddleware.apply(orderedResourceDocs, routes => IdempotencyMiddleware(routes))(IdempotencyMiddleware(allOwnRoutes)) // ─── path-rewriting bridge: /obp/v1.4.0/… → /obp/v1.3.0/… ────────────── // Delegates to Http4s130 so all inherited v1.3.0 and v1.2.1 endpoints are diff --git a/obp-api/src/main/scala/code/api/v2_0_0/Http4s200.scala b/obp-api/src/main/scala/code/api/v2_0_0/Http4s200.scala index b4b6e63cfd..4820642d24 100644 --- a/obp-api/src/main/scala/code/api/v2_0_0/Http4s200.scala +++ b/obp-api/src/main/scala/code/api/v2_0_0/Http4s200.scala @@ -34,12 +34,12 @@ import code.TransactionTypes.TransactionType import code.api.APIFailureNewStyle import code.api.Constant._ import code.api.ResourceDocs1_4_0.SwaggerDefinitionsJSON._ -import code.api.util.APIUtil.{EmptyBody, ResourceDoc, _} +import code.api.util.APIUtil.{Http4sHandler, Http4sRoute, EmptyBody, ResourceDoc, _} import code.api.util.ApiRole._ import code.api.util.ApiTag._ import code.api.util.ErrorMessages._ import code.api.util.http4s.Http4sRequestAttributes.{EndpointHelpers, RequestOps} -import code.api.util.http4s.ResourceDocMiddleware +import code.api.util.http4s.{ResourceDocMatcher, ResourceDocMiddleware} import code.api.util.http4s.IdempotencyMiddleware import code.api.util.newstyle.ViewNewStyle import code.api.util.{APIUtil, ApiRole, CustomJsonFormats, NewStyle} @@ -96,7 +96,7 @@ object Http4s200 { // ─── root ───────────────────────────────────────────────────────────────── - val root: HttpRoutes[IO] = HttpRoutes.of[IO] { + val root: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` => EndpointHelpers.executeAndRespond(req) { _ => Future.successful(JSONFactory121.getApiInfoJSON(ApiVersion.v2_0_0, versionStatus)) @@ -121,7 +121,7 @@ object Http4s200 { // ─── getPrivateAccountsAllBanks ─────────────────────────────────────────── - val getPrivateAccountsAllBanks: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getPrivateAccountsAllBanks: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "accounts" => EndpointHelpers.withUser(req) { (user, cc) => Future { @@ -147,7 +147,7 @@ object Http4s200 { // ─── corePrivateAccountsAllBanks ────────────────────────────────────────── - val corePrivateAccountsAllBanks: HttpRoutes[IO] = HttpRoutes.of[IO] { + val corePrivateAccountsAllBanks: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "my" / "accounts" => EndpointHelpers.withUser(req) { (user, cc) => Future { @@ -175,7 +175,7 @@ object Http4s200 { // ─── publicAccountsAllBanks ─────────────────────────────────────────────── - val publicAccountsAllBanks: HttpRoutes[IO] = HttpRoutes.of[IO] { + val publicAccountsAllBanks: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "accounts" / "public" => EndpointHelpers.executeAndRespond(req) { _ => Future { @@ -221,7 +221,7 @@ object Http4s200 { // ─── getPrivateAccountsAtOneBank ────────────────────────────────────────── - val getPrivateAccountsAtOneBank: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getPrivateAccountsAtOneBank: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "accounts" => EndpointHelpers.withUserAndBank(req) { (user, bank, cc) => for { @@ -261,7 +261,7 @@ object Http4s200 { // ─── corePrivateAccountsAtOneBank ───────────────────────────────────────── - val corePrivateAccountsAtOneBank: HttpRoutes[IO] = HttpRoutes.of[IO] { + val corePrivateAccountsAtOneBank: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "my" / "banks" / _ / "accounts" => EndpointHelpers.withUserAndBank(req) { (user, bank, cc) => for { @@ -315,7 +315,7 @@ object Http4s200 { // ─── privateAccountsAtOneBank ───────────────────────────────────────────── - val privateAccountsAtOneBank: HttpRoutes[IO] = HttpRoutes.of[IO] { + val privateAccountsAtOneBank: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "accounts" / "private" => EndpointHelpers.withUserAndBank(req) { (user, bank, cc) => for { @@ -347,7 +347,7 @@ object Http4s200 { // ─── publicAccountsAtOneBank ────────────────────────────────────────────── - val publicAccountsAtOneBank: HttpRoutes[IO] = HttpRoutes.of[IO] { + val publicAccountsAtOneBank: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "accounts" / "public" => EndpointHelpers.withBank(req) { (bank, cc) => Future { @@ -379,7 +379,7 @@ object Http4s200 { // ─── getKycDocuments ────────────────────────────────────────────────────── - val getKycDocuments: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getKycDocuments: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "customers" / customerId / "kyc_documents" => EndpointHelpers.withUser(req) { (user, cc) => for { @@ -407,7 +407,7 @@ object Http4s200 { // ─── getKycMedia ────────────────────────────────────────────────────────── - val getKycMedia: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getKycMedia: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "customers" / customerId / "kyc_media" => EndpointHelpers.withUser(req) { (user, cc) => for { @@ -434,7 +434,7 @@ object Http4s200 { // ─── getKycChecks ───────────────────────────────────────────────────────── - val getKycChecks: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getKycChecks: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "customers" / customerId / "kyc_checks" => EndpointHelpers.withUser(req) { (user, cc) => for { @@ -461,7 +461,7 @@ object Http4s200 { // ─── getKycStatuses ─────────────────────────────────────────────────────── - val getKycStatuses: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getKycStatuses: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "customers" / customerId / "kyc_statuses" => EndpointHelpers.withUser(req) { (user, cc) => for { @@ -488,7 +488,7 @@ object Http4s200 { // ─── getSocialMediaHandles ──────────────────────────────────────────────── - val getSocialMediaHandles: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getSocialMediaHandles: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "customers" / customerId / "social_media_handles" => EndpointHelpers.withUserAndBank(req) { (user, bank, cc) => for { @@ -518,7 +518,7 @@ object Http4s200 { // ─── addKycDocument ─────────────────────────────────────────────────────── - val addKycDocument: HttpRoutes[IO] = HttpRoutes.of[IO] { + val addKycDocument: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "banks" / _ / "customers" / customerId / "kyc_documents" / documentId => EndpointHelpers.withUserAndBankAndBodyCreated[PostKycDocumentJSON, KycDocumentJSON](req) { (user, bank, body, cc) => for { @@ -547,7 +547,7 @@ object Http4s200 { // ─── addKycMedia ────────────────────────────────────────────────────────── - val addKycMedia: HttpRoutes[IO] = HttpRoutes.of[IO] { + val addKycMedia: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "banks" / _ / "customers" / customerId / "kyc_media" / mediaId => EndpointHelpers.withUserAndBankAndBodyCreated[PostKycMediaJSON, KycMediaJSON](req) { (user, bank, body, cc) => for { @@ -576,7 +576,7 @@ object Http4s200 { // ─── addKycCheck ────────────────────────────────────────────────────────── - val addKycCheck: HttpRoutes[IO] = HttpRoutes.of[IO] { + val addKycCheck: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "banks" / _ / "customers" / customerId / "kyc_check" / checkId => EndpointHelpers.withUserAndBankAndBodyCreated[PostKycCheckJSON, KycCheckJSON](req) { (user, bank, body, cc) => for { @@ -605,7 +605,7 @@ object Http4s200 { // ─── addKycStatus ───────────────────────────────────────────────────────── - val addKycStatus: HttpRoutes[IO] = HttpRoutes.of[IO] { + val addKycStatus: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "banks" / _ / "customers" / customerId / "kyc_statuses" => EndpointHelpers.withUserAndBankAndBodyCreated[PostKycStatusJSON, KycStatusJSON](req) { (user, bank, body, cc) => for { @@ -633,7 +633,7 @@ object Http4s200 { // ─── addSocialMediaHandle ───────────────────────────────────────────────── - val addSocialMediaHandle: HttpRoutes[IO] = HttpRoutes.of[IO] { + val addSocialMediaHandle: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "banks" / _ / "customers" / customerId / "social_media_handles" => EndpointHelpers.withUserAndBankAndBodyCreated[SocialMediaJSON, SuccessMessage](req) { (user, bank, body, cc) => for { @@ -666,7 +666,7 @@ object Http4s200 { // ─── getCoreAccountById ─────────────────────────────────────────────────── - val getCoreAccountById: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getCoreAccountById: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "my" / "banks" / _ / "accounts" / _ / "account" => EndpointHelpers.withBankAccount(req) { (user, account, cc) => for { @@ -702,7 +702,7 @@ object Http4s200 { // ─── getCoreTransactionsForBankAccount ──────────────────────────────────── - val getCoreTransactionsForBankAccount: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getCoreTransactionsForBankAccount: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "my" / "banks" / _ / "accounts" / _ / "transactions" => EndpointHelpers.withBankAccount(req) { (user, account, cc) => for { @@ -735,7 +735,7 @@ object Http4s200 { // ─── accountById ────────────────────────────────────────────────────────── - val accountById: HttpRoutes[IO] = HttpRoutes.of[IO] { + val accountById: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "accounts" / _ / _ / "account" => EndpointHelpers.withView(req) { (user, account, view, cc) => for { @@ -782,7 +782,7 @@ object Http4s200 { // ─── getPermissionsForBankAccount ───────────────────────────────────────── - val getPermissionsForBankAccount: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getPermissionsForBankAccount: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "accounts" / _ / "permissions" => EndpointHelpers.withBankAccount(req) { (user, account, cc) => val bankIdAccountId = BankIdAccountId(account.bankId, account.accountId) @@ -816,7 +816,7 @@ object Http4s200 { // ─── getPermissionForUserForBankAccount ─────────────────────────────────── - val getPermissionForUserForBankAccount: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getPermissionForUserForBankAccount: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "accounts" / _ / "permissions" / provider / providerId => EndpointHelpers.withBankAccount(req) { (user, account, cc) => val bankIdAccountId = BankIdAccountId(account.bankId, account.accountId) @@ -861,7 +861,7 @@ object Http4s200 { // ─── createAccount ──────────────────────────────────────────────────────── - val createAccount: HttpRoutes[IO] = HttpRoutes.of[IO] { + val createAccount: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "banks" / _ / "accounts" / _ => EndpointHelpers.withUserAndBankAndBody[CreateAccountJSON, CoreAccountJSON](req) { (user, bank, body, cc) => for { @@ -935,7 +935,7 @@ object Http4s200 { private val getTransactionTypesIsPublic = APIUtil.getPropsAsBoolValue("apiOptions.getTransactionTypesIsPublic", true) - val getTransactionTypes: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getTransactionTypes: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "transaction-types" => EndpointHelpers.withBank(req) { (bank, cc) => Future { @@ -969,7 +969,7 @@ object Http4s200 { // ─── createUser ─────────────────────────────────────────────────────────── - val createUser: HttpRoutes[IO] = HttpRoutes.of[IO] { + val createUser: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "users" => EndpointHelpers.executeFutureWithBodyCreated[CreateUserJson, JSONFactory200.UserJsonV200](req) { (body, cc) => for { @@ -1051,7 +1051,7 @@ object Http4s200 { // ─── createCustomer ─────────────────────────────────────────────────────── - val createCustomer: HttpRoutes[IO] = HttpRoutes.of[IO] { + val createCustomer: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "banks" / _ / "customers" => EndpointHelpers.withUserAndBankAndBodyCreated[CreateCustomerJson, JSONFactory1_4_0.CustomerJsonV140](req) { (user, bank, body, cc) => for { @@ -1123,7 +1123,7 @@ object Http4s200 { // ─── getCurrentUser ─────────────────────────────────────────────────────── - val getCurrentUser: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getCurrentUser: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "users" / "current" => EndpointHelpers.withUser(req) { (user, cc) => Future.successful(createUserJSON(user)) @@ -1143,7 +1143,7 @@ object Http4s200 { // ─── getUser ────────────────────────────────────────────────────────────── - val getUser: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getUser: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "users" / userEmail => EndpointHelpers.withUser(req) { (user, cc) => for { @@ -1174,7 +1174,7 @@ object Http4s200 { // ─── createUserCustomerLinks ────────────────────────────────────────────── - val createUserCustomerLinks: HttpRoutes[IO] = HttpRoutes.of[IO] { + val createUserCustomerLinks: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "banks" / _ / "user_customer_links" => EndpointHelpers.withUserAndBankAndBody[CreateUserCustomerLinkJson, UserCustomerLinkJson](req) { (_, bank, body, cc) => for { @@ -1233,7 +1233,7 @@ object Http4s200 { // ─── addEntitlement ─────────────────────────────────────────────────────── - val addEntitlement: HttpRoutes[IO] = HttpRoutes.of[IO] { + val addEntitlement: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "users" / userId / "entitlements" => EndpointHelpers.withUserAndBodyCreated[CreateEntitlementJSON, EntitlementJSON](req) { (user, body, cc) => for { @@ -1299,7 +1299,7 @@ object Http4s200 { // ─── getEntitlements ────────────────────────────────────────────────────── - val getEntitlements: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getEntitlements: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "users" / userId / "entitlements" => EndpointHelpers.withUser(req) { (user, cc) => for { @@ -1330,7 +1330,7 @@ object Http4s200 { // ─── deleteEntitlement ──────────────────────────────────────────────────── - val deleteEntitlement: HttpRoutes[IO] = HttpRoutes.of[IO] { + val deleteEntitlement: Http4sRoute = Http4sRoute { case req @ DELETE -> `prefixPath` / "users" / userId / "entitlement" / entitlementId => EndpointHelpers.withUserDelete(req) { (user, cc) => for { @@ -1373,7 +1373,7 @@ object Http4s200 { // ─── getAllEntitlements ──────────────────────────────────────────────────── - val getAllEntitlements: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getAllEntitlements: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "entitlements" => EndpointHelpers.withUser(req) { (user, cc) => for { @@ -1401,7 +1401,7 @@ object Http4s200 { val esw = new elasticsearchWarehouse - val elasticSearchWarehouse: HttpRoutes[IO] = HttpRoutes.of[IO] { + val elasticSearchWarehouse: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "search" / "warehouse" / queryString => EndpointHelpers.withUser(req) { (user, cc) => for { @@ -1493,7 +1493,7 @@ object Http4s200 { val esm = new elasticsearchMetrics - val elasticSearchMetrics: HttpRoutes[IO] = HttpRoutes.of[IO] { + val elasticSearchMetrics: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "search" / "metrics" / queryString => EndpointHelpers.withUser(req) { (user, cc) => for { @@ -1577,7 +1577,7 @@ object Http4s200 { // ─── getCustomers ───────────────────────────────────────────────────────── - val getCustomers: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getCustomers: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "users" / "current" / "customers" => EndpointHelpers.withUser(req) { (user, cc) => Future { @@ -1601,47 +1601,53 @@ object Http4s200 { // ─── allRoutes ──────────────────────────────────────────────────────────── - private val allOwnRoutes: HttpRoutes[IO] = Kleisli[HttpF, Request[IO], Response[IO]] { req => - root.run(req) - .orElse(getPrivateAccountsAllBanks.run(req)) - .orElse(corePrivateAccountsAllBanks.run(req)) - .orElse(publicAccountsAllBanks.run(req)) - .orElse(getPrivateAccountsAtOneBank.run(req)) - .orElse(corePrivateAccountsAtOneBank.run(req)) - .orElse(privateAccountsAtOneBank.run(req)) - .orElse(publicAccountsAtOneBank.run(req)) - .orElse(getKycDocuments.run(req)) - .orElse(getKycMedia.run(req)) - .orElse(getKycChecks.run(req)) - .orElse(getKycStatuses.run(req)) - .orElse(getSocialMediaHandles.run(req)) - .orElse(addKycDocument.run(req)) - .orElse(addKycMedia.run(req)) - .orElse(addKycCheck.run(req)) - .orElse(addKycStatus.run(req)) - .orElse(addSocialMediaHandle.run(req)) - .orElse(getCoreAccountById.run(req)) - .orElse(getCoreTransactionsForBankAccount.run(req)) - .orElse(accountById.run(req)) - .orElse(getPermissionsForBankAccount.run(req)) - .orElse(getPermissionForUserForBankAccount.run(req)) - .orElse(createAccount.run(req)) - .orElse(getTransactionTypes.run(req)) - .orElse(createUser.run(req)) - .orElse(createCustomer.run(req)) - .orElse(getCustomers.run(req)) - .orElse(getCurrentUser.run(req)) - .orElse(getUser.run(req)) - .orElse(createUserCustomerLinks.run(req)) - .orElse(addEntitlement.run(req)) - .orElse(getEntitlements.run(req)) - .orElse(deleteEntitlement.run(req)) - .orElse(getAllEntitlements.run(req)) - .orElse(elasticSearchWarehouse.run(req)) - .orElse(elasticSearchMetrics.run(req)) - } - - val allRoutesWithMiddleware: HttpRoutes[IO] = ResourceDocMiddleware.apply(resourceDocs)(IdempotencyMiddleware(allOwnRoutes)) + // The routes in the order they are tried. ResourceDocMiddleware selects the doc of the first + // route that serves a request, so it is given the docs in this same order. + lazy val routesInOrder: List[Http4sHandler] = List( + root, + getPrivateAccountsAllBanks, + corePrivateAccountsAllBanks, + publicAccountsAllBanks, + getPrivateAccountsAtOneBank, + corePrivateAccountsAtOneBank, + privateAccountsAtOneBank, + publicAccountsAtOneBank, + getKycDocuments, + getKycMedia, + getKycChecks, + getKycStatuses, + getSocialMediaHandles, + addKycDocument, + addKycMedia, + addKycCheck, + addKycStatus, + addSocialMediaHandle, + getCoreAccountById, + getCoreTransactionsForBankAccount, + accountById, + getPermissionsForBankAccount, + getPermissionForUserForBankAccount, + createAccount, + getTransactionTypes, + createUser, + createCustomer, + getCustomers, + getCurrentUser, + getUser, + createUserCustomerLinks, + addEntitlement, + getEntitlements, + deleteEntitlement, + getAllEntitlements, + elasticSearchWarehouse, + elasticSearchMetrics + ) + + lazy val orderedResourceDocs: ArrayBuffer[ResourceDoc] = ResourceDocMatcher.orderByRoutes(resourceDocs, routesInOrder) + + private lazy val allOwnRoutes: HttpRoutes[IO] = Http4sRoute.chain(routesInOrder) + + lazy val allRoutesWithMiddleware: HttpRoutes[IO] = ResourceDocMiddleware.apply(orderedResourceDocs, routes => IdempotencyMiddleware(routes))(IdempotencyMiddleware(allOwnRoutes)) // ─── path-rewriting bridge: /obp/v2.0.0/… → /obp/v1.4.0/… ────────────── // Delegates to Http4s140 so all inherited v1.4.0/v1.3.0/v1.2.1 endpoints are diff --git a/obp-api/src/main/scala/code/api/v2_1_0/Http4s210.scala b/obp-api/src/main/scala/code/api/v2_1_0/Http4s210.scala index 61337055c8..c20a94f502 100644 --- a/obp-api/src/main/scala/code/api/v2_1_0/Http4s210.scala +++ b/obp-api/src/main/scala/code/api/v2_1_0/Http4s210.scala @@ -33,12 +33,12 @@ import cats.effect._ import code.TransactionTypes.TransactionType import code.api.Constant.{ApiPathZero, CAN_SEE_TRANSACTION_REQUESTS} import code.api.ResourceDocs1_4_0.SwaggerDefinitionsJSON._ -import code.api.util.APIUtil.{EmptyBody, ResourceDoc, _} +import code.api.util.APIUtil.{Http4sHandler, Http4sRoute, EmptyBody, ResourceDoc, _} import code.api.util.ApiRole._ import code.api.util.ApiTag._ import code.api.util.ErrorMessages._ import code.api.util.http4s.Http4sRequestAttributes.{EndpointHelpers, RequestOps} -import code.api.util.http4s.ResourceDocMiddleware +import code.api.util.http4s.{ResourceDocMatcher, ResourceDocMiddleware} import code.api.util.http4s.IdempotencyMiddleware import code.api.util.newstyle.ViewNewStyle import code.api.util.{APIUtil, ApiRole, CallContext, CustomJsonFormats, NewStyle} @@ -109,7 +109,7 @@ object Http4s210 { // ─── root ───────────────────────────────────────────────────────────────── - val root: HttpRoutes[IO] = HttpRoutes.of[IO] { + val root: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` => EndpointHelpers.executeAndRespond(req) { _ => Future.successful(JSONFactory121.getApiInfoJSON(ApiVersion.v2_1_0, versionStatus)) @@ -134,7 +134,7 @@ object Http4s210 { // ─── sandboxDataImport ──────────────────────────────────────────────────── - val sandboxDataImport: HttpRoutes[IO] = HttpRoutes.of[IO] { + val sandboxDataImport: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "sandbox" / "data-import" => EndpointHelpers.withUserAndBodyCreated[SandboxDataImport, SuccessMessage](req) { (user, body, cc) => for { @@ -176,7 +176,7 @@ object Http4s210 { private val getTransactionRequestTypesIsPublic = APIUtil.getPropsAsBoolValue("apiOptions.getTransactionRequestTypesIsPublic", true) - val getTransactionRequestTypesSupportedByBank: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getTransactionRequestTypesSupportedByBank: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "transaction-request-types" => EndpointHelpers.withBank(req) { (bank, cc) => for { @@ -213,7 +213,7 @@ object Http4s210 { private val v210SupportedTransactionRequestTypes: Set[String] = Set("SANDBOX_TAN", "COUNTERPARTY", "SEPA", "FREE_FORM") - val createTransactionRequest: HttpRoutes[IO] = HttpRoutes.of[IO] { + val createTransactionRequest: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "banks" / _ / "accounts" / _ / viewIdStr / "transaction-request-types" / transactionRequestTypeStr / "transaction-requests" => // Check the type before requiring middleware-resolved entities: for an invalid type the // middleware finds no matching ResourceDoc and skips bankAccount resolution, so cc.bankAccount @@ -424,7 +424,7 @@ object Http4s210 { // ─── answerTransactionRequestChallenge ──────────────────────────────────── - val answerTransactionRequestChallenge: HttpRoutes[IO] = HttpRoutes.of[IO] { + val answerTransactionRequestChallenge: Http4sRoute = Http4sRoute { // Same guard as createTransactionRequest: v4 trans-req types (ACCOUNT, ACCOUNT_OTP, // REFUND, SIMPLE, AGENT_CASH_WITHDRAWAL, CARD, …) need v4's answer-challenge // logic (maker-checker, ChallengeJsonV400 shape, attribute attachment). Routing @@ -560,7 +560,7 @@ object Http4s210 { // ─── getTransactionRequests ─────────────────────────────────────────────── - val getTransactionRequests: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getTransactionRequests: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "accounts" / _ / _ / "transaction-requests" => EndpointHelpers.withView(req) { (user, account, view, cc) => for { @@ -613,7 +613,7 @@ object Http4s210 { // ─── getRoles ───────────────────────────────────────────────────────────── - val getRoles: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getRoles: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "roles" => EndpointHelpers.withUser(req) { (_, _) => Future.successful(JSONFactory210.createAvailableRolesJSON(ApiRole.availableRoles.sorted)) @@ -634,7 +634,7 @@ object Http4s210 { // ─── getEntitlementsByBankAndUser ───────────────────────────────────────── - val getEntitlementsByBankAndUser: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getEntitlementsByBankAndUser: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "users" / userId / "entitlements" => EndpointHelpers.withUserAndBank(req) { (user, bank, cc) => val allowedEntitlements = canGetEntitlementsForAnyUserAtOneBank :: @@ -675,7 +675,7 @@ object Http4s210 { // ─── getConsumer ────────────────────────────────────────────────────────── - val getConsumer: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getConsumer: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "management" / "consumers" / consumerId => EndpointHelpers.withUser(req) { (user, cc) => for { @@ -703,7 +703,7 @@ object Http4s210 { // ─── getConsumers ───────────────────────────────────────────────────────── - val getConsumers: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getConsumers: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "management" / "consumers" => EndpointHelpers.withUser(req) { (user, cc) => for { @@ -730,7 +730,7 @@ object Http4s210 { // ─── enableDisableConsumers ─────────────────────────────────────────────── - val enableDisableConsumers: HttpRoutes[IO] = HttpRoutes.of[IO] { + val enableDisableConsumers: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "management" / "consumers" / consumerId => EndpointHelpers.withUserAndBody[PutEnabledJSON, PutEnabledJSON](req) { (user, body, cc) => for { @@ -773,7 +773,7 @@ object Http4s210 { // ─── addCardForBank ─────────────────────────────────────────────────────── - val addCardForBank: HttpRoutes[IO] = HttpRoutes.of[IO] { + val addCardForBank: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "banks" / _ / "cards" => EndpointHelpers.withUserAndBankAndBodyCreated[PostPhysicalCardJSON, PhysicalCardJSON](req) { (user, bank, body, cc) => for { @@ -840,7 +840,7 @@ object Http4s210 { // ─── getUsers ───────────────────────────────────────────────────────────── - val getUsers: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getUsers: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "users" => EndpointHelpers.withUser(req) { (user, cc) => for { @@ -874,7 +874,7 @@ object Http4s210 { // ─── createTransactionType ──────────────────────────────────────────────── - val createTransactionType: HttpRoutes[IO] = HttpRoutes.of[IO] { + val createTransactionType: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "banks" / _ / "transaction-types" => EndpointHelpers.withUserAndBankAndBody[TransactionTypeJsonV200, TransactionTypeJsonV200](req) { (user, bank, body, cc) => for { @@ -920,7 +920,7 @@ object Http4s210 { private val getAtmsIsPublic = APIUtil.getPropsAsBoolValue("apiOptions.getAtmsIsPublic", true) - val getAtm: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getAtm: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "atms" / atmIdStr => EndpointHelpers.withBank(req) { (bank, cc) => for { @@ -956,7 +956,7 @@ object Http4s210 { private val getBranchesIsPublic = APIUtil.getPropsAsBoolValue("apiOptions.getBranchesIsPublic", true) - val getBranch: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getBranch: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "branches" / branchIdStr => EndpointHelpers.withBank(req) { (bank, cc) => for { @@ -994,7 +994,7 @@ object Http4s210 { private val getProductsIsPublic = APIUtil.getPropsAsBoolValue("apiOptions.getProductsIsPublic", true) - val getProduct: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getProduct: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "products" / productCodeStr => EndpointHelpers.withBank(req) { (bank, cc) => for { @@ -1029,7 +1029,7 @@ object Http4s210 { // ─── getProducts ────────────────────────────────────────────────────────── - val getProducts: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getProducts: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "products" => EndpointHelpers.withBank(req) { (bank, cc) => for { @@ -1065,7 +1065,7 @@ object Http4s210 { // ─── createCustomer ─────────────────────────────────────────────────────── - val createCustomer: HttpRoutes[IO] = HttpRoutes.of[IO] { + val createCustomer: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "banks" / _ / "customers" => EndpointHelpers.withUserAndBankAndBodyCreated[PostCustomerJsonV210, CustomerJsonV210](req) { (user, bank, body, cc) => for { @@ -1137,7 +1137,7 @@ object Http4s210 { // ─── getCustomersForUser ────────────────────────────────────────────────── - val getCustomersForUser: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getCustomersForUser: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "users" / "current" / "customers" => EndpointHelpers.withUser(req) { (user, cc) => Future { @@ -1161,7 +1161,7 @@ object Http4s210 { // ─── getCustomersForCurrentUserAtBank ────────────────────────────────────── - val getCustomersForCurrentUserAtBank: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getCustomersForCurrentUserAtBank: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "customers" => EndpointHelpers.withUserAndBank(req) { (user, bank, cc) => for { @@ -1196,7 +1196,7 @@ object Http4s210 { // ─── updateBranch ───────────────────────────────────────────────────────── - val updateBranch: HttpRoutes[IO] = HttpRoutes.of[IO] { + val updateBranch: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "banks" / _ / "branches" / branchIdStr => EndpointHelpers.withUserAndBankAndBodyCreated[BranchJsonPutV210, JSONFactory1_4_0.BranchJson](req) { (user, bank, body, cc) => for { @@ -1229,7 +1229,7 @@ object Http4s210 { // ─── createBranch ───────────────────────────────────────────────────────── - val createBranch: HttpRoutes[IO] = HttpRoutes.of[IO] { + val createBranch: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "banks" / _ / "branches" => EndpointHelpers.withUserAndBankAndBodyCreated[BranchJsonPostV210, JSONFactory1_4_0.BranchJson](req) { (user, bank, body, cc) => for { @@ -1265,7 +1265,7 @@ object Http4s210 { // ─── updateConsumerRedirectUrl ──────────────────────────────────────────── - val updateConsumerRedirectUrl: HttpRoutes[IO] = HttpRoutes.of[IO] { + val updateConsumerRedirectUrl: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "management" / "consumers" / consumerId / "consumer" / "redirect_url" => EndpointHelpers.withUserAndBody[ConsumerRedirectUrlJSON, ConsumerJsonV210](req) { (user, body, cc) => for { @@ -1312,7 +1312,7 @@ object Http4s210 { // ─── getMetrics ─────────────────────────────────────────────────────────── - val getMetrics: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getMetrics: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "management" / "metrics" => EndpointHelpers.withUser(req) { (user, cc) => for { @@ -1395,35 +1395,41 @@ object Http4s210 { // ─── allRoutes ──────────────────────────────────────────────────────────── - private val allOwnRoutes: HttpRoutes[IO] = Kleisli[HttpF, Request[IO], Response[IO]] { req => - root.run(req) - .orElse(sandboxDataImport.run(req)) - .orElse(getTransactionRequestTypesSupportedByBank.run(req)) - .orElse(createTransactionRequest.run(req)) - .orElse(answerTransactionRequestChallenge.run(req)) - .orElse(getTransactionRequests.run(req)) - .orElse(getRoles.run(req)) - .orElse(getEntitlementsByBankAndUser.run(req)) - .orElse(getConsumers.run(req)) - .orElse(getConsumer.run(req)) - .orElse(enableDisableConsumers.run(req)) - .orElse(addCardForBank.run(req)) - .orElse(getUsers.run(req)) - .orElse(createTransactionType.run(req)) - .orElse(getAtm.run(req)) - .orElse(getBranch.run(req)) - .orElse(getProduct.run(req)) - .orElse(getProducts.run(req)) - .orElse(createCustomer.run(req)) - .orElse(getCustomersForUser.run(req)) - .orElse(getCustomersForCurrentUserAtBank.run(req)) - .orElse(updateBranch.run(req)) - .orElse(createBranch.run(req)) - .orElse(updateConsumerRedirectUrl.run(req)) - .orElse(getMetrics.run(req)) - } - - val allRoutesWithMiddleware: HttpRoutes[IO] = ResourceDocMiddleware.apply(resourceDocs)(IdempotencyMiddleware(allOwnRoutes)) + // The routes in the order they are tried. ResourceDocMiddleware selects the doc of the first + // route that serves a request, so it is given the docs in this same order. + lazy val routesInOrder: List[Http4sHandler] = List( + root, + sandboxDataImport, + getTransactionRequestTypesSupportedByBank, + createTransactionRequest, + answerTransactionRequestChallenge, + getTransactionRequests, + getRoles, + getEntitlementsByBankAndUser, + getConsumers, + getConsumer, + enableDisableConsumers, + addCardForBank, + getUsers, + createTransactionType, + getAtm, + getBranch, + getProduct, + getProducts, + createCustomer, + getCustomersForUser, + getCustomersForCurrentUserAtBank, + updateBranch, + createBranch, + updateConsumerRedirectUrl, + getMetrics + ) + + lazy val orderedResourceDocs: ArrayBuffer[ResourceDoc] = ResourceDocMatcher.orderByRoutes(resourceDocs, routesInOrder) + + private lazy val allOwnRoutes: HttpRoutes[IO] = Http4sRoute.chain(routesInOrder) + + lazy val allRoutesWithMiddleware: HttpRoutes[IO] = ResourceDocMiddleware.apply(orderedResourceDocs, routes => IdempotencyMiddleware(routes))(IdempotencyMiddleware(allOwnRoutes)) // ─── path-rewriting bridge: /obp/v2.1.0/… → /obp/v2.0.0/… ────────────── // Delegates to Http4s200 so all inherited v2.0.0/v1.4.0/v1.3.0/v1.2.1 endpoints diff --git a/obp-api/src/main/scala/code/api/v2_2_0/Http4s220.scala b/obp-api/src/main/scala/code/api/v2_2_0/Http4s220.scala index 71dc927940..9eb1c2385a 100644 --- a/obp-api/src/main/scala/code/api/v2_2_0/Http4s220.scala +++ b/obp-api/src/main/scala/code/api/v2_2_0/Http4s220.scala @@ -32,14 +32,14 @@ import cats.data.{Kleisli, OptionT} import cats.effect._ import code.api.Constant._ import code.api.ResourceDocs1_4_0.SwaggerDefinitionsJSON._ -import code.api.util.APIUtil.{EmptyBody, ResourceDoc, _} +import code.api.util.APIUtil.{Http4sHandler, Http4sRoute, EmptyBody, ResourceDoc, _} import code.api.util.ApiRole._ import code.api.util.ApiTag._ import code.api.util.ErrorMessages._ import code.api.util.Glossary import code.api.util.http4s.Http4sRequestAttributes.{EndpointHelpers, RequestOps} import java.util.Date -import code.api.util.http4s.ResourceDocMiddleware +import code.api.util.http4s.{ResourceDocMatcher, ResourceDocMiddleware} import code.api.util.http4s.IdempotencyMiddleware import code.api.util.newstyle.ViewNewStyle import code.api.util.{APIUtil, CallContext, CustomJsonFormats, NewStyle} @@ -83,7 +83,7 @@ object Http4s220 { // ─── root ───────────────────────────────────────────────────────────────── - val root: HttpRoutes[IO] = HttpRoutes.of[IO] { + val root: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` => EndpointHelpers.executeAndRespond(req) { _ => Future.successful(JSONFactory121.getApiInfoJSON(ApiVersion.v2_2_0, versionStatus)) @@ -108,7 +108,7 @@ object Http4s220 { // ─── getViewsForBankAccount ─────────────────────────────────────────────── - val getViewsForBankAccount: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getViewsForBankAccount: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "accounts" / _ / "views" => EndpointHelpers.withBankAccount(req) { (user, account, cc) => for { @@ -160,7 +160,7 @@ object Http4s220 { // ─── createViewForBankAccount ───────────────────────────────────────────── - val createViewForBankAccount: HttpRoutes[IO] = HttpRoutes.of[IO] { + val createViewForBankAccount: Http4sRoute = Http4sRoute { // VIEW_ACCOUNT_ID (non-standard name) bypasses middleware account-existence check so the // handler can return 400 (not 404) for a missing account, matching Lift behaviour. case req @ POST -> `prefixPath` / "banks" / _ / "accounts" / accountIdStr / "views" => @@ -227,7 +227,7 @@ object Http4s220 { // ─── updateViewForBankAccount ───────────────────────────────────────────── - val updateViewForBankAccount: HttpRoutes[IO] = HttpRoutes.of[IO] { + val updateViewForBankAccount: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "banks" / _ / "accounts" / _ / "views" / viewIdStr => EndpointHelpers.withBankAccount(req) { (user, account, cc) => updateViewImpl(user, account, ViewId(viewIdStr), cc.httpBody.getOrElse(""), cc) @@ -281,7 +281,7 @@ object Http4s220 { private val getCurrentFxRateIsPublic = APIUtil.getPropsAsBoolValue("apiOptions.getCurrentFxRateIsPublic", false) - val getCurrentFxRate: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getCurrentFxRate: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "fx" / fromCurrencyCode / toCurrencyCode => EndpointHelpers.withBank(req) { (bank, cc) => val fromUpper = fromCurrencyCode.toUpperCase @@ -347,7 +347,7 @@ object Http4s220 { // ─── getExplicitCounterpartiesForAccount ────────────────────────────────── - val getExplicitCounterpartiesForAccount: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getExplicitCounterpartiesForAccount: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "accounts" / _ / _ / "counterparties" => EndpointHelpers.withView(req) { (user, account, view, cc) => for { @@ -386,7 +386,7 @@ object Http4s220 { // ─── getExplicitCounterpartyById ────────────────────────────────────────── - val getExplicitCounterpartyById: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getExplicitCounterpartyById: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "banks" / _ / "accounts" / _ / _ / "counterparties" / _ => EndpointHelpers.withCounterparty(req) { (_, account, view, counterparty, cc) => for { @@ -416,7 +416,7 @@ object Http4s220 { // ─── getMessageDocs ─────────────────────────────────────────────────────── - val getMessageDocs: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getMessageDocs: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "message-docs" / connector => EndpointHelpers.executeAndRespond(req) { cc => Future { @@ -450,7 +450,7 @@ object Http4s220 { // ─── createBank ─────────────────────────────────────────────────────────── - val createBank: HttpRoutes[IO] = HttpRoutes.of[IO] { + val createBank: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "banks" => EndpointHelpers.withUserAndBodyCreated[BankJSONV220, BankJSONV220](req) { (user, bank, cc) => val checkShort = APIUtil.checkShortString(bank.id) @@ -508,7 +508,7 @@ object Http4s220 { // ─── createBranch ───────────────────────────────────────────────────────── - val createBranch: HttpRoutes[IO] = HttpRoutes.of[IO] { + val createBranch: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "banks" / _ / "branches" => EndpointHelpers.withUserAndBankAndBodyCreated[BranchJsonV220, BranchJsonV220](req) { (user, bank, body, cc) => for { @@ -542,7 +542,7 @@ object Http4s220 { // ─── createAtm ──────────────────────────────────────────────────────────── - val createAtm: HttpRoutes[IO] = HttpRoutes.of[IO] { + val createAtm: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "banks" / _ / "atms" => EndpointHelpers.withUserAndBankAndBodyCreated[AtmJsonV220, AtmJsonV220](req) { (user, bank, body, cc) => for { @@ -575,7 +575,7 @@ object Http4s220 { // ─── createProduct ──────────────────────────────────────────────────────── - val createProduct: HttpRoutes[IO] = HttpRoutes.of[IO] { + val createProduct: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "banks" / _ / "products" => EndpointHelpers.withUserAndBankAndBodyCreated[ProductJsonV220, ProductJsonV220](req) { (user, bank, body, cc) => for { @@ -619,7 +619,7 @@ object Http4s220 { val createFxEntitlementsRequiredForSpecificBank = canCreateFxRate :: Nil val createFxEntitlementsRequiredForAnyBank = canCreateFxRateAtAnyBank :: Nil - val createFx: HttpRoutes[IO] = HttpRoutes.of[IO] { + val createFx: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "banks" / _ / "fx" => EndpointHelpers.withUserAndBankAndBodyCreated[FXRateJsonV220, FXRateJsonV220](req) { (user, bank, body, cc) => for { @@ -674,7 +674,7 @@ object Http4s220 { // ─── createAccount ──────────────────────────────────────────────────────── - val createAccount: HttpRoutes[IO] = HttpRoutes.of[IO] { + val createAccount: Http4sRoute = Http4sRoute { case req @ PUT -> `prefixPath` / "banks" / _ / "accounts" / accountIdStr => EndpointHelpers.withUserAndBankAndBody[CreateAccountJSONV220, CreateAccountJSONV220](req) { (user, bank, body, cc) => for { @@ -734,7 +734,7 @@ object Http4s220 { // ─── config ─────────────────────────────────────────────────────────────── - val config: HttpRoutes[IO] = HttpRoutes.of[IO] { + val config: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "config" => EndpointHelpers.withUser(req) { (user, cc) => for { @@ -761,7 +761,7 @@ object Http4s220 { // ─── getConnectorMetrics ────────────────────────────────────────────────── - val getConnectorMetrics: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getConnectorMetrics: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "management" / "connector" / "metrics" => EndpointHelpers.withUser(req) { (user, cc) => for { @@ -814,7 +814,7 @@ object Http4s220 { // ─── createConsumer ─────────────────────────────────────────────────────── - val createConsumer: HttpRoutes[IO] = HttpRoutes.of[IO] { + val createConsumer: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "management" / "consumers" => EndpointHelpers.withUserAndBodyCreated[ConsumerPostJSON, ConsumerJson](req) { (user, body, cc) => for { @@ -880,7 +880,7 @@ object Http4s220 { // ─── createCounterparty ─────────────────────────────────────────────────── - val createCounterparty: HttpRoutes[IO] = HttpRoutes.of[IO] { + val createCounterparty: Http4sRoute = Http4sRoute { case req @ POST -> `prefixPath` / "banks" / _ / "accounts" / _ / _ / "counterparties" => EndpointHelpers.withViewCreated(req) { (user, account, view, cc) => createCounterpartyImpl(user, account, view, cc.httpBody.getOrElse(""), cc) @@ -1044,28 +1044,34 @@ object Http4s220 { // ─── allRoutes ──────────────────────────────────────────────────────────── - private val allOwnRoutes: HttpRoutes[IO] = Kleisli[HttpF, Request[IO], Response[IO]] { req => - root.run(req) - .orElse(getViewsForBankAccount.run(req)) - .orElse(createViewForBankAccount.run(req)) - .orElse(updateViewForBankAccount.run(req)) - .orElse(getCurrentFxRate.run(req)) - .orElse(getExplicitCounterpartiesForAccount.run(req)) - .orElse(getExplicitCounterpartyById.run(req)) - .orElse(getMessageDocs.run(req)) - .orElse(createBank.run(req)) - .orElse(createBranch.run(req)) - .orElse(createAtm.run(req)) - .orElse(createProduct.run(req)) - .orElse(createFx.run(req)) - .orElse(createAccount.run(req)) - .orElse(config.run(req)) - .orElse(getConnectorMetrics.run(req)) - .orElse(createConsumer.run(req)) - .orElse(createCounterparty.run(req)) - } - - val allRoutesWithMiddleware: HttpRoutes[IO] = ResourceDocMiddleware.apply(resourceDocs)(IdempotencyMiddleware(allOwnRoutes)) + // The routes in the order they are tried. ResourceDocMiddleware selects the doc of the first + // route that serves a request, so it is given the docs in this same order. + lazy val routesInOrder: List[Http4sHandler] = List( + root, + getViewsForBankAccount, + createViewForBankAccount, + updateViewForBankAccount, + getCurrentFxRate, + getExplicitCounterpartiesForAccount, + getExplicitCounterpartyById, + getMessageDocs, + createBank, + createBranch, + createAtm, + createProduct, + createFx, + createAccount, + config, + getConnectorMetrics, + createConsumer, + createCounterparty + ) + + lazy val orderedResourceDocs: ArrayBuffer[ResourceDoc] = ResourceDocMatcher.orderByRoutes(resourceDocs, routesInOrder) + + private lazy val allOwnRoutes: HttpRoutes[IO] = Http4sRoute.chain(routesInOrder) + + lazy val allRoutesWithMiddleware: HttpRoutes[IO] = ResourceDocMiddleware.apply(orderedResourceDocs, routes => IdempotencyMiddleware(routes))(IdempotencyMiddleware(allOwnRoutes)) // ─── path-rewriting bridge: /obp/v2.2.0/… → /obp/v2.1.0/… ────────────── From 0c1055a578ddd51509af0315054d0b1a311e7f9f Mon Sep 17 00:00:00 2001 From: Hongwei Date: Wed, 7 Oct 2026 17:29:58 +0200 Subject: [PATCH 17/29] refactor: bind every Berlin Group v1.3 and v2 ResourceDoc to its route Each group lists its routes in order and the aggregator concatenates them, so the middleware selects the doc of the route that serves a request. A signing basket's status and authorisations no longer fall to getPaymentInformation, whose template is made only of placeholders. The v2 routes are declared as lazy vals because their docs are registered before them. --- .../api/berlin/group/v1_3/Http4sBGv13.scala | 21 ++--- .../berlin/group/v1_3/Http4sBGv13AIS.scala | 76 ++++++++++--------- .../berlin/group/v1_3/Http4sBGv13PIIS.scala | 10 ++- .../berlin/group/v1_3/Http4sBGv13PIS.scala | 64 ++++++++-------- .../v1_3/Http4sBGv13SigningBaskets.scala | 40 +++++----- .../code/api/berlin/group/v2/Http4sBGv2.scala | 19 +++-- .../api/berlin/group/v2/Http4sBGv2AIS.scala | 44 ++++++----- .../api/berlin/group/v2/Http4sBGv2PIIS.scala | 12 +-- .../api/berlin/group/v2/Http4sBGv2PIS.scala | 64 +++++++--------- 9 files changed, 181 insertions(+), 169 deletions(-) diff --git a/obp-api/src/main/scala/code/api/berlin/group/v1_3/Http4sBGv13.scala b/obp-api/src/main/scala/code/api/berlin/group/v1_3/Http4sBGv13.scala index 75a80949cf..275f647fc7 100644 --- a/obp-api/src/main/scala/code/api/berlin/group/v1_3/Http4sBGv13.scala +++ b/obp-api/src/main/scala/code/api/berlin/group/v1_3/Http4sBGv13.scala @@ -30,8 +30,8 @@ package code.api.berlin.group.v1_3 import cats.data.{Kleisli, OptionT} import cats.effect._ import code.api.berlin.group.ConstantsBG -import code.api.util.APIUtil.ResourceDoc -import code.api.util.http4s.ResourceDocMiddleware +import code.api.util.APIUtil.{Http4sHandler, Http4sRoute, ResourceDoc} +import code.api.util.http4s.{ResourceDocMatcher, ResourceDocMiddleware} import code.api.util.http4s.IdempotencyMiddleware import code.util.Helper.MdcLoggable import org.http4s._ @@ -56,12 +56,15 @@ object Http4sBGv13 extends MdcLoggable { Http4sBGv13PIIS.resourceDocs ++ Http4sBGv13SigningBaskets.resourceDocs - val allRoutes: HttpRoutes[IO] = Kleisli[HttpF, Request[IO], Response[IO]] { req => - Http4sBGv13AIS.routes(req) - .orElse(Http4sBGv13PIS.routes(req)) - .orElse(Http4sBGv13PIIS.routes(req)) - .orElse(Http4sBGv13SigningBaskets.routes(req)) - } + lazy val routesInOrder: List[Http4sHandler] = + Http4sBGv13AIS.routesInOrder ++ + Http4sBGv13PIS.routesInOrder ++ + Http4sBGv13PIIS.routesInOrder ++ + Http4sBGv13SigningBaskets.routesInOrder - val wrappedRoutes: HttpRoutes[IO] = ResourceDocMiddleware.apply(resourceDocs)(IdempotencyMiddleware(allRoutes)) + lazy val orderedResourceDocs: ArrayBuffer[ResourceDoc] = ResourceDocMatcher.orderByRoutes(resourceDocs, routesInOrder) + + lazy val allRoutes: HttpRoutes[IO] = Http4sRoute.chain(routesInOrder) + + lazy val wrappedRoutes: HttpRoutes[IO] = ResourceDocMiddleware.apply(orderedResourceDocs, routes => IdempotencyMiddleware(routes))(IdempotencyMiddleware(allRoutes)) } diff --git a/obp-api/src/main/scala/code/api/berlin/group/v1_3/Http4sBGv13AIS.scala b/obp-api/src/main/scala/code/api/berlin/group/v1_3/Http4sBGv13AIS.scala index a961d1c0d9..3a2c2858c1 100644 --- a/obp-api/src/main/scala/code/api/berlin/group/v1_3/Http4sBGv13AIS.scala +++ b/obp-api/src/main/scala/code/api/berlin/group/v1_3/Http4sBGv13AIS.scala @@ -38,7 +38,7 @@ import code.api.berlin.group.v1_3.model._ import code.api.berlin.group.v1_3.{BgSpecValidation, JSONFactory_BERLIN_GROUP_1_3, JvalueCaseClass} import code.api.RequestHeader import code.api.util.APIUtil -import code.api.util.APIUtil.{EmptyBody, ResourceDoc, UserOrApplication, connectorEmptyResponse, createQueriesByHttpParams, fullBoxOrException, getHttpRequestUrlParam, getSuggestedDefaultScaMethod, mockedDataText, passesPsd2Aisp, unboxFull, unboxFullOrFail} +import code.api.util.APIUtil.{Http4sHandler, Http4sRoute, EmptyBody, ResourceDoc, UserOrApplication, connectorEmptyResponse, createQueriesByHttpParams, fullBoxOrException, getHttpRequestUrlParam, getSuggestedDefaultScaMethod, mockedDataText, passesPsd2Aisp, unboxFull, unboxFullOrFail} import code.api.util.CallContext import code.api.util.ApiTag._ import code.api.util.CustomJsonFormats @@ -97,7 +97,7 @@ object Http4sBGv13AIS extends MdcLoggable { } // ── POST /consents ────────────────────────────────────────────────────── - lazy val createConsent: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val createConsent: Http4sRoute = Http4sRoute { case req @ POST -> `bgV13Prefix` / "consents" => EndpointHelpers.executeFutureCreated(req) { val cc = req.callContext @@ -181,7 +181,7 @@ object Http4sBGv13AIS extends MdcLoggable { } // ── DELETE /consents/CONSENTID ────────────────────────────────────────── - lazy val deleteConsent: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val deleteConsent: Http4sRoute = Http4sRoute { case req @ DELETE -> `bgV13Prefix` / "consents" / consentId => EndpointHelpers.executeDelete(req) { cc => val callContext = Some(cc) @@ -199,7 +199,7 @@ object Http4sBGv13AIS extends MdcLoggable { } // ── GET /accounts ─────────────────────────────────────────────────────── - lazy val getAccountList: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getAccountList: Http4sRoute = Http4sRoute { case req @ GET -> `bgV13Prefix` / "accounts" => EndpointHelpers.executeAndRespond(req) { cc => val callContext = Some(cc) @@ -238,7 +238,7 @@ object Http4sBGv13AIS extends MdcLoggable { } // ── GET /accounts/ACCOUNT_ID/balances ─────────────────────────────────── - lazy val getBalances: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getBalances: Http4sRoute = Http4sRoute { case req @ GET -> `bgV13Prefix` / "accounts" / accountId / "balances" => EndpointHelpers.executeAndRespond(req) { cc => val callContext = Some(cc) @@ -258,7 +258,7 @@ object Http4sBGv13AIS extends MdcLoggable { } // ── GET /card-accounts ────────────────────────────────────────────────── - lazy val getCardAccounts: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getCardAccounts: Http4sRoute = Http4sRoute { case req @ GET -> `bgV13Prefix` / "card-accounts" => EndpointHelpers.executeAndRespond(req) { cc => val callContext = Some(cc) @@ -288,7 +288,7 @@ object Http4sBGv13AIS extends MdcLoggable { } // ── GET /card-accounts/ACCOUNT_ID/balances ────────────────────────────── - lazy val getCardAccountBalances: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getCardAccountBalances: Http4sRoute = Http4sRoute { case req @ GET -> `bgV13Prefix` / "card-accounts" / accountId / "balances" => EndpointHelpers.executeAndRespond(req) { cc => val callContext = Some(cc) @@ -308,7 +308,7 @@ object Http4sBGv13AIS extends MdcLoggable { } // ── GET /card-accounts/ACCOUNT_ID/transactions ────────────────────────── - lazy val getCardAccountTransactionList: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getCardAccountTransactionList: Http4sRoute = Http4sRoute { case req @ GET -> `bgV13Prefix` / "card-accounts" / accountId / "transactions" => EndpointHelpers.executeAndRespond(req) { cc => val callContext = Some(cc) @@ -333,7 +333,7 @@ object Http4sBGv13AIS extends MdcLoggable { } // ── GET /consents/CONSENTID/authorisations ────────────────────────────── - lazy val getConsentAuthorisation: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getConsentAuthorisation: Http4sRoute = Http4sRoute { case req @ GET -> `bgV13Prefix` / "consents" / consentId / "authorisations" => EndpointHelpers.executeAndRespond(req) { cc => val callContext = Some(cc) @@ -356,7 +356,7 @@ object Http4sBGv13AIS extends MdcLoggable { } // ── GET /consents/CONSENTID ───────────────────────────────────────────── - lazy val getConsentInformation: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getConsentInformation: Http4sRoute = Http4sRoute { case req @ GET -> `bgV13Prefix` / "consents" / consentId => EndpointHelpers.executeAndRespond(req) { cc => val callContext = Some(cc) @@ -373,7 +373,7 @@ object Http4sBGv13AIS extends MdcLoggable { } // ── GET /consents/CONSENTID/authorisations/AUTHORISATIONID ───────────── - lazy val getConsentScaStatus: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getConsentScaStatus: Http4sRoute = Http4sRoute { case req @ GET -> `bgV13Prefix` / "consents" / consentId / "authorisations" / authorisationId => EndpointHelpers.executeAndRespond(req) { cc => val callContext = Some(cc) @@ -396,7 +396,7 @@ object Http4sBGv13AIS extends MdcLoggable { } // ── GET /consents/CONSENTID/status ────────────────────────────────────── - lazy val getConsentStatus: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getConsentStatus: Http4sRoute = Http4sRoute { case req @ GET -> `bgV13Prefix` / "consents" / consentId / "status" => EndpointHelpers.executeAndRespond(req) { cc => val callContext = Some(cc) @@ -416,7 +416,7 @@ object Http4sBGv13AIS extends MdcLoggable { } // ── GET /accounts/ACCOUNT_ID/transactions/TRANSACTIONID ───────────────── - lazy val getTransactionDetails: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getTransactionDetails: Http4sRoute = Http4sRoute { case req @ GET -> `bgV13Prefix` / "accounts" / accountId / "transactions" / transactionId => EndpointHelpers.executeAndRespond(req) { cc => val callContext = Some(cc) @@ -436,7 +436,7 @@ object Http4sBGv13AIS extends MdcLoggable { } // ── GET /accounts/ACCOUNT_ID/transactions ─────────────────────────────── - lazy val getTransactionList: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getTransactionList: Http4sRoute = Http4sRoute { case req @ GET -> `bgV13Prefix` / "accounts" / accountId / "transactions" => EndpointHelpers.executeAndRespond(req) { cc => val callContext = Some(cc) @@ -468,7 +468,7 @@ object Http4sBGv13AIS extends MdcLoggable { } // ── GET /accounts/ACCOUNT_ID ──────────────────────────────────────────── - lazy val getAccountDetails: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getAccountDetails: Http4sRoute = Http4sRoute { case req @ GET -> `bgV13Prefix` / "accounts" / accountId => EndpointHelpers.executeAndRespond(req) { cc => val callContext = Some(cc) @@ -501,7 +501,7 @@ object Http4sBGv13AIS extends MdcLoggable { } // ── GET /card-accounts/ACCOUNT_ID ─────────────────────────────────────── - lazy val readCardAccount: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val readCardAccount: Http4sRoute = Http4sRoute { case req @ GET -> `bgV13Prefix` / "card-accounts" / accountId => EndpointHelpers.executeAndRespond(req) { cc => val callContext = Some(cc) @@ -552,7 +552,7 @@ object Http4sBGv13AIS extends MdcLoggable { } // ── POST /consents/CONSENTID/authorisations (3 body-guard variants) ───── - lazy val startConsentAuthorisationAll: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val startConsentAuthorisationAll: Http4sRoute = Http4sRoute { case req @ POST -> `bgV13Prefix` / "consents" / consentId / "authorisations" => EndpointHelpers.executeFutureCreated(req) { val cc = req.callContext @@ -638,7 +638,7 @@ object Http4sBGv13AIS extends MdcLoggable { } // ── PUT /consents/CONSENTID/authorisations/AUTHORISATIONID (4 variants) ─ - lazy val updateConsentsPsuDataAll: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val updateConsentsPsuDataAll: Http4sRoute = Http4sRoute { case req @ PUT -> `bgV13Prefix` / "consents" / consentId / "authorisations" / authorisationId => EndpointHelpers.executeAndRespond(req) { cc => val callContext = Some(cc) @@ -1499,23 +1499,25 @@ respectively the OAuth2 access token. initConsentResourceDocs() initAccountResourceDocs() - val routes: HttpRoutes[IO] = Kleisli[HttpF, Request[IO], Response[IO]] { req => - createConsent(req) - .orElse(deleteConsent(req)) - .orElse(getAccountList(req)) - .orElse(getBalances(req)) - .orElse(getCardAccounts(req)) - .orElse(getCardAccountBalances(req)) - .orElse(getCardAccountTransactionList(req)) - .orElse(getConsentAuthorisation(req)) - .orElse(getConsentInformation(req)) - .orElse(getConsentScaStatus(req)) - .orElse(getConsentStatus(req)) - .orElse(getTransactionDetails(req)) - .orElse(getTransactionList(req)) - .orElse(getAccountDetails(req)) - .orElse(readCardAccount(req)) - .orElse(startConsentAuthorisationAll(req)) - .orElse(updateConsentsPsuDataAll(req)) - } + lazy val routesInOrder: List[Http4sHandler] = List( + createConsent, + deleteConsent, + getAccountList, + getBalances, + getCardAccounts, + getCardAccountBalances, + getCardAccountTransactionList, + getConsentAuthorisation, + getConsentInformation, + getConsentScaStatus, + getConsentStatus, + getTransactionDetails, + getTransactionList, + getAccountDetails, + readCardAccount, + startConsentAuthorisationAll, + updateConsentsPsuDataAll + ) + + lazy val routes: HttpRoutes[IO] = Http4sRoute.chain(routesInOrder) } diff --git a/obp-api/src/main/scala/code/api/berlin/group/v1_3/Http4sBGv13PIIS.scala b/obp-api/src/main/scala/code/api/berlin/group/v1_3/Http4sBGv13PIIS.scala index c2ba48736a..d92447d610 100644 --- a/obp-api/src/main/scala/code/api/berlin/group/v1_3/Http4sBGv13PIIS.scala +++ b/obp-api/src/main/scala/code/api/berlin/group/v1_3/Http4sBGv13PIIS.scala @@ -70,7 +70,7 @@ object Http4sBGv13PIIS extends MdcLoggable { // ── POST /funds-confirmations ───────────────────────────────────── // Lift source: code.api.builder.ConfirmationOfFundsServicePIISApi.checkAvailabilityOfFunds // Auth: authenticatedAccess → authMode UserOnly (default); user resolved by ResourceDocMiddleware. - val checkAvailabilityOfFunds: HttpRoutes[IO] = HttpRoutes.of[IO] { + val checkAvailabilityOfFunds: Http4sRoute = Http4sRoute { case req @ POST -> `bgV13Prefix` / "funds-confirmations" => EndpointHelpers.executeAndRespond(req) { cc => val callContext = Some(cc) @@ -144,7 +144,9 @@ in the header. This field is contained but commented out in this specification. http4sPartialFunction = Some(checkAvailabilityOfFunds) ) - val routes: HttpRoutes[IO] = Kleisli[HttpF, Request[IO], Response[IO]] { req => - checkAvailabilityOfFunds(req) - } + lazy val routesInOrder: List[Http4sHandler] = List( + checkAvailabilityOfFunds + ) + + lazy val routes: HttpRoutes[IO] = Http4sRoute.chain(routesInOrder) } diff --git a/obp-api/src/main/scala/code/api/berlin/group/v1_3/Http4sBGv13PIS.scala b/obp-api/src/main/scala/code/api/berlin/group/v1_3/Http4sBGv13PIS.scala index 0f9e0e5a83..91cf03015c 100644 --- a/obp-api/src/main/scala/code/api/berlin/group/v1_3/Http4sBGv13PIS.scala +++ b/obp-api/src/main/scala/code/api/berlin/group/v1_3/Http4sBGv13PIS.scala @@ -34,7 +34,7 @@ import code.api.berlin.group.ConstantsBG import code.api.berlin.group.v1_3.JSONFactory_BERLIN_GROUP_1_3._ import code.api.berlin.group.v1_3.model.TransactionStatus.mapTransactionStatus import code.api.berlin.group.v1_3.model._ -import code.api.util.APIUtil.{EmptyBody, ResourceDoc, UserOrApplication, getScaMethodAtInstance, getServerUrl, isValidCurrencyISOCode, mockedDataText, passesPsd2Pisp} +import code.api.util.APIUtil.{Http4sHandler, Http4sRoute, EmptyBody, ResourceDoc, UserOrApplication, getScaMethodAtInstance, getServerUrl, isValidCurrencyISOCode, mockedDataText, passesPsd2Pisp} import code.api.util.ApiTag._ import code.api.util.ErrorMessages._ import code.api.util.CustomJsonFormats @@ -200,7 +200,7 @@ object Http4sBGv13PIS extends MdcLoggable { // ── DELETE /{paymentService}/{paymentProduct}/{paymentId} ────────────────────────────────────────────────────── // Variable response: 202 (SCA required) with CancelPaymentResponseJson body, or 204 (direct cancel) with no body. // Custom IO handler to produce truly-empty 204 (NoContent) — executeFutureWithStatus would always add a body. - lazy val cancelPayment: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val cancelPayment: Http4sRoute = Http4sRoute { case req @ DELETE -> `bgV13Prefix` / paymentService / paymentProduct / paymentId => implicit val cc: CallContext = req.callContext val callContext = Some(cc) @@ -276,7 +276,7 @@ object Http4sBGv13PIS extends MdcLoggable { } // ── GET /{paymentService}/{paymentProduct}/{paymentId}/cancellation-authorisations/{cancellationId} ─── - lazy val getPaymentCancellationScaStatus: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getPaymentCancellationScaStatus: Http4sRoute = Http4sRoute { case req @ GET -> `bgV13Prefix` / paymentService / paymentProduct / paymentId / "cancellation-authorisations" / cancellationId => EndpointHelpers.executeAndRespond(req) { cc => val callContext = Some(cc) @@ -297,7 +297,7 @@ object Http4sBGv13PIS extends MdcLoggable { } // ── GET /{paymentService}/{paymentProduct}/{paymentId} (with checkPaymentServiceType guard in Lift) ── - lazy val getPaymentInformation: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getPaymentInformation: Http4sRoute = Http4sRoute { case req @ GET -> `bgV13Prefix` / paymentService / paymentProduct / paymentId if checkPaymentServiceType(paymentService) => EndpointHelpers.executeAndRespond(req) { cc => val callContext = Some(cc) @@ -320,7 +320,7 @@ object Http4sBGv13PIS extends MdcLoggable { } // ── GET /{paymentService}/{paymentProduct}/{paymentId}/authorisations ───────────────────────────── - lazy val getPaymentInitiationAuthorisation: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getPaymentInitiationAuthorisation: Http4sRoute = Http4sRoute { case req @ GET -> `bgV13Prefix` / paymentService / paymentProduct / paymentId / "authorisations" => EndpointHelpers.executeAndRespond(req) { cc => val callContext = Some(cc) @@ -341,7 +341,7 @@ object Http4sBGv13PIS extends MdcLoggable { } // ── GET /{paymentService}/{paymentProduct}/{paymentId}/cancellation-authorisations ────────────────── - lazy val getPaymentInitiationCancellationAuthorisationInformation: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getPaymentInitiationCancellationAuthorisationInformation: Http4sRoute = Http4sRoute { case req @ GET -> `bgV13Prefix` / paymentService / paymentProduct / paymentId / "cancellation-authorisations" => EndpointHelpers.executeAndRespond(req) { cc => val callContext = Some(cc) @@ -362,7 +362,7 @@ object Http4sBGv13PIS extends MdcLoggable { } // ── GET /{paymentService}/{paymentProduct}/{paymentId}/authorisations/{authorisationId} ──────────── - lazy val getPaymentInitiationScaStatus: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getPaymentInitiationScaStatus: Http4sRoute = Http4sRoute { case req @ GET -> `bgV13Prefix` / paymentService / paymentProduct / paymentId / "authorisations" / authorisationId => EndpointHelpers.executeAndRespond(req) { cc => val callContext = Some(cc) @@ -383,7 +383,7 @@ object Http4sBGv13PIS extends MdcLoggable { } // ── GET /{paymentService}/{paymentProduct}/{paymentId}/status ───────────────────────────────────── - lazy val getPaymentInitiationStatus: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getPaymentInitiationStatus: Http4sRoute = Http4sRoute { case req @ GET -> `bgV13Prefix` / paymentService / paymentProduct / paymentId / "status" => EndpointHelpers.executeAndRespond(req) { cc => val callContext = Some(cc) @@ -428,7 +428,7 @@ object Http4sBGv13PIS extends MdcLoggable { // ── POST /payments/{paymentProduct} ────────────────────────────────────────────────────────────── // Auth: applicationAccess in Lift → authMode = UserOrApplication in ResourceDoc - lazy val initiatePayments: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val initiatePayments: Http4sRoute = Http4sRoute { case req @ POST -> `bgV13Prefix` / "payments" / paymentProduct => EndpointHelpers.executeFutureCreated(req) { initiatePaymentImpl("payments", paymentProduct, Some(req.callContext)) @@ -436,7 +436,7 @@ object Http4sBGv13PIS extends MdcLoggable { } // ── POST /periodic-payments/{paymentProduct} ────────────────────────────────────────────────────── - lazy val initiatePeriodicPayments: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val initiatePeriodicPayments: Http4sRoute = Http4sRoute { case req @ POST -> `bgV13Prefix` / "periodic-payments" / paymentProduct => EndpointHelpers.executeFutureCreated(req) { initiatePaymentImpl("periodic-payments", paymentProduct, Some(req.callContext)) @@ -444,7 +444,7 @@ object Http4sBGv13PIS extends MdcLoggable { } // ── POST /bulk-payments/{paymentProduct} ────────────────────────────────────────────────────────── - lazy val initiateBulkPayments: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val initiateBulkPayments: Http4sRoute = Http4sRoute { case req @ POST -> `bgV13Prefix` / "bulk-payments" / paymentProduct => EndpointHelpers.executeFutureCreated(req) { initiatePaymentImpl("bulk-payments", paymentProduct, Some(req.callContext)) @@ -457,7 +457,7 @@ object Http4sBGv13PIS extends MdcLoggable { // scaAuthenticationData → startPaymentAuthorisationTransactionAuthorisation (real SCA logic) // psuData → startPaymentAuthorisationUpdatePsuAuthentication (mocked) // authenticationMethodId → startPaymentAuthorisationSelectPsuAuthenticationMethod (mocked) - lazy val startPaymentAuthorisationAll: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val startPaymentAuthorisationAll: Http4sRoute = Http4sRoute { case req @ POST -> `bgV13Prefix` / paymentService / paymentProduct / paymentId / "authorisations" => EndpointHelpers.executeFutureCreated(req) { val cc = req.callContext @@ -519,7 +519,7 @@ object Http4sBGv13PIS extends MdcLoggable { } // ── POST /{paymentService}/{paymentProduct}/{paymentId}/cancellation-authorisations (3 variants) ─ - lazy val startPaymentInitiationCancellationAuthorisationAll: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val startPaymentInitiationCancellationAuthorisationAll: Http4sRoute = Http4sRoute { case req @ POST -> `bgV13Prefix` / paymentService / paymentProduct / paymentId / "cancellation-authorisations" => EndpointHelpers.executeFutureCreated(req) { val cc = req.callContext @@ -586,7 +586,7 @@ object Http4sBGv13PIS extends MdcLoggable { } // ── PUT /{paymentService}/{paymentProduct}/{paymentId}/cancellation-authorisations/{authorisationId} (4 variants) ─ - lazy val updatePaymentCancellationPsuDataAll: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val updatePaymentCancellationPsuDataAll: Http4sRoute = Http4sRoute { case req @ PUT -> `bgV13Prefix` / paymentService / paymentProduct / paymentId / "cancellation-authorisations" / authorisationId => EndpointHelpers.executeAndRespond(req) { cc => val callContext = Some(cc) @@ -685,7 +685,7 @@ object Http4sBGv13PIS extends MdcLoggable { } // ── PUT /{paymentService}/{paymentProduct}/{paymentId}/authorisations/{authorisationId} (4 variants) ─ - lazy val updatePaymentPsuDataAll: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val updatePaymentPsuDataAll: Http4sRoute = Http4sRoute { case req @ PUT -> `bgV13Prefix` / paymentService / paymentProduct / paymentId / "authorisations" / authorisationId => EndpointHelpers.executeAndRespond(req) { cc => val callContext = Some(cc) @@ -1344,20 +1344,22 @@ This method updates PSU data on the cancellation authorisation resource if neede initStartAuthorisationResourceDocs() initUpdatePsuDataResourceDocs() - val routes: HttpRoutes[IO] = Kleisli[HttpF, Request[IO], Response[IO]] { req => - cancelPayment(req) - .orElse(getPaymentCancellationScaStatus(req)) - .orElse(getPaymentInformation(req)) - .orElse(getPaymentInitiationAuthorisation(req)) - .orElse(getPaymentInitiationCancellationAuthorisationInformation(req)) - .orElse(getPaymentInitiationScaStatus(req)) - .orElse(getPaymentInitiationStatus(req)) - .orElse(initiatePayments(req)) - .orElse(initiatePeriodicPayments(req)) - .orElse(initiateBulkPayments(req)) - .orElse(startPaymentAuthorisationAll(req)) - .orElse(startPaymentInitiationCancellationAuthorisationAll(req)) - .orElse(updatePaymentCancellationPsuDataAll(req)) - .orElse(updatePaymentPsuDataAll(req)) - } + lazy val routesInOrder: List[Http4sHandler] = List( + cancelPayment, + getPaymentCancellationScaStatus, + getPaymentInformation, + getPaymentInitiationAuthorisation, + getPaymentInitiationCancellationAuthorisationInformation, + getPaymentInitiationScaStatus, + getPaymentInitiationStatus, + initiatePayments, + initiatePeriodicPayments, + initiateBulkPayments, + startPaymentAuthorisationAll, + startPaymentInitiationCancellationAuthorisationAll, + updatePaymentCancellationPsuDataAll, + updatePaymentPsuDataAll + ) + + lazy val routes: HttpRoutes[IO] = Http4sRoute.chain(routesInOrder) } diff --git a/obp-api/src/main/scala/code/api/berlin/group/v1_3/Http4sBGv13SigningBaskets.scala b/obp-api/src/main/scala/code/api/berlin/group/v1_3/Http4sBGv13SigningBaskets.scala index aa8ca2788e..16598483f1 100644 --- a/obp-api/src/main/scala/code/api/berlin/group/v1_3/Http4sBGv13SigningBaskets.scala +++ b/obp-api/src/main/scala/code/api/berlin/group/v1_3/Http4sBGv13SigningBaskets.scala @@ -32,7 +32,7 @@ import cats.data.{Kleisli, OptionT} import cats.effect._ import code.api.berlin.group.ConstantsBG import code.api.berlin.group.v1_3.JSONFactory_BERLIN_GROUP_1_3._ -import code.api.util.APIUtil.{EmptyBody, ResourceDoc, connectorEmptyResponse, getSuggestedDefaultScaMethod, mockedDataText, passesPsd2Pisp, unboxFullOrFail} +import code.api.util.APIUtil.{Http4sHandler, Http4sRoute, EmptyBody, ResourceDoc, connectorEmptyResponse, getSuggestedDefaultScaMethod, mockedDataText, passesPsd2Pisp, unboxFullOrFail} import code.api.util.ApiTag._ import code.api.util.ErrorMessages._ import code.api.util.CustomJsonFormats @@ -74,7 +74,7 @@ object Http4sBGv13SigningBaskets extends MdcLoggable { val bgV13Prefix = Root / ConstantsBG.berlinGroupVersion1.urlPrefix / ConstantsBG.berlinGroupVersion1.apiShortVersion // ── POST /signing-baskets ────────────────────────────────────────────── - val createSigningBasket: HttpRoutes[IO] = HttpRoutes.of[IO] { + val createSigningBasket: Http4sRoute = Http4sRoute { case req @ POST -> `bgV13Prefix` / "signing-baskets" => EndpointHelpers.executeFutureCreated(req) { val cc = req.callContext @@ -143,7 +143,7 @@ The resource identifications of these transactions are contained in the payload ) // ── DELETE /signing-baskets/BASKETID ────────────────────────────────── - val deleteSigningBasket: HttpRoutes[IO] = HttpRoutes.of[IO] { + val deleteSigningBasket: Http4sRoute = Http4sRoute { case req @ DELETE -> `bgV13Prefix` / "signing-baskets" / basketid => EndpointHelpers.executeDelete(req) { cc => val callContext = Some(cc) @@ -177,7 +177,7 @@ Nevertheless, single transactions might be cancelled on an individual basis on t ) // ── GET /signing-baskets/BASKETID ───────────────────────────────────── - val getSigningBasket: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getSigningBasket: Http4sRoute = Http4sRoute { case req @ GET -> `bgV13Prefix` / "signing-baskets" / basketid => EndpointHelpers.executeAndRespond(req) { cc => val callContext = Some(cc) @@ -212,7 +212,7 @@ Returns the content of an signing basket object.""", ) // ── GET /signing-baskets/BASKETID/authorisations ────────────────────── - val getSigningBasketAuthorisation: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getSigningBasketAuthorisation: Http4sRoute = Http4sRoute { case req @ GET -> `bgV13Prefix` / "signing-baskets" / basketid / "authorisations" => EndpointHelpers.executeAndRespond(req) { cc => val callContext = Some(cc) @@ -246,7 +246,7 @@ This function returns an array of hyperlinks to all generated authorisation sub- ) // ── GET /signing-baskets/BASKETID/authorisations/AUTHORISATIONID ─────── - val getSigningBasketScaStatus: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getSigningBasketScaStatus: Http4sRoute = Http4sRoute { case req @ GET -> `bgV13Prefix` / "signing-baskets" / basketId / "authorisations" / authorisationId => EndpointHelpers.executeAndRespond(req) { cc => val callContext = Some(cc) @@ -282,7 +282,7 @@ This method returns the SCA status of a signing basket's authorisation sub-resou ) // ── GET /signing-baskets/BASKETID/status ────────────────────────────── - val getSigningBasketStatus: HttpRoutes[IO] = HttpRoutes.of[IO] { + val getSigningBasketStatus: Http4sRoute = Http4sRoute { case req @ GET -> `bgV13Prefix` / "signing-baskets" / basketid / "status" => EndpointHelpers.executeAndRespond(req) { cc => val callContext = Some(cc) @@ -316,7 +316,7 @@ Returns the status of a signing basket object. ) // ── POST /signing-baskets/BASKETID/authorisations ───────────────────── - val startSigningBasketAuthorisation: HttpRoutes[IO] = HttpRoutes.of[IO] { + val startSigningBasketAuthorisation: Http4sRoute = Http4sRoute { case req @ POST -> `bgV13Prefix` / "signing-baskets" / basketId / "authorisations" => EndpointHelpers.executeFutureCreated(req) { val cc = req.callContext @@ -414,7 +414,7 @@ This applies in the following scenarios: ) // ── PUT /signing-baskets/BASKETID/authorisations/AUTHORISATIONID ─────── - val updateSigningBasketPsuData: HttpRoutes[IO] = HttpRoutes.of[IO] { + val updateSigningBasketPsuData: Http4sRoute = Http4sRoute { case req @ PUT -> `bgV13Prefix` / "signing-baskets" / basketId / "authorisations" / authorisationId => EndpointHelpers.executeAndRespond(req) { cc => val callContext = Some(cc) @@ -546,14 +546,16 @@ There are the following request types on this access path: http4sPartialFunction = Some(updateSigningBasketPsuData) ) - val routes: HttpRoutes[IO] = Kleisli[HttpF, Request[IO], Response[IO]] { req => - createSigningBasket(req) - .orElse(deleteSigningBasket(req)) - .orElse(getSigningBasket(req)) - .orElse(getSigningBasketAuthorisation(req)) - .orElse(getSigningBasketScaStatus(req)) - .orElse(getSigningBasketStatus(req)) - .orElse(startSigningBasketAuthorisation(req)) - .orElse(updateSigningBasketPsuData(req)) - } + lazy val routesInOrder: List[Http4sHandler] = List( + createSigningBasket, + deleteSigningBasket, + getSigningBasket, + getSigningBasketAuthorisation, + getSigningBasketScaStatus, + getSigningBasketStatus, + startSigningBasketAuthorisation, + updateSigningBasketPsuData + ) + + lazy val routes: HttpRoutes[IO] = Http4sRoute.chain(routesInOrder) } diff --git a/obp-api/src/main/scala/code/api/berlin/group/v2/Http4sBGv2.scala b/obp-api/src/main/scala/code/api/berlin/group/v2/Http4sBGv2.scala index 2c224fa7b4..8c51736be9 100644 --- a/obp-api/src/main/scala/code/api/berlin/group/v2/Http4sBGv2.scala +++ b/obp-api/src/main/scala/code/api/berlin/group/v2/Http4sBGv2.scala @@ -30,9 +30,9 @@ package code.api.berlin.group.v2 import cats.data.{Kleisli, OptionT} import cats.effect._ import code.api.berlin.group.ConstantsBG -import code.api.util.APIUtil.ResourceDoc +import code.api.util.APIUtil.{Http4sHandler, Http4sRoute, ResourceDoc} import code.api.util.ScannedApis -import code.api.util.http4s.ResourceDocMiddleware +import code.api.util.http4s.{ResourceDocMatcher, ResourceDocMiddleware} import code.api.util.http4s.IdempotencyMiddleware import code.util.Helper.MdcLoggable import com.openbankproject.commons.util.ScannedApiVersion @@ -57,11 +57,14 @@ object Http4sBGv2 extends MdcLoggable with ScannedApis { override val allResourceDocs: ArrayBuffer[ResourceDoc] = resourceDocs - val allRoutes: HttpRoutes[IO] = Kleisli[HttpF, Request[IO], Response[IO]] { req => - Http4sBGv2AIS.routes(req) - .orElse(Http4sBGv2PIS.routes(req)) - .orElse(Http4sBGv2PIIS.routes(req)) - } + lazy val routesInOrder: List[Http4sHandler] = + Http4sBGv2AIS.routesInOrder ++ + Http4sBGv2PIS.routesInOrder ++ + Http4sBGv2PIIS.routesInOrder - val wrappedRoutes: HttpRoutes[IO] = ResourceDocMiddleware.apply(resourceDocs)(IdempotencyMiddleware(allRoutes)) + lazy val orderedResourceDocs: ArrayBuffer[ResourceDoc] = ResourceDocMatcher.orderByRoutes(resourceDocs, routesInOrder) + + lazy val allRoutes: HttpRoutes[IO] = Http4sRoute.chain(routesInOrder) + + lazy val wrappedRoutes: HttpRoutes[IO] = ResourceDocMiddleware.apply(orderedResourceDocs, routes => IdempotencyMiddleware(routes))(IdempotencyMiddleware(allRoutes)) } diff --git a/obp-api/src/main/scala/code/api/berlin/group/v2/Http4sBGv2AIS.scala b/obp-api/src/main/scala/code/api/berlin/group/v2/Http4sBGv2AIS.scala index fea286407e..f3ee40af61 100644 --- a/obp-api/src/main/scala/code/api/berlin/group/v2/Http4sBGv2AIS.scala +++ b/obp-api/src/main/scala/code/api/berlin/group/v2/Http4sBGv2AIS.scala @@ -31,7 +31,7 @@ import org.json4s._ import cats.data.{Kleisli, OptionT} import cats.effect._ import code.api.berlin.group.ConstantsBG -import code.api.util.APIUtil.{EmptyBody, ResourceDoc} +import code.api.util.APIUtil.{Http4sHandler, Http4sRoute, EmptyBody, ResourceDoc} import code.api.util.ApiTag._ import code.api.util.ErrorMessages._ import code.api.util.CustomJsonFormats @@ -73,7 +73,7 @@ object Http4sBGv2AIS extends MdcLoggable { http4sPartialFunction = Some(getAccountList) ) - val getAccountList: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getAccountList: Http4sRoute = Http4sRoute { case req @ GET -> `bgV2Prefix` / "accounts" => Ok(convertAnyToJsonString(JSONFactory_BERLIN_GROUP_v2.mockAccountList)) } @@ -94,7 +94,7 @@ object Http4sBGv2AIS extends MdcLoggable { http4sPartialFunction = Some(getAccountDetails) ) - val getAccountDetails: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getAccountDetails: Http4sRoute = Http4sRoute { case req @ GET -> `bgV2Prefix` / "accounts" / accountId if !accountId.contains("/") => Ok(convertAnyToJsonString(JSONFactory_BERLIN_GROUP_v2.mockAccountDetails(accountId))) } @@ -115,7 +115,7 @@ object Http4sBGv2AIS extends MdcLoggable { http4sPartialFunction = Some(getAccountBalances) ) - val getAccountBalances: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getAccountBalances: Http4sRoute = Http4sRoute { case req @ GET -> `bgV2Prefix` / "accounts" / accountId / "balances" => Ok(convertAnyToJsonString(JSONFactory_BERLIN_GROUP_v2.mockBalances(accountId))) } @@ -136,7 +136,7 @@ object Http4sBGv2AIS extends MdcLoggable { http4sPartialFunction = Some(getTransactionList) ) - val getTransactionList: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getTransactionList: Http4sRoute = Http4sRoute { case req @ GET -> `bgV2Prefix` / "accounts" / accountId / "transactions" => Ok(convertAnyToJsonString(JSONFactory_BERLIN_GROUP_v2.mockTransactions(accountId))) } @@ -157,7 +157,7 @@ object Http4sBGv2AIS extends MdcLoggable { http4sPartialFunction = Some(getTransactionDetails) ) - val getTransactionDetails: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getTransactionDetails: Http4sRoute = Http4sRoute { case req @ GET -> `bgV2Prefix` / "accounts" / accountId / "transactions" / transactionId => Ok(convertAnyToJsonString(JSONFactory_BERLIN_GROUP_v2.mockTransactionDetails(accountId, transactionId))) } @@ -178,7 +178,7 @@ object Http4sBGv2AIS extends MdcLoggable { http4sPartialFunction = Some(getCardAccountList) ) - val getCardAccountList: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getCardAccountList: Http4sRoute = Http4sRoute { case req @ GET -> `bgV2Prefix` / "card-accounts" => Ok(convertAnyToJsonString(JSONFactory_BERLIN_GROUP_v2.mockCardAccountList)) } @@ -199,7 +199,7 @@ object Http4sBGv2AIS extends MdcLoggable { http4sPartialFunction = Some(getCardAccountDetails) ) - val getCardAccountDetails: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getCardAccountDetails: Http4sRoute = Http4sRoute { case req @ GET -> `bgV2Prefix` / "card-accounts" / accountId if !accountId.contains("/") => Ok(convertAnyToJsonString(JSONFactory_BERLIN_GROUP_v2.mockCardAccountDetails(accountId))) } @@ -220,7 +220,7 @@ object Http4sBGv2AIS extends MdcLoggable { http4sPartialFunction = Some(getCardAccountBalances) ) - val getCardAccountBalances: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getCardAccountBalances: Http4sRoute = Http4sRoute { case req @ GET -> `bgV2Prefix` / "card-accounts" / accountId / "balances" => Ok(convertAnyToJsonString(JSONFactory_BERLIN_GROUP_v2.mockCardAccountBalances(accountId))) } @@ -241,22 +241,24 @@ object Http4sBGv2AIS extends MdcLoggable { http4sPartialFunction = Some(getCardAccountTransactionList) ) - val getCardAccountTransactionList: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getCardAccountTransactionList: Http4sRoute = Http4sRoute { case req @ GET -> `bgV2Prefix` / "card-accounts" / accountId / "transactions" => Ok(convertAnyToJsonString(JSONFactory_BERLIN_GROUP_v2.mockCardAccountTransactions(accountId))) } // ── Combined routes ─────────────────────────────────────────────── - val routes: HttpRoutes[IO] = Kleisli[HttpF, Request[IO], Response[IO]] { req => - getAccountList(req) - .orElse(getAccountBalances(req)) - .orElse(getTransactionDetails(req)) - .orElse(getTransactionList(req)) - .orElse(getAccountDetails(req)) - .orElse(getCardAccountList(req)) - .orElse(getCardAccountBalances(req)) - .orElse(getCardAccountTransactionList(req)) - .orElse(getCardAccountDetails(req)) - } + lazy val routesInOrder: List[Http4sHandler] = List( + getAccountList, + getAccountBalances, + getTransactionDetails, + getTransactionList, + getAccountDetails, + getCardAccountList, + getCardAccountBalances, + getCardAccountTransactionList, + getCardAccountDetails + ) + + lazy val routes: HttpRoutes[IO] = Http4sRoute.chain(routesInOrder) } diff --git a/obp-api/src/main/scala/code/api/berlin/group/v2/Http4sBGv2PIIS.scala b/obp-api/src/main/scala/code/api/berlin/group/v2/Http4sBGv2PIIS.scala index 19b95113c1..03d47c2077 100644 --- a/obp-api/src/main/scala/code/api/berlin/group/v2/Http4sBGv2PIIS.scala +++ b/obp-api/src/main/scala/code/api/berlin/group/v2/Http4sBGv2PIIS.scala @@ -31,7 +31,7 @@ import org.json4s._ import cats.data.{Kleisli, OptionT} import cats.effect._ import code.api.berlin.group.ConstantsBG -import code.api.util.APIUtil.{EmptyBody, ResourceDoc} +import code.api.util.APIUtil.{Http4sHandler, Http4sRoute, EmptyBody, ResourceDoc} import code.api.util.ApiTag._ import code.api.util.ErrorMessages._ import code.api.util.CustomJsonFormats @@ -73,14 +73,16 @@ object Http4sBGv2PIIS extends MdcLoggable { http4sPartialFunction = Some(postConfirmationOfFunds) ) - val postConfirmationOfFunds: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val postConfirmationOfFunds: Http4sRoute = Http4sRoute { case req @ POST -> `bgV2Prefix` / "funds-confirmations" => Ok(convertAnyToJsonString(JSONFactory_BERLIN_GROUP_v2.mockFundsConfirmation)) } // ── Combined routes ─────────────────────────────────────────────── - val routes: HttpRoutes[IO] = Kleisli[HttpF, Request[IO], Response[IO]] { req => - postConfirmationOfFunds(req) - } + lazy val routesInOrder: List[Http4sHandler] = List( + postConfirmationOfFunds + ) + + lazy val routes: HttpRoutes[IO] = Http4sRoute.chain(routesInOrder) } diff --git a/obp-api/src/main/scala/code/api/berlin/group/v2/Http4sBGv2PIS.scala b/obp-api/src/main/scala/code/api/berlin/group/v2/Http4sBGv2PIS.scala index 7ac767a131..a6472437ca 100644 --- a/obp-api/src/main/scala/code/api/berlin/group/v2/Http4sBGv2PIS.scala +++ b/obp-api/src/main/scala/code/api/berlin/group/v2/Http4sBGv2PIS.scala @@ -31,7 +31,7 @@ import org.json4s._ import cats.data.{Kleisli, OptionT} import cats.effect._ import code.api.berlin.group.ConstantsBG -import code.api.util.APIUtil.{EmptyBody, ResourceDoc} +import code.api.util.APIUtil.{Http4sHandler, Http4sRoute, EmptyBody, ResourceDoc} import code.api.util.ApiTag._ import code.api.util.ErrorMessages._ import code.api.util.CustomJsonFormats @@ -73,7 +73,7 @@ object Http4sBGv2PIS extends MdcLoggable { http4sPartialFunction = Some(initiatePayment) ) - val initiatePayment: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val initiatePayment: Http4sRoute = Http4sRoute { case req @ POST -> `bgV2Prefix` / "payments" / paymentProduct => Created(convertAnyToJsonString(JSONFactory_BERLIN_GROUP_v2.mockPaymentInitiation(paymentProduct))) } @@ -94,7 +94,7 @@ object Http4sBGv2PIS extends MdcLoggable { http4sPartialFunction = Some(initiateBulkPayment) ) - val initiateBulkPayment: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val initiateBulkPayment: Http4sRoute = Http4sRoute { case req @ POST -> `bgV2Prefix` / "bulk-payments" / paymentProduct => Created(convertAnyToJsonString(JSONFactory_BERLIN_GROUP_v2.mockPaymentInitiation(paymentProduct))) } @@ -115,7 +115,7 @@ object Http4sBGv2PIS extends MdcLoggable { http4sPartialFunction = Some(initiatePeriodicPayment) ) - val initiatePeriodicPayment: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val initiatePeriodicPayment: Http4sRoute = Http4sRoute { case req @ POST -> `bgV2Prefix` / "periodic-payments" / paymentProduct => Created(convertAnyToJsonString(JSONFactory_BERLIN_GROUP_v2.mockPaymentInitiation(paymentProduct))) } @@ -137,7 +137,7 @@ object Http4sBGv2PIS extends MdcLoggable { http4sPartialFunction = Some(getBulkPaymentExtendedStatus) ) - val getBulkPaymentExtendedStatus: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getBulkPaymentExtendedStatus: Http4sRoute = Http4sRoute { case req @ GET -> `bgV2Prefix` / "bulk-payments" / paymentProduct / paymentId / "extended-status" => Ok(convertAnyToJsonString(JSONFactory_BERLIN_GROUP_v2.mockBulkPaymentExtendedStatus(paymentProduct, paymentId))) } @@ -158,7 +158,7 @@ object Http4sBGv2PIS extends MdcLoggable { http4sPartialFunction = Some(getPaymentStatus) ) - val getPaymentStatus: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getPaymentStatus: Http4sRoute = Http4sRoute { case req @ GET -> `bgV2Prefix` / paymentService / paymentProduct / paymentId / "status" if Set("payments", "bulk-payments", "periodic-payments").contains(paymentService) => Ok(convertAnyToJsonString(JSONFactory_BERLIN_GROUP_v2.mockPaymentStatus)) @@ -180,7 +180,7 @@ object Http4sBGv2PIS extends MdcLoggable { http4sPartialFunction = Some(getPayment) ) - val getPayment: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getPayment: Http4sRoute = Http4sRoute { case req @ GET -> `bgV2Prefix` / paymentService / paymentProduct / paymentId if Set("payments", "bulk-payments", "periodic-payments").contains(paymentService) => Ok(convertAnyToJsonString(JSONFactory_BERLIN_GROUP_v2.mockPaymentDetails(paymentService, paymentProduct, paymentId))) @@ -202,7 +202,7 @@ object Http4sBGv2PIS extends MdcLoggable { http4sPartialFunction = Some(deletePayment) ) - val deletePayment: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val deletePayment: Http4sRoute = Http4sRoute { case req @ DELETE -> `bgV2Prefix` / paymentService / paymentProduct / paymentId if Set("payments", "bulk-payments", "periodic-payments").contains(paymentService) => NoContent() @@ -224,7 +224,7 @@ object Http4sBGv2PIS extends MdcLoggable { http4sPartialFunction = Some(startAuthorisation) ) - val startAuthorisation: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val startAuthorisation: Http4sRoute = Http4sRoute { case req @ POST -> `bgV2Prefix` / paymentService / paymentProduct / resourceId / authorisationCategory if Set("payments", "bulk-payments", "periodic-payments").contains(paymentService) && Set("authorisations", "cancellation-authorisations").contains(authorisationCategory) => @@ -248,7 +248,7 @@ object Http4sBGv2PIS extends MdcLoggable { http4sPartialFunction = Some(getAuthorisationSubResources) ) - val getAuthorisationSubResources: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getAuthorisationSubResources: Http4sRoute = Http4sRoute { case req @ GET -> `bgV2Prefix` / paymentService / paymentProduct / resourceId / authorisationCategory if Set("payments", "bulk-payments", "periodic-payments").contains(paymentService) && Set("authorisations", "cancellation-authorisations").contains(authorisationCategory) => @@ -272,7 +272,7 @@ object Http4sBGv2PIS extends MdcLoggable { http4sPartialFunction = Some(getAuthorisationStatus) ) - val getAuthorisationStatus: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getAuthorisationStatus: Http4sRoute = Http4sRoute { case req @ GET -> `bgV2Prefix` / paymentService / paymentProduct / resourceId / authorisationCategory / authorisationId if Set("payments", "bulk-payments", "periodic-payments").contains(paymentService) && Set("authorisations", "cancellation-authorisations").contains(authorisationCategory) => @@ -295,7 +295,7 @@ object Http4sBGv2PIS extends MdcLoggable { http4sPartialFunction = Some(updatePsuData) ) - val updatePsuData: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val updatePsuData: Http4sRoute = Http4sRoute { case req @ PUT -> `bgV2Prefix` / paymentService / paymentProduct / resourceId / authorisationCategory / authorisationId if Set("payments", "bulk-payments", "periodic-payments").contains(paymentService) && Set("authorisations", "cancellation-authorisations").contains(authorisationCategory) => @@ -318,7 +318,7 @@ object Http4sBGv2PIS extends MdcLoggable { http4sPartialFunction = Some(updateResourceWithDebtorAccount) ) - val updateResourceWithDebtorAccount: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val updateResourceWithDebtorAccount: Http4sRoute = Http4sRoute { case req @ PUT -> `bgV2Prefix` / paymentService / paymentProduct / resourceId if Set("payments", "bulk-payments", "periodic-payments").contains(paymentService) => Ok(convertAnyToJsonString(JSONFactory_BERLIN_GROUP_v2.mockUpdateDebtorAccount(resourceId))) @@ -327,26 +327,20 @@ object Http4sBGv2PIS extends MdcLoggable { // ── Combined routes (ordering matters!) ─────────────────────────── // More specific paths first, then generic patterns - val routes: HttpRoutes[IO] = Kleisli[HttpF, Request[IO], Response[IO]] { req => - // POST routes (2-segment after prefix) - initiatePayment(req) - .orElse(initiateBulkPayment(req)) - .orElse(initiatePeriodicPayment(req)) - // GET specific 4-segment: bulk extended status - .orElse(getBulkPaymentExtendedStatus(req)) - // GET/DELETE generic 4-segment: status - .orElse(getPaymentStatus(req)) - // 5-segment: authorisation with ID - .orElse(getAuthorisationStatus(req)) - .orElse(updatePsuData(req)) - // 4-segment: authorisation list / start - .orElse(startAuthorisation(req)) - .orElse(getAuthorisationSubResources(req)) - // DELETE 3-segment - .orElse(deletePayment(req)) - // GET 3-segment: payment details - .orElse(getPayment(req)) - // PUT 3-segment: debtor account update - .orElse(updateResourceWithDebtorAccount(req)) - } + lazy val routesInOrder: List[Http4sHandler] = List( + initiatePayment, + initiateBulkPayment, + initiatePeriodicPayment, + getBulkPaymentExtendedStatus, + getPaymentStatus, + getAuthorisationStatus, + updatePsuData, + startAuthorisation, + getAuthorisationSubResources, + deletePayment, + getPayment, + updateResourceWithDebtorAccount + ) + + lazy val routes: HttpRoutes[IO] = Http4sRoute.chain(routesInOrder) } From 3ad5274e15b1af871be4328f615c53e1f5891660 Mon Sep 17 00:00:00 2001 From: Hongwei Date: Wed, 7 Oct 2026 17:29:59 +0200 Subject: [PATCH 18/29] refactor: bind every UK Open Banking v2.0.0, v3.1.0 and v4.0.1 ResourceDoc to its route Each group lists its routes in order and the aggregator concatenates them, so the middleware selects the doc of the route that serves a request. --- .../UKOpenBanking/v2_0_0/Http4sUKOBv200.scala | 15 +- .../v2_0_0/Http4sUKOBv200AIS.scala | 28 +-- .../UKOpenBanking/v3_1_0/Http4sUKOBv310.scala | 55 +++--- .../v3_1_0/Http4sUKOBv310AccountAccess.scala | 20 +- .../v3_1_0/Http4sUKOBv310Accounts.scala | 16 +- .../v3_1_0/Http4sUKOBv310Balances.scala | 16 +- .../v3_1_0/Http4sUKOBv310Beneficiaries.scala | 15 +- .../v3_1_0/Http4sUKOBv310DirectDebits.scala | 15 +- .../Http4sUKOBv310DomesticPayments.scala | 28 +-- ...p4sUKOBv310DomesticScheduledPayments.scala | 24 +-- ...Http4sUKOBv310DomesticStandingOrders.scala | 24 +-- .../v3_1_0/Http4sUKOBv310FilePayments.scala | 36 ++-- .../Http4sUKOBv310FundsConfirmations.scala | 24 +-- .../Http4sUKOBv310InternationalPayments.scala | 28 +-- ...OBv310InternationalScheduledPayments.scala | 28 +-- ...sUKOBv310InternationalStandingOrders.scala | 24 +-- .../v3_1_0/Http4sUKOBv310Offers.scala | 15 +- .../v3_1_0/Http4sUKOBv310Partys.scala | 15 +- .../v3_1_0/Http4sUKOBv310Products.scala | 16 +- .../Http4sUKOBv310ScheduledPayments.scala | 15 +- .../v3_1_0/Http4sUKOBv310StandingOrders.scala | 15 +- .../v3_1_0/Http4sUKOBv310Statements.scala | 28 +-- .../v3_1_0/Http4sUKOBv310Transactions.scala | 20 +- .../UKOpenBanking/v4_0_1/Http4sUKOBv401.scala | 25 +-- .../v4_0_1/Http4sUKOBv401AccountInfo.scala | 124 ++++++------- .../Http4sUKOBv401ConfirmationFunds.scala | 24 +-- .../Http4sUKOBv401EventNotifications.scala | 12 +- .../v4_0_1/Http4sUKOBv401Events.scala | 28 +-- .../Http4sUKOBv401PaymentInitiation.scala | 172 +++++++++--------- .../v4_0_1/Http4sUKOBv401Vrp.scala | 44 ++--- 30 files changed, 509 insertions(+), 440 deletions(-) diff --git a/obp-api/src/main/scala/code/api/UKOpenBanking/v2_0_0/Http4sUKOBv200.scala b/obp-api/src/main/scala/code/api/UKOpenBanking/v2_0_0/Http4sUKOBv200.scala index 1c83dc867e..b2235269bb 100644 --- a/obp-api/src/main/scala/code/api/UKOpenBanking/v2_0_0/Http4sUKOBv200.scala +++ b/obp-api/src/main/scala/code/api/UKOpenBanking/v2_0_0/Http4sUKOBv200.scala @@ -29,8 +29,8 @@ package code.api.UKOpenBanking.v2_0_0 import cats.data.{Kleisli, OptionT} import cats.effect._ -import code.api.util.APIUtil.ResourceDoc -import code.api.util.http4s.ResourceDocMiddleware +import code.api.util.APIUtil.{Http4sHandler, Http4sRoute, ResourceDoc} +import code.api.util.http4s.{ResourceDocMatcher, ResourceDocMiddleware} import code.api.util.http4s.IdempotencyMiddleware import code.util.Helper.MdcLoggable import com.openbankproject.commons.util.ApiVersion @@ -57,9 +57,12 @@ object Http4sUKOBv200 extends MdcLoggable { val resourceDocs: ArrayBuffer[ResourceDoc] = Http4sUKOBv200AIS.resourceDocs - val allRoutes: HttpRoutes[IO] = Kleisli[HttpF, Request[IO], Response[IO]] { req => - Http4sUKOBv200AIS.routes(req) - } + lazy val routesInOrder: List[Http4sHandler] = + Http4sUKOBv200AIS.routesInOrder - val wrappedRoutes: HttpRoutes[IO] = ResourceDocMiddleware.apply(resourceDocs)(IdempotencyMiddleware(allRoutes)) + lazy val orderedResourceDocs: ArrayBuffer[ResourceDoc] = ResourceDocMatcher.orderByRoutes(resourceDocs, routesInOrder) + + lazy val allRoutes: HttpRoutes[IO] = Http4sRoute.chain(routesInOrder) + + lazy val wrappedRoutes: HttpRoutes[IO] = ResourceDocMiddleware.apply(orderedResourceDocs, routes => IdempotencyMiddleware(routes))(IdempotencyMiddleware(allRoutes)) } diff --git a/obp-api/src/main/scala/code/api/UKOpenBanking/v2_0_0/Http4sUKOBv200AIS.scala b/obp-api/src/main/scala/code/api/UKOpenBanking/v2_0_0/Http4sUKOBv200AIS.scala index 5499510c37..e46d86ddd7 100644 --- a/obp-api/src/main/scala/code/api/UKOpenBanking/v2_0_0/Http4sUKOBv200AIS.scala +++ b/obp-api/src/main/scala/code/api/UKOpenBanking/v2_0_0/Http4sUKOBv200AIS.scala @@ -32,7 +32,7 @@ import cats.data.{Kleisli, OptionT} import cats.effect.IO import code.api.APIFailureNewStyle import code.api.ResourceDocs1_4_0.SwaggerDefinitionsJSON -import code.api.util.APIUtil.{HTTPParam, EmptyBody, ResourceDoc, createQueriesByHttpParams, fullBoxOrException, unboxFull} +import code.api.util.APIUtil.{Http4sHandler, Http4sRoute, HTTPParam, EmptyBody, ResourceDoc, createQueriesByHttpParams, fullBoxOrException, unboxFull} import code.api.util.ApiTag._ import code.api.util.CustomJsonFormats import code.api.util.ErrorMessages.{AuthenticatedUserIsRequired, UnknownError} @@ -74,7 +74,7 @@ object Http4sUKOBv200AIS extends MdcLoggable { val ukV20Prefix = Root / ApiVersion.ukOpenBankingV20.urlPrefix / ApiVersion.ukOpenBankingV20.apiShortVersion // GET /accounts — list all private accounts of the logged-in user - lazy val getAccountList: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getAccountList: Http4sRoute = Http4sRoute { case req @ GET -> `ukV20Prefix` / "accounts" => EndpointHelpers.withUser(req) { (u, cc) => val callContext = Some(cc) @@ -99,7 +99,7 @@ object Http4sUKOBv200AIS extends MdcLoggable { ) // GET /accounts/{accountId} — single account - lazy val getAccount: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getAccount: Http4sRoute = Http4sRoute { case req @ GET -> `ukV20Prefix` / "accounts" / accountId => EndpointHelpers.withUser(req) { (u, cc) => val callContext = Some(cc) @@ -124,7 +124,7 @@ object Http4sUKOBv200AIS extends MdcLoggable { ) // GET /balances — bulk balances for all private accounts - lazy val getBalances: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getBalances: Http4sRoute = Http4sRoute { case req @ GET -> `ukV20Prefix` / "balances" => EndpointHelpers.withUser(req) { (u, cc) => val callContext = Some(cc) @@ -149,7 +149,7 @@ object Http4sUKOBv200AIS extends MdcLoggable { ) // GET /accounts/{accountId}/balances — account-level balances - lazy val getAccountBalances: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getAccountBalances: Http4sRoute = Http4sRoute { case req @ GET -> `ukV20Prefix` / "accounts" / accountIdStr / "balances" => EndpointHelpers.withUser(req) { (u, cc) => for { @@ -176,7 +176,7 @@ object Http4sUKOBv200AIS extends MdcLoggable { ) // GET /accounts/{accountId}/transactions — account-level transactions - lazy val getAccountTransactions: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getAccountTransactions: Http4sRoute = Http4sRoute { case req @ GET -> `ukV20Prefix` / "accounts" / accountIdStr / "transactions" => EndpointHelpers.withUser(req) { (u, cc) => for { @@ -206,11 +206,13 @@ object Http4sUKOBv200AIS extends MdcLoggable { http4sPartialFunction = Some(getAccountTransactions) ) - val routes: HttpRoutes[IO] = Kleisli[HttpF, Request[IO], Response[IO]] { req => - getAccountList(req) - .orElse(getAccount(req)) - .orElse(getAccountBalances(req)) - .orElse(getAccountTransactions(req)) - .orElse(getBalances(req)) - } + lazy val routesInOrder: List[Http4sHandler] = List( + getAccountList, + getAccount, + getAccountBalances, + getAccountTransactions, + getBalances + ) + + lazy val routes: HttpRoutes[IO] = Http4sRoute.chain(routesInOrder) } diff --git a/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310.scala b/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310.scala index 17cb0130a2..f10dc0e972 100644 --- a/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310.scala +++ b/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310.scala @@ -29,8 +29,8 @@ package code.api.UKOpenBanking.v3_1_0 import cats.data.{Kleisli, OptionT} import cats.effect._ -import code.api.util.APIUtil.ResourceDoc -import code.api.util.http4s.ResourceDocMiddleware +import code.api.util.APIUtil.{Http4sHandler, Http4sRoute, ResourceDoc} +import code.api.util.http4s.{ResourceDocMatcher, ResourceDocMiddleware} import code.api.util.http4s.IdempotencyMiddleware import code.util.Helper.MdcLoggable import com.openbankproject.commons.util.ApiVersion @@ -79,28 +79,31 @@ object Http4sUKOBv310 extends MdcLoggable { Http4sUKOBv310Balances.resourceDocs ++ Http4sUKOBv310Transactions.resourceDocs - val allRoutes: HttpRoutes[IO] = Kleisli[HttpF, Request[IO], Response[IO]] { req => - Http4sUKOBv310AccountAccess.routes(req) - .orElse(Http4sUKOBv310Accounts.routes(req)) - .orElse(Http4sUKOBv310Balances.routes(req)) - .orElse(Http4sUKOBv310Transactions.routes(req)) - .orElse(Http4sUKOBv310Products.routes(req)) - .orElse(Http4sUKOBv310Beneficiaries.routes(req)) - .orElse(Http4sUKOBv310DirectDebits.routes(req)) - .orElse(Http4sUKOBv310Offers.routes(req)) - .orElse(Http4sUKOBv310Partys.routes(req)) - .orElse(Http4sUKOBv310ScheduledPayments.routes(req)) - .orElse(Http4sUKOBv310StandingOrders.routes(req)) - .orElse(Http4sUKOBv310Statements.routes(req)) - .orElse(Http4sUKOBv310DomesticPayments.routes(req)) - .orElse(Http4sUKOBv310DomesticScheduledPayments.routes(req)) - .orElse(Http4sUKOBv310DomesticStandingOrders.routes(req)) - .orElse(Http4sUKOBv310FilePayments.routes(req)) - .orElse(Http4sUKOBv310FundsConfirmations.routes(req)) - .orElse(Http4sUKOBv310InternationalPayments.routes(req)) - .orElse(Http4sUKOBv310InternationalScheduledPayments.routes(req)) - .orElse(Http4sUKOBv310InternationalStandingOrders.routes(req)) - } - - val wrappedRoutes: HttpRoutes[IO] = ResourceDocMiddleware.apply(resourceDocs)(IdempotencyMiddleware(allRoutes)) + lazy val routesInOrder: List[Http4sHandler] = + Http4sUKOBv310AccountAccess.routesInOrder ++ + Http4sUKOBv310Accounts.routesInOrder ++ + Http4sUKOBv310Balances.routesInOrder ++ + Http4sUKOBv310Transactions.routesInOrder ++ + Http4sUKOBv310Products.routesInOrder ++ + Http4sUKOBv310Beneficiaries.routesInOrder ++ + Http4sUKOBv310DirectDebits.routesInOrder ++ + Http4sUKOBv310Offers.routesInOrder ++ + Http4sUKOBv310Partys.routesInOrder ++ + Http4sUKOBv310ScheduledPayments.routesInOrder ++ + Http4sUKOBv310StandingOrders.routesInOrder ++ + Http4sUKOBv310Statements.routesInOrder ++ + Http4sUKOBv310DomesticPayments.routesInOrder ++ + Http4sUKOBv310DomesticScheduledPayments.routesInOrder ++ + Http4sUKOBv310DomesticStandingOrders.routesInOrder ++ + Http4sUKOBv310FilePayments.routesInOrder ++ + Http4sUKOBv310FundsConfirmations.routesInOrder ++ + Http4sUKOBv310InternationalPayments.routesInOrder ++ + Http4sUKOBv310InternationalScheduledPayments.routesInOrder ++ + Http4sUKOBv310InternationalStandingOrders.routesInOrder + + lazy val orderedResourceDocs: ArrayBuffer[ResourceDoc] = ResourceDocMatcher.orderByRoutes(resourceDocs, routesInOrder) + + lazy val allRoutes: HttpRoutes[IO] = Http4sRoute.chain(routesInOrder) + + lazy val wrappedRoutes: HttpRoutes[IO] = ResourceDocMiddleware.apply(orderedResourceDocs, routes => IdempotencyMiddleware(routes))(IdempotencyMiddleware(allRoutes)) } diff --git a/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310AccountAccess.scala b/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310AccountAccess.scala index 4398c80ea3..092ba151d5 100644 --- a/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310AccountAccess.scala +++ b/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310AccountAccess.scala @@ -32,7 +32,7 @@ import cats.data.{Kleisli, OptionT} import cats.effect.IO import code.api.Constant import code.api.UKOpenBanking.v3_1_0.JSONFactory_UKOpenBanking_310.ConsentPostBodyUKV310 -import code.api.util.APIUtil.{EmptyBody, ResourceDoc, UserOrApplication, connectorEmptyResponse, mockedDataText, passesPsd2Aisp, unboxFullOrFail, parseIso8601OrDayDate} +import code.api.util.APIUtil.{Http4sHandler, Http4sRoute, EmptyBody, ResourceDoc, UserOrApplication, connectorEmptyResponse, mockedDataText, passesPsd2Aisp, unboxFullOrFail, parseIso8601OrDayDate} import code.api.util.ApiTag import code.api.util.CustomJsonFormats import code.api.util.ErrorMessages.{AuthenticatedUserIsRequired, ConsentNotFound, ConsentViewNotFund, InvalidJsonFormat, InvalidUKConsentPermissions, UnknownError} @@ -67,7 +67,7 @@ object Http4sUKOBv310AccountAccess extends MdcLoggable { private def parseBody(s: String): code.api.berlin.group.v1_3.JvalueCaseClass = code.api.berlin.group.v1_3.JvalueCaseClass(com.openbankproject.commons.util.JsonAliases.parse(s).asInstanceOf[org.json4s.JObject]) val ukV31Prefix = Root / ApiVersion.ukOpenBankingV31.urlPrefix / ApiVersion.ukOpenBankingV31.apiShortVersion - lazy val createAccountAccessConsents: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val createAccountAccessConsents: Http4sRoute = Http4sRoute { case req @ POST -> `ukV31Prefix` / "account-access-consents" => // Check auth FIRST (before body parsing) to mirror Lift's wrappedWithAuthCheck behaviour: // unauthenticated → 401, invalid body → 400. withUserAndBodyCreated parses body first @@ -206,7 +206,7 @@ object Http4sUKOBv310AccountAccess extends MdcLoggable { http4sPartialFunction = Some(createAccountAccessConsents) ) - lazy val deleteAccountAccessConsentsConsentId: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val deleteAccountAccessConsentsConsentId: Http4sRoute = Http4sRoute { case req @ DELETE -> `ukV31Prefix` / "account-access-consents" / consentId => // Not withUserDelete: the standard has the AISP revoke its own consent with a // client-credentials token, which carries no PSU. Consent.checkUKConsentAccess decides who @@ -243,7 +243,7 @@ object Http4sUKOBv310AccountAccess extends MdcLoggable { http4sPartialFunction = Some(deleteAccountAccessConsentsConsentId) ) - lazy val getAccountAccessConsentsConsentId: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getAccountAccessConsentsConsentId: Http4sRoute = Http4sRoute { case req @ GET -> `ukV31Prefix` / "account-access-consents" / consentId => // Not withUser -- see the DELETE twin above. EndpointHelpers.executeAndRespond(req) { cc => @@ -321,9 +321,11 @@ object Http4sUKOBv310AccountAccess extends MdcLoggable { http4sPartialFunction = Some(getAccountAccessConsentsConsentId) ) - val routes: HttpRoutes[IO] = Kleisli[HttpF, Request[IO], Response[IO]] { req => - createAccountAccessConsents(req) - .orElse(deleteAccountAccessConsentsConsentId(req)) - .orElse(getAccountAccessConsentsConsentId(req)) - } + lazy val routesInOrder: List[Http4sHandler] = List( + createAccountAccessConsents, + deleteAccountAccessConsentsConsentId, + getAccountAccessConsentsConsentId + ) + + lazy val routes: HttpRoutes[IO] = Http4sRoute.chain(routesInOrder) } diff --git a/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310Accounts.scala b/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310Accounts.scala index 2778c285d0..f3f4a0170f 100644 --- a/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310Accounts.scala +++ b/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310Accounts.scala @@ -31,7 +31,7 @@ import org.json4s._ import cats.data.{Kleisli, OptionT} import cats.effect.IO import code.api.Constant -import code.api.util.APIUtil.{EmptyBody, ResourceDoc, passesPsd2Aisp} +import code.api.util.APIUtil.{Http4sHandler, Http4sRoute, EmptyBody, ResourceDoc, passesPsd2Aisp} import code.api.util.ApiTag import code.api.util.CustomJsonFormats import code.api.util.ErrorMessages.{AuthenticatedUserIsRequired, UnknownError, attemptedToOpenAnEmptyBox} @@ -65,7 +65,7 @@ object Http4sUKOBv310Accounts extends MdcLoggable { private def parseBody(s: String): code.api.berlin.group.v1_3.JvalueCaseClass = code.api.berlin.group.v1_3.JvalueCaseClass(com.openbankproject.commons.util.JsonAliases.parse(s).asInstanceOf[org.json4s.JObject]) val ukV31Prefix = Root / ApiVersion.ukOpenBankingV31.urlPrefix / ApiVersion.ukOpenBankingV31.apiShortVersion - lazy val getAccounts: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getAccounts: Http4sRoute = Http4sRoute { case req @ GET -> `ukV31Prefix` / "accounts" => EndpointHelpers.withUser(req) { (u, cc) => val detailViewId = ViewId(Constant.SYSTEM_READ_ACCOUNTS_DETAIL_VIEW_ID) @@ -127,7 +127,7 @@ object Http4sUKOBv310Accounts extends MdcLoggable { http4sPartialFunction = Some(getAccounts) ) - lazy val getAccountsAccountId: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getAccountsAccountId: Http4sRoute = Http4sRoute { case req @ GET -> `ukV31Prefix` / "accounts" / accountIdStr => EndpointHelpers.withUser(req) { (u, cc) => val accountId = AccountId(accountIdStr) @@ -233,8 +233,10 @@ object Http4sUKOBv310Accounts extends MdcLoggable { http4sPartialFunction = Some(getAccountsAccountId) ) - val routes: HttpRoutes[IO] = Kleisli[HttpF, Request[IO], Response[IO]] { req => - getAccounts(req) - .orElse(getAccountsAccountId(req)) - } + lazy val routesInOrder: List[Http4sHandler] = List( + getAccounts, + getAccountsAccountId + ) + + lazy val routes: HttpRoutes[IO] = Http4sRoute.chain(routesInOrder) } diff --git a/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310Balances.scala b/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310Balances.scala index df800db283..261819e775 100644 --- a/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310Balances.scala +++ b/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310Balances.scala @@ -31,7 +31,7 @@ import org.json4s._ import cats.data.{Kleisli, OptionT} import cats.effect.IO import code.api.Constant -import code.api.util.APIUtil.{EmptyBody, ResourceDoc, passesPsd2Aisp} +import code.api.util.APIUtil.{Http4sHandler, Http4sRoute, EmptyBody, ResourceDoc, passesPsd2Aisp} import code.api.util.ApiTag import code.api.util.CustomJsonFormats import code.api.util.ErrorMessages.{AuthenticatedUserIsRequired, UnknownError} @@ -132,7 +132,7 @@ object Http4sUKOBv310Balances extends MdcLoggable { } }""") - lazy val getAccountsAccountIdBalances: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getAccountsAccountIdBalances: Http4sRoute = Http4sRoute { case req @ GET -> `ukV31Prefix` / "accounts" / accountIdStr / "balances" => EndpointHelpers.withUser(req) { (u, cc) => val accountId = AccountId(accountIdStr) @@ -160,7 +160,7 @@ object Http4sUKOBv310Balances extends MdcLoggable { http4sPartialFunction = Some(getAccountsAccountIdBalances) ) - lazy val getBalances: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getBalances: Http4sRoute = Http4sRoute { case req @ GET -> `ukV31Prefix` / "balances" => EndpointHelpers.withUser(req) { (u, cc) => val balancesViewId = ViewId(Constant.SYSTEM_READ_BALANCES_VIEW_ID) @@ -195,8 +195,10 @@ object Http4sUKOBv310Balances extends MdcLoggable { http4sPartialFunction = Some(getBalances) ) - val routes: HttpRoutes[IO] = Kleisli[HttpF, Request[IO], Response[IO]] { req => - getAccountsAccountIdBalances(req) - .orElse(getBalances(req)) - } + lazy val routesInOrder: List[Http4sHandler] = List( + getAccountsAccountIdBalances, + getBalances + ) + + lazy val routes: HttpRoutes[IO] = Http4sRoute.chain(routesInOrder) } diff --git a/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310Beneficiaries.scala b/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310Beneficiaries.scala index b9e3e2363d..245fa08879 100644 --- a/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310Beneficiaries.scala +++ b/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310Beneficiaries.scala @@ -30,7 +30,7 @@ package code.api.UKOpenBanking.v3_1_0 import org.json4s._ import cats.data.{Kleisli, OptionT} import cats.effect.IO -import code.api.util.APIUtil.{EmptyBody, ResourceDoc, mockedDataText} +import code.api.util.APIUtil.{Http4sHandler, Http4sRoute, EmptyBody, ResourceDoc, mockedDataText} import code.api.util.ApiTag import code.api.util.ApiTag._ import code.api.util.CustomJsonFormats @@ -64,7 +64,7 @@ object Http4sUKOBv310Beneficiaries extends MdcLoggable { val ukV31Prefix = Root / ApiVersion.ukOpenBankingV31.urlPrefix / ApiVersion.ukOpenBankingV31.apiShortVersion private val tag = ApiTag("Beneficiaries") :: apiTagMockedData :: Nil - lazy val getAccountsAccountIdBeneficiaries: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getAccountsAccountIdBeneficiaries: Http4sRoute = Http4sRoute { case req @ GET -> `ukV31Prefix` / "accounts" / _ / "beneficiaries" => EndpointHelpers.withUser(req) { (_, _) => Future.successful(ErrorMessages.NotImplemented) } } @@ -151,7 +151,7 @@ object Http4sUKOBv310Beneficiaries extends MdcLoggable { http4sPartialFunction = Some(getAccountsAccountIdBeneficiaries) ) - lazy val getBeneficiaries: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getBeneficiaries: Http4sRoute = Http4sRoute { case req @ GET -> `ukV31Prefix` / "beneficiaries" => EndpointHelpers.withUser(req) { (_, _) => Future.successful(ErrorMessages.NotImplemented) } } @@ -238,7 +238,10 @@ object Http4sUKOBv310Beneficiaries extends MdcLoggable { http4sPartialFunction = Some(getBeneficiaries) ) - val routes: HttpRoutes[IO] = Kleisli[HttpF, Request[IO], Response[IO]] { req => - getAccountsAccountIdBeneficiaries(req).orElse(getBeneficiaries(req)) - } + lazy val routesInOrder: List[Http4sHandler] = List( + getAccountsAccountIdBeneficiaries, + getBeneficiaries + ) + + lazy val routes: HttpRoutes[IO] = Http4sRoute.chain(routesInOrder) } diff --git a/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310DirectDebits.scala b/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310DirectDebits.scala index 19b5cc8f70..41d0e810c2 100644 --- a/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310DirectDebits.scala +++ b/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310DirectDebits.scala @@ -30,7 +30,7 @@ package code.api.UKOpenBanking.v3_1_0 import org.json4s._ import cats.data.{Kleisli, OptionT} import cats.effect.IO -import code.api.util.APIUtil.{EmptyBody, ResourceDoc, mockedDataText} +import code.api.util.APIUtil.{Http4sHandler, Http4sRoute, EmptyBody, ResourceDoc, mockedDataText} import code.api.util.ApiTag import code.api.util.ApiTag._ import code.api.util.CustomJsonFormats @@ -59,7 +59,7 @@ object Http4sUKOBv310DirectDebits extends MdcLoggable { val ukV31Prefix = Root / ApiVersion.ukOpenBankingV31.urlPrefix / ApiVersion.ukOpenBankingV31.apiShortVersion private val tag = ApiTag("Direct Debits") :: apiTagMockedData :: Nil - lazy val getAccountsAccountIdDirectDebits: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getAccountsAccountIdDirectDebits: Http4sRoute = Http4sRoute { case req @ GET -> `ukV31Prefix` / "accounts" / _ / "direct-debits" => EndpointHelpers.withUser(req) { (_, _) => Future.successful(ErrorMessages.NotImplemented) } } @@ -114,7 +114,7 @@ object Http4sUKOBv310DirectDebits extends MdcLoggable { http4sPartialFunction = Some(getAccountsAccountIdDirectDebits) ) - lazy val getDirectDebits: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getDirectDebits: Http4sRoute = Http4sRoute { case req @ GET -> `ukV31Prefix` / "direct-debits" => EndpointHelpers.withUser(req) { (_, _) => Future.successful(ErrorMessages.NotImplemented) } } @@ -169,7 +169,10 @@ object Http4sUKOBv310DirectDebits extends MdcLoggable { http4sPartialFunction = Some(getDirectDebits) ) - val routes: HttpRoutes[IO] = Kleisli[HttpF, Request[IO], Response[IO]] { req => - getAccountsAccountIdDirectDebits(req).orElse(getDirectDebits(req)) - } + lazy val routesInOrder: List[Http4sHandler] = List( + getAccountsAccountIdDirectDebits, + getDirectDebits + ) + + lazy val routes: HttpRoutes[IO] = Http4sRoute.chain(routesInOrder) } diff --git a/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310DomesticPayments.scala b/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310DomesticPayments.scala index a7c2891e22..22e3c89ced 100644 --- a/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310DomesticPayments.scala +++ b/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310DomesticPayments.scala @@ -30,7 +30,7 @@ package code.api.UKOpenBanking.v3_1_0 import org.json4s._ import cats.data.{Kleisli, OptionT} import cats.effect.IO -import code.api.util.APIUtil.{EmptyBody, ResourceDoc, mockedDataText} +import code.api.util.APIUtil.{Http4sHandler, Http4sRoute, EmptyBody, ResourceDoc, mockedDataText} import code.api.util.ApiTag import code.api.util.ApiTag._ import code.api.util.CustomJsonFormats @@ -59,7 +59,7 @@ object Http4sUKOBv310DomesticPayments extends MdcLoggable { val ukV31Prefix = Root / ApiVersion.ukOpenBankingV31.urlPrefix / ApiVersion.ukOpenBankingV31.apiShortVersion private val tag = ApiTag("Domestic Payments") :: apiTagMockedData :: Nil - lazy val createDomesticPaymentConsents: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val createDomesticPaymentConsents: Http4sRoute = Http4sRoute { case req @ POST -> `ukV31Prefix` / "domestic-payment-consents" => EndpointHelpers.withUser(req) { (_, _) => Future.successful(ErrorMessages.NotImplemented) } } @@ -168,7 +168,7 @@ object Http4sUKOBv310DomesticPayments extends MdcLoggable { http4sPartialFunction = Some(createDomesticPaymentConsents) ) - lazy val createDomesticPayments: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val createDomesticPayments: Http4sRoute = Http4sRoute { case req @ POST -> `ukV31Prefix` / "domestic-payments" => EndpointHelpers.withUser(req) { (_, _) => Future.successful(ErrorMessages.NotImplemented) } } @@ -267,7 +267,7 @@ object Http4sUKOBv310DomesticPayments extends MdcLoggable { ) // Deeper path must come before single-wildcard path in `routes` - lazy val getDomesticPaymentConsentsConsentIdFundsConfirmation: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getDomesticPaymentConsentsConsentIdFundsConfirmation: Http4sRoute = Http4sRoute { case req @ GET -> `ukV31Prefix` / "domestic-payment-consents" / _ / "funds-confirmation" => EndpointHelpers.withUser(req) { (_, _) => Future.successful(ErrorMessages.NotImplemented) } } @@ -304,7 +304,7 @@ object Http4sUKOBv310DomesticPayments extends MdcLoggable { http4sPartialFunction = Some(getDomesticPaymentConsentsConsentIdFundsConfirmation) ) - lazy val getDomesticPaymentConsentsConsentId: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getDomesticPaymentConsentsConsentId: Http4sRoute = Http4sRoute { case req @ GET -> `ukV31Prefix` / "domestic-payment-consents" / _ => EndpointHelpers.withUser(req) { (_, _) => Future.successful(ErrorMessages.NotImplemented) } } @@ -413,7 +413,7 @@ object Http4sUKOBv310DomesticPayments extends MdcLoggable { http4sPartialFunction = Some(getDomesticPaymentConsentsConsentId) ) - lazy val getDomesticPaymentsDomesticPaymentId: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getDomesticPaymentsDomesticPaymentId: Http4sRoute = Http4sRoute { case req @ GET -> `ukV31Prefix` / "domestic-payments" / _ => EndpointHelpers.withUser(req) { (_, _) => Future.successful(ErrorMessages.NotImplemented) } } @@ -511,11 +511,13 @@ object Http4sUKOBv310DomesticPayments extends MdcLoggable { http4sPartialFunction = Some(getDomesticPaymentsDomesticPaymentId) ) - val routes: HttpRoutes[IO] = Kleisli[HttpF, Request[IO], Response[IO]] { req => - createDomesticPaymentConsents(req) - .orElse(createDomesticPayments(req)) - .orElse(getDomesticPaymentConsentsConsentIdFundsConfirmation(req)) - .orElse(getDomesticPaymentConsentsConsentId(req)) - .orElse(getDomesticPaymentsDomesticPaymentId(req)) - } + lazy val routesInOrder: List[Http4sHandler] = List( + createDomesticPaymentConsents, + createDomesticPayments, + getDomesticPaymentConsentsConsentIdFundsConfirmation, + getDomesticPaymentConsentsConsentId, + getDomesticPaymentsDomesticPaymentId + ) + + lazy val routes: HttpRoutes[IO] = Http4sRoute.chain(routesInOrder) } diff --git a/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310DomesticScheduledPayments.scala b/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310DomesticScheduledPayments.scala index f75670dd3d..db318b70d7 100644 --- a/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310DomesticScheduledPayments.scala +++ b/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310DomesticScheduledPayments.scala @@ -30,7 +30,7 @@ package code.api.UKOpenBanking.v3_1_0 import org.json4s._ import cats.data.{Kleisli, OptionT} import cats.effect.IO -import code.api.util.APIUtil.{EmptyBody, ResourceDoc, mockedDataText} +import code.api.util.APIUtil.{Http4sHandler, Http4sRoute, EmptyBody, ResourceDoc, mockedDataText} import code.api.util.ApiTag import code.api.util.ApiTag._ import code.api.util.CustomJsonFormats @@ -59,7 +59,7 @@ object Http4sUKOBv310DomesticScheduledPayments extends MdcLoggable { val ukV31Prefix = Root / ApiVersion.ukOpenBankingV31.urlPrefix / ApiVersion.ukOpenBankingV31.apiShortVersion private val tag = ApiTag("Domestic Scheduled Payments") :: apiTagMockedData :: Nil - lazy val createDomesticScheduledPaymentConsents: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val createDomesticScheduledPaymentConsents: Http4sRoute = Http4sRoute { case req @ POST -> `ukV31Prefix` / "domestic-scheduled-payment-consents" => EndpointHelpers.withUser(req) { (_, _) => Future.successful(ErrorMessages.NotImplemented) } } @@ -170,7 +170,7 @@ object Http4sUKOBv310DomesticScheduledPayments extends MdcLoggable { http4sPartialFunction = Some(createDomesticScheduledPaymentConsents) ) - lazy val createDomesticScheduledPayments: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val createDomesticScheduledPayments: Http4sRoute = Http4sRoute { case req @ POST -> `ukV31Prefix` / "domestic-scheduled-payments" => EndpointHelpers.withUser(req) { (_, _) => Future.successful(ErrorMessages.NotImplemented) } } @@ -269,7 +269,7 @@ object Http4sUKOBv310DomesticScheduledPayments extends MdcLoggable { http4sPartialFunction = Some(createDomesticScheduledPayments) ) - lazy val getDomesticScheduledPaymentConsentsConsentId: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getDomesticScheduledPaymentConsentsConsentId: Http4sRoute = Http4sRoute { case req @ GET -> `ukV31Prefix` / "domestic-scheduled-payment-consents" / _ => EndpointHelpers.withUser(req) { (_, _) => Future.successful(ErrorMessages.NotImplemented) } } @@ -380,7 +380,7 @@ object Http4sUKOBv310DomesticScheduledPayments extends MdcLoggable { http4sPartialFunction = Some(getDomesticScheduledPaymentConsentsConsentId) ) - lazy val getDomesticScheduledPaymentsDomesticScheduledPaymentId: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getDomesticScheduledPaymentsDomesticScheduledPaymentId: Http4sRoute = Http4sRoute { case req @ GET -> `ukV31Prefix` / "domestic-scheduled-payments" / _ => EndpointHelpers.withUser(req) { (_, _) => Future.successful(ErrorMessages.NotImplemented) } } @@ -479,10 +479,12 @@ object Http4sUKOBv310DomesticScheduledPayments extends MdcLoggable { http4sPartialFunction = Some(getDomesticScheduledPaymentsDomesticScheduledPaymentId) ) - val routes: HttpRoutes[IO] = Kleisli[HttpF, Request[IO], Response[IO]] { req => - createDomesticScheduledPaymentConsents(req) - .orElse(createDomesticScheduledPayments(req)) - .orElse(getDomesticScheduledPaymentConsentsConsentId(req)) - .orElse(getDomesticScheduledPaymentsDomesticScheduledPaymentId(req)) - } + lazy val routesInOrder: List[Http4sHandler] = List( + createDomesticScheduledPaymentConsents, + createDomesticScheduledPayments, + getDomesticScheduledPaymentConsentsConsentId, + getDomesticScheduledPaymentsDomesticScheduledPaymentId + ) + + lazy val routes: HttpRoutes[IO] = Http4sRoute.chain(routesInOrder) } diff --git a/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310DomesticStandingOrders.scala b/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310DomesticStandingOrders.scala index a9769b2735..fe2702560c 100644 --- a/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310DomesticStandingOrders.scala +++ b/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310DomesticStandingOrders.scala @@ -30,7 +30,7 @@ package code.api.UKOpenBanking.v3_1_0 import org.json4s._ import cats.data.{Kleisli, OptionT} import cats.effect.IO -import code.api.util.APIUtil.{EmptyBody, ResourceDoc, mockedDataText} +import code.api.util.APIUtil.{Http4sHandler, Http4sRoute, EmptyBody, ResourceDoc, mockedDataText} import code.api.util.ApiTag import code.api.util.ApiTag._ import code.api.util.CustomJsonFormats @@ -59,7 +59,7 @@ object Http4sUKOBv310DomesticStandingOrders extends MdcLoggable { val ukV31Prefix = Root / ApiVersion.ukOpenBankingV31.urlPrefix / ApiVersion.ukOpenBankingV31.apiShortVersion private val tag = ApiTag("Domestic Standing Orders") :: apiTagMockedData :: Nil - lazy val createDomesticStandingOrderConsents: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val createDomesticStandingOrderConsents: Http4sRoute = Http4sRoute { case req @ POST -> `ukV31Prefix` / "domestic-standing-order-consents" => EndpointHelpers.withUser(req) { (_, _) => Future.successful(ErrorMessages.NotImplemented) } } @@ -161,7 +161,7 @@ object Http4sUKOBv310DomesticStandingOrders extends MdcLoggable { http4sPartialFunction = Some(createDomesticStandingOrderConsents) ) - lazy val createDomesticStandingOrders: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val createDomesticStandingOrders: Http4sRoute = Http4sRoute { case req @ POST -> `ukV31Prefix` / "domestic-standing-orders" => EndpointHelpers.withUser(req) { (_, _) => Future.successful(ErrorMessages.NotImplemented) } } @@ -251,7 +251,7 @@ object Http4sUKOBv310DomesticStandingOrders extends MdcLoggable { http4sPartialFunction = Some(createDomesticStandingOrders) ) - lazy val getDomesticStandingOrderConsentsConsentId: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getDomesticStandingOrderConsentsConsentId: Http4sRoute = Http4sRoute { case req @ GET -> `ukV31Prefix` / "domestic-standing-order-consents" / _ => EndpointHelpers.withUser(req) { (_, _) => Future.successful(ErrorMessages.NotImplemented) } } @@ -353,7 +353,7 @@ object Http4sUKOBv310DomesticStandingOrders extends MdcLoggable { http4sPartialFunction = Some(getDomesticStandingOrderConsentsConsentId) ) - lazy val getDomesticStandingOrdersDomesticStandingOrderId: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getDomesticStandingOrdersDomesticStandingOrderId: Http4sRoute = Http4sRoute { case req @ GET -> `ukV31Prefix` / "domestic-standing-orders" / _ => EndpointHelpers.withUser(req) { (_, _) => Future.successful(ErrorMessages.NotImplemented) } } @@ -443,10 +443,12 @@ object Http4sUKOBv310DomesticStandingOrders extends MdcLoggable { http4sPartialFunction = Some(getDomesticStandingOrdersDomesticStandingOrderId) ) - val routes: HttpRoutes[IO] = Kleisli[HttpF, Request[IO], Response[IO]] { req => - createDomesticStandingOrderConsents(req) - .orElse(createDomesticStandingOrders(req)) - .orElse(getDomesticStandingOrderConsentsConsentId(req)) - .orElse(getDomesticStandingOrdersDomesticStandingOrderId(req)) - } + lazy val routesInOrder: List[Http4sHandler] = List( + createDomesticStandingOrderConsents, + createDomesticStandingOrders, + getDomesticStandingOrderConsentsConsentId, + getDomesticStandingOrdersDomesticStandingOrderId + ) + + lazy val routes: HttpRoutes[IO] = Http4sRoute.chain(routesInOrder) } diff --git a/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310FilePayments.scala b/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310FilePayments.scala index 287d9ce883..1a373f8ba2 100644 --- a/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310FilePayments.scala +++ b/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310FilePayments.scala @@ -30,7 +30,7 @@ package code.api.UKOpenBanking.v3_1_0 import org.json4s._ import cats.data.{Kleisli, OptionT} import cats.effect.IO -import code.api.util.APIUtil.{EmptyBody, ResourceDoc, mockedDataText} +import code.api.util.APIUtil.{Http4sHandler, Http4sRoute, EmptyBody, ResourceDoc, mockedDataText} import code.api.util.ApiTag import code.api.util.ApiTag._ import code.api.util.CustomJsonFormats @@ -60,7 +60,7 @@ object Http4sUKOBv310FilePayments extends MdcLoggable { private val tag = ApiTag("File Payments") :: apiTagMockedData :: Nil // Deeper path (/{id}/file) must come before single-wildcard (/{id}) in `routes` - lazy val createFilePaymentConsentsConsentIdFile: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val createFilePaymentConsentsConsentIdFile: Http4sRoute = Http4sRoute { case req @ POST -> `ukV31Prefix` / "file-payment-consents" / _ / "file" => EndpointHelpers.withUser(req) { (_, _) => Future.successful(ErrorMessages.NotImplemented) } } @@ -78,7 +78,7 @@ object Http4sUKOBv310FilePayments extends MdcLoggable { http4sPartialFunction = Some(createFilePaymentConsentsConsentIdFile) ) - lazy val createFilePaymentConsents: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val createFilePaymentConsents: Http4sRoute = Http4sRoute { case req @ POST -> `ukV31Prefix` / "file-payment-consents" => EndpointHelpers.withUser(req) { (_, _) => Future.successful(ErrorMessages.NotImplemented) } } @@ -154,7 +154,7 @@ object Http4sUKOBv310FilePayments extends MdcLoggable { http4sPartialFunction = Some(createFilePaymentConsents) ) - lazy val createFilePayments: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val createFilePayments: Http4sRoute = Http4sRoute { case req @ POST -> `ukV31Prefix` / "file-payments" => EndpointHelpers.withUser(req) { (_, _) => Future.successful(ErrorMessages.NotImplemented) } } @@ -233,7 +233,7 @@ object Http4sUKOBv310FilePayments extends MdcLoggable { http4sPartialFunction = Some(createFilePayments) ) - lazy val getFilePaymentConsentsConsentIdFile: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getFilePaymentConsentsConsentIdFile: Http4sRoute = Http4sRoute { case req @ GET -> `ukV31Prefix` / "file-payment-consents" / _ / "file" => EndpointHelpers.withUser(req) { (_, _) => Future.successful(ErrorMessages.NotImplemented) } } @@ -251,7 +251,7 @@ object Http4sUKOBv310FilePayments extends MdcLoggable { http4sPartialFunction = Some(getFilePaymentConsentsConsentIdFile) ) - lazy val getFilePaymentConsentsConsentId: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getFilePaymentConsentsConsentId: Http4sRoute = Http4sRoute { case req @ GET -> `ukV31Prefix` / "file-payment-consents" / _ => EndpointHelpers.withUser(req) { (_, _) => Future.successful(ErrorMessages.NotImplemented) } } @@ -327,7 +327,7 @@ object Http4sUKOBv310FilePayments extends MdcLoggable { http4sPartialFunction = Some(getFilePaymentConsentsConsentId) ) - lazy val getFilePaymentsFilePaymentIdReportFile: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getFilePaymentsFilePaymentIdReportFile: Http4sRoute = Http4sRoute { case req @ GET -> `ukV31Prefix` / "file-payments" / _ / "report-file" => EndpointHelpers.withUser(req) { (_, _) => Future.successful(ErrorMessages.NotImplemented) } } @@ -345,7 +345,7 @@ object Http4sUKOBv310FilePayments extends MdcLoggable { http4sPartialFunction = Some(getFilePaymentsFilePaymentIdReportFile) ) - lazy val getFilePaymentsFilePaymentId: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getFilePaymentsFilePaymentId: Http4sRoute = Http4sRoute { case req @ GET -> `ukV31Prefix` / "file-payments" / _ => EndpointHelpers.withUser(req) { (_, _) => Future.successful(ErrorMessages.NotImplemented) } } @@ -425,13 +425,15 @@ object Http4sUKOBv310FilePayments extends MdcLoggable { ) // Routes ordered deep-first: /{id}/file and /{id}/report-file before /{id} - val routes: HttpRoutes[IO] = Kleisli[HttpF, Request[IO], Response[IO]] { req => - createFilePaymentConsentsConsentIdFile(req) - .orElse(createFilePaymentConsents(req)) - .orElse(createFilePayments(req)) - .orElse(getFilePaymentConsentsConsentIdFile(req)) - .orElse(getFilePaymentConsentsConsentId(req)) - .orElse(getFilePaymentsFilePaymentIdReportFile(req)) - .orElse(getFilePaymentsFilePaymentId(req)) - } + lazy val routesInOrder: List[Http4sHandler] = List( + createFilePaymentConsentsConsentIdFile, + createFilePaymentConsents, + createFilePayments, + getFilePaymentConsentsConsentIdFile, + getFilePaymentConsentsConsentId, + getFilePaymentsFilePaymentIdReportFile, + getFilePaymentsFilePaymentId + ) + + lazy val routes: HttpRoutes[IO] = Http4sRoute.chain(routesInOrder) } diff --git a/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310FundsConfirmations.scala b/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310FundsConfirmations.scala index 0292a41309..59fbe02700 100644 --- a/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310FundsConfirmations.scala +++ b/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310FundsConfirmations.scala @@ -30,7 +30,7 @@ package code.api.UKOpenBanking.v3_1_0 import org.json4s._ import cats.data.{Kleisli, OptionT} import cats.effect.IO -import code.api.util.APIUtil.{EmptyBody, ResourceDoc, mockedDataText} +import code.api.util.APIUtil.{Http4sHandler, Http4sRoute, EmptyBody, ResourceDoc, mockedDataText} import code.api.util.ApiTag import code.api.util.ApiTag._ import code.api.util.CustomJsonFormats @@ -63,7 +63,7 @@ object Http4sUKOBv310FundsConfirmations extends MdcLoggable { val ukV31Prefix = Root / ApiVersion.ukOpenBankingV31.urlPrefix / ApiVersion.ukOpenBankingV31.apiShortVersion private val tag = ApiTag("Funds Confirmations") :: apiTagMockedData :: Nil - lazy val createFundsConfirmationConsents: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val createFundsConfirmationConsents: Http4sRoute = Http4sRoute { case req @ POST -> `ukV31Prefix` / "funds-confirmation-consents" => EndpointHelpers.withUser(req) { (_, _) => Future.successful(ErrorMessages.NotImplemented) } } @@ -106,7 +106,7 @@ object Http4sUKOBv310FundsConfirmations extends MdcLoggable { http4sPartialFunction = Some(createFundsConfirmationConsents) ) - lazy val createFundsConfirmations: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val createFundsConfirmations: Http4sRoute = Http4sRoute { case req @ POST -> `ukV31Prefix` / "funds-confirmations" => EndpointHelpers.withUser(req) { (_, _) => Future.successful(ErrorMessages.NotImplemented) } } @@ -148,7 +148,7 @@ object Http4sUKOBv310FundsConfirmations extends MdcLoggable { ) // DELETE returns 204 — matching Lift's HttpCode.`204` response - lazy val deleteFundsConfirmationConsentsConsentId: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val deleteFundsConfirmationConsentsConsentId: Http4sRoute = Http4sRoute { case req @ DELETE -> `ukV31Prefix` / "funds-confirmation-consents" / _ => EndpointHelpers.withUserDelete(req) { (_, _) => Future.successful(()) } } @@ -166,7 +166,7 @@ object Http4sUKOBv310FundsConfirmations extends MdcLoggable { http4sPartialFunction = Some(deleteFundsConfirmationConsentsConsentId) ) - lazy val getFundsConfirmationConsentsConsentId: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getFundsConfirmationConsentsConsentId: Http4sRoute = Http4sRoute { case req @ GET -> `ukV31Prefix` / "funds-confirmation-consents" / _ => EndpointHelpers.withUser(req) { (_, _) => Future.successful(ErrorMessages.NotImplemented) } } @@ -209,10 +209,12 @@ object Http4sUKOBv310FundsConfirmations extends MdcLoggable { http4sPartialFunction = Some(getFundsConfirmationConsentsConsentId) ) - val routes: HttpRoutes[IO] = Kleisli[HttpF, Request[IO], Response[IO]] { req => - createFundsConfirmationConsents(req) - .orElse(createFundsConfirmations(req)) - .orElse(deleteFundsConfirmationConsentsConsentId(req)) - .orElse(getFundsConfirmationConsentsConsentId(req)) - } + lazy val routesInOrder: List[Http4sHandler] = List( + createFundsConfirmationConsents, + createFundsConfirmations, + deleteFundsConfirmationConsentsConsentId, + getFundsConfirmationConsentsConsentId + ) + + lazy val routes: HttpRoutes[IO] = Http4sRoute.chain(routesInOrder) } diff --git a/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310InternationalPayments.scala b/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310InternationalPayments.scala index 476631c8f7..1d1006ee95 100644 --- a/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310InternationalPayments.scala +++ b/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310InternationalPayments.scala @@ -30,7 +30,7 @@ package code.api.UKOpenBanking.v3_1_0 import org.json4s._ import cats.data.{Kleisli, OptionT} import cats.effect.IO -import code.api.util.APIUtil.{EmptyBody, ResourceDoc, mockedDataText} +import code.api.util.APIUtil.{Http4sHandler, Http4sRoute, EmptyBody, ResourceDoc, mockedDataText} import code.api.util.ApiTag import code.api.util.ApiTag._ import code.api.util.CustomJsonFormats @@ -59,7 +59,7 @@ object Http4sUKOBv310InternationalPayments extends MdcLoggable { val ukV31Prefix = Root / ApiVersion.ukOpenBankingV31.urlPrefix / ApiVersion.ukOpenBankingV31.apiShortVersion private val tag = ApiTag("International Payments") :: apiTagMockedData :: Nil - lazy val createInternationalPaymentConsents: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val createInternationalPaymentConsents: Http4sRoute = Http4sRoute { case req @ POST -> `ukV31Prefix` / "international-payment-consents" => EndpointHelpers.withUser(req) { (_, _) => Future.successful(ErrorMessages.NotImplemented) } } @@ -204,7 +204,7 @@ object Http4sUKOBv310InternationalPayments extends MdcLoggable { http4sPartialFunction = Some(createInternationalPaymentConsents) ) - lazy val createInternationalPayments: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val createInternationalPayments: Http4sRoute = Http4sRoute { case req @ POST -> `ukV31Prefix` / "international-payments" => EndpointHelpers.withUser(req) { (_, _) => Future.successful(ErrorMessages.NotImplemented) } } @@ -339,7 +339,7 @@ object Http4sUKOBv310InternationalPayments extends MdcLoggable { ) // Deeper path must come before single-wildcard path in `routes` - lazy val getInternationalPaymentConsentsConsentIdFundsConfirmation: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getInternationalPaymentConsentsConsentIdFundsConfirmation: Http4sRoute = Http4sRoute { case req @ GET -> `ukV31Prefix` / "international-payment-consents" / _ / "funds-confirmation" => EndpointHelpers.withUser(req) { (_, _) => Future.successful(ErrorMessages.NotImplemented) } } @@ -376,7 +376,7 @@ object Http4sUKOBv310InternationalPayments extends MdcLoggable { http4sPartialFunction = Some(getInternationalPaymentConsentsConsentIdFundsConfirmation) ) - lazy val getInternationalPaymentConsentsConsentId: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getInternationalPaymentConsentsConsentId: Http4sRoute = Http4sRoute { case req @ GET -> `ukV31Prefix` / "international-payment-consents" / _ => EndpointHelpers.withUser(req) { (_, _) => Future.successful(ErrorMessages.NotImplemented) } } @@ -521,7 +521,7 @@ object Http4sUKOBv310InternationalPayments extends MdcLoggable { http4sPartialFunction = Some(getInternationalPaymentConsentsConsentId) ) - lazy val getInternationalPaymentsInternationalPaymentId: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getInternationalPaymentsInternationalPaymentId: Http4sRoute = Http4sRoute { case req @ GET -> `ukV31Prefix` / "international-payments" / _ => EndpointHelpers.withUser(req) { (_, _) => Future.successful(ErrorMessages.NotImplemented) } } @@ -655,11 +655,13 @@ object Http4sUKOBv310InternationalPayments extends MdcLoggable { http4sPartialFunction = Some(getInternationalPaymentsInternationalPaymentId) ) - val routes: HttpRoutes[IO] = Kleisli[HttpF, Request[IO], Response[IO]] { req => - createInternationalPaymentConsents(req) - .orElse(createInternationalPayments(req)) - .orElse(getInternationalPaymentConsentsConsentIdFundsConfirmation(req)) - .orElse(getInternationalPaymentConsentsConsentId(req)) - .orElse(getInternationalPaymentsInternationalPaymentId(req)) - } + lazy val routesInOrder: List[Http4sHandler] = List( + createInternationalPaymentConsents, + createInternationalPayments, + getInternationalPaymentConsentsConsentIdFundsConfirmation, + getInternationalPaymentConsentsConsentId, + getInternationalPaymentsInternationalPaymentId + ) + + lazy val routes: HttpRoutes[IO] = Http4sRoute.chain(routesInOrder) } diff --git a/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310InternationalScheduledPayments.scala b/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310InternationalScheduledPayments.scala index a896c79a63..8facaee8ac 100644 --- a/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310InternationalScheduledPayments.scala +++ b/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310InternationalScheduledPayments.scala @@ -30,7 +30,7 @@ package code.api.UKOpenBanking.v3_1_0 import org.json4s._ import cats.data.{Kleisli, OptionT} import cats.effect.IO -import code.api.util.APIUtil.{EmptyBody, ResourceDoc, mockedDataText} +import code.api.util.APIUtil.{Http4sHandler, Http4sRoute, EmptyBody, ResourceDoc, mockedDataText} import code.api.util.ApiTag import code.api.util.ApiTag._ import code.api.util.CustomJsonFormats @@ -59,7 +59,7 @@ object Http4sUKOBv310InternationalScheduledPayments extends MdcLoggable { val ukV31Prefix = Root / ApiVersion.ukOpenBankingV31.urlPrefix / ApiVersion.ukOpenBankingV31.apiShortVersion private val tag = ApiTag("International Scheduled Payments") :: apiTagMockedData :: Nil - lazy val createInternationalScheduledPaymentConsents: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val createInternationalScheduledPaymentConsents: Http4sRoute = Http4sRoute { case req @ POST -> `ukV31Prefix` / "international-scheduled-payment-consents" => EndpointHelpers.withUser(req) { (_, _) => Future.successful(ErrorMessages.NotImplemented) } } @@ -206,7 +206,7 @@ object Http4sUKOBv310InternationalScheduledPayments extends MdcLoggable { http4sPartialFunction = Some(createInternationalScheduledPaymentConsents) ) - lazy val createInternationalScheduledPayments: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val createInternationalScheduledPayments: Http4sRoute = Http4sRoute { case req @ POST -> `ukV31Prefix` / "international-scheduled-payments" => EndpointHelpers.withUser(req) { (_, _) => Future.successful(ErrorMessages.NotImplemented) } } @@ -342,7 +342,7 @@ object Http4sUKOBv310InternationalScheduledPayments extends MdcLoggable { ) // Deeper path must come before single-wildcard path in `routes` - lazy val getInternationalScheduledPaymentConsentsConsentIdFundsConfirmation: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getInternationalScheduledPaymentConsentsConsentIdFundsConfirmation: Http4sRoute = Http4sRoute { case req @ GET -> `ukV31Prefix` / "international-scheduled-payment-consents" / _ / "funds-confirmation" => EndpointHelpers.withUser(req) { (_, _) => Future.successful(ErrorMessages.NotImplemented) } } @@ -379,7 +379,7 @@ object Http4sUKOBv310InternationalScheduledPayments extends MdcLoggable { http4sPartialFunction = Some(getInternationalScheduledPaymentConsentsConsentIdFundsConfirmation) ) - lazy val getInternationalScheduledPaymentConsentsConsentId: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getInternationalScheduledPaymentConsentsConsentId: Http4sRoute = Http4sRoute { case req @ GET -> `ukV31Prefix` / "international-scheduled-payment-consents" / _ => EndpointHelpers.withUser(req) { (_, _) => Future.successful(ErrorMessages.NotImplemented) } } @@ -526,7 +526,7 @@ object Http4sUKOBv310InternationalScheduledPayments extends MdcLoggable { http4sPartialFunction = Some(getInternationalScheduledPaymentConsentsConsentId) ) - lazy val getInternationalScheduledPaymentsInternationalScheduledPaymentId: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getInternationalScheduledPaymentsInternationalScheduledPaymentId: Http4sRoute = Http4sRoute { case req @ GET -> `ukV31Prefix` / "international-scheduled-payments" / _ => EndpointHelpers.withUser(req) { (_, _) => Future.successful(ErrorMessages.NotImplemented) } } @@ -661,11 +661,13 @@ object Http4sUKOBv310InternationalScheduledPayments extends MdcLoggable { http4sPartialFunction = Some(getInternationalScheduledPaymentsInternationalScheduledPaymentId) ) - val routes: HttpRoutes[IO] = Kleisli[HttpF, Request[IO], Response[IO]] { req => - createInternationalScheduledPaymentConsents(req) - .orElse(createInternationalScheduledPayments(req)) - .orElse(getInternationalScheduledPaymentConsentsConsentIdFundsConfirmation(req)) - .orElse(getInternationalScheduledPaymentConsentsConsentId(req)) - .orElse(getInternationalScheduledPaymentsInternationalScheduledPaymentId(req)) - } + lazy val routesInOrder: List[Http4sHandler] = List( + createInternationalScheduledPaymentConsents, + createInternationalScheduledPayments, + getInternationalScheduledPaymentConsentsConsentIdFundsConfirmation, + getInternationalScheduledPaymentConsentsConsentId, + getInternationalScheduledPaymentsInternationalScheduledPaymentId + ) + + lazy val routes: HttpRoutes[IO] = Http4sRoute.chain(routesInOrder) } diff --git a/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310InternationalStandingOrders.scala b/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310InternationalStandingOrders.scala index 7af316e01f..7bc6adfaa5 100644 --- a/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310InternationalStandingOrders.scala +++ b/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310InternationalStandingOrders.scala @@ -30,7 +30,7 @@ package code.api.UKOpenBanking.v3_1_0 import org.json4s._ import cats.data.{Kleisli, OptionT} import cats.effect.IO -import code.api.util.APIUtil.{EmptyBody, ResourceDoc, mockedDataText} +import code.api.util.APIUtil.{Http4sHandler, Http4sRoute, EmptyBody, ResourceDoc, mockedDataText} import code.api.util.ApiTag import code.api.util.ApiTag._ import code.api.util.CustomJsonFormats @@ -59,7 +59,7 @@ object Http4sUKOBv310InternationalStandingOrders extends MdcLoggable { val ukV31Prefix = Root / ApiVersion.ukOpenBankingV31.urlPrefix / ApiVersion.ukOpenBankingV31.apiShortVersion private val tag = ApiTag("International Standing Orders") :: apiTagMockedData :: Nil - lazy val createInternationalStandingOrderConsents: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val createInternationalStandingOrderConsents: Http4sRoute = Http4sRoute { case req @ POST -> `ukV31Prefix` / "international-standing-order-consents" => EndpointHelpers.withUser(req) { (_, _) => Future.successful(ErrorMessages.NotImplemented) } } @@ -188,7 +188,7 @@ object Http4sUKOBv310InternationalStandingOrders extends MdcLoggable { http4sPartialFunction = Some(createInternationalStandingOrderConsents) ) - lazy val createInternationalStandingOrders: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val createInternationalStandingOrders: Http4sRoute = Http4sRoute { case req @ POST -> `ukV31Prefix` / "international-standing-orders" => EndpointHelpers.withUser(req) { (_, _) => Future.successful(ErrorMessages.NotImplemented) } } @@ -305,7 +305,7 @@ object Http4sUKOBv310InternationalStandingOrders extends MdcLoggable { http4sPartialFunction = Some(createInternationalStandingOrders) ) - lazy val getInternationalStandingOrderConsentsConsentId: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getInternationalStandingOrderConsentsConsentId: Http4sRoute = Http4sRoute { case req @ GET -> `ukV31Prefix` / "international-standing-order-consents" / _ => EndpointHelpers.withUser(req) { (_, _) => Future.successful(ErrorMessages.NotImplemented) } } @@ -434,7 +434,7 @@ object Http4sUKOBv310InternationalStandingOrders extends MdcLoggable { http4sPartialFunction = Some(getInternationalStandingOrderConsentsConsentId) ) - lazy val getInternationalStandingOrdersInternationalStandingOrderPaymentId: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getInternationalStandingOrdersInternationalStandingOrderPaymentId: Http4sRoute = Http4sRoute { case req @ GET -> `ukV31Prefix` / "international-standing-orders" / _ => EndpointHelpers.withUser(req) { (_, _) => Future.successful(ErrorMessages.NotImplemented) } } @@ -551,10 +551,12 @@ object Http4sUKOBv310InternationalStandingOrders extends MdcLoggable { http4sPartialFunction = Some(getInternationalStandingOrdersInternationalStandingOrderPaymentId) ) - val routes: HttpRoutes[IO] = Kleisli[HttpF, Request[IO], Response[IO]] { req => - createInternationalStandingOrderConsents(req) - .orElse(createInternationalStandingOrders(req)) - .orElse(getInternationalStandingOrderConsentsConsentId(req)) - .orElse(getInternationalStandingOrdersInternationalStandingOrderPaymentId(req)) - } + lazy val routesInOrder: List[Http4sHandler] = List( + createInternationalStandingOrderConsents, + createInternationalStandingOrders, + getInternationalStandingOrderConsentsConsentId, + getInternationalStandingOrdersInternationalStandingOrderPaymentId + ) + + lazy val routes: HttpRoutes[IO] = Http4sRoute.chain(routesInOrder) } diff --git a/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310Offers.scala b/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310Offers.scala index a2a0ca8f8d..9c61013513 100644 --- a/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310Offers.scala +++ b/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310Offers.scala @@ -30,7 +30,7 @@ package code.api.UKOpenBanking.v3_1_0 import org.json4s._ import cats.data.{Kleisli, OptionT} import cats.effect.IO -import code.api.util.APIUtil.{EmptyBody, ResourceDoc, mockedDataText} +import code.api.util.APIUtil.{Http4sHandler, Http4sRoute, EmptyBody, ResourceDoc, mockedDataText} import code.api.util.ApiTag import code.api.util.ApiTag._ import code.api.util.CustomJsonFormats @@ -59,7 +59,7 @@ object Http4sUKOBv310Offers extends MdcLoggable { val ukV31Prefix = Root / ApiVersion.ukOpenBankingV31.urlPrefix / ApiVersion.ukOpenBankingV31.apiShortVersion private val tag = ApiTag("Offers") :: apiTagMockedData :: Nil - lazy val getAccountsAccountIdOffers: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getAccountsAccountIdOffers: Http4sRoute = Http4sRoute { case req @ GET -> `ukV31Prefix` / "accounts" / _ / "offers" => EndpointHelpers.withUser(req) { (_, _) => Future.successful(ErrorMessages.NotImplemented) } } @@ -130,7 +130,7 @@ object Http4sUKOBv310Offers extends MdcLoggable { http4sPartialFunction = Some(getAccountsAccountIdOffers) ) - lazy val getOffers: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getOffers: Http4sRoute = Http4sRoute { case req @ GET -> `ukV31Prefix` / "offers" => EndpointHelpers.withUser(req) { (_, _) => Future.successful(ErrorMessages.NotImplemented) } } @@ -201,7 +201,10 @@ object Http4sUKOBv310Offers extends MdcLoggable { http4sPartialFunction = Some(getOffers) ) - val routes: HttpRoutes[IO] = Kleisli[HttpF, Request[IO], Response[IO]] { req => - getAccountsAccountIdOffers(req).orElse(getOffers(req)) - } + lazy val routesInOrder: List[Http4sHandler] = List( + getAccountsAccountIdOffers, + getOffers + ) + + lazy val routes: HttpRoutes[IO] = Http4sRoute.chain(routesInOrder) } diff --git a/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310Partys.scala b/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310Partys.scala index 8073a6c061..8068dbfc3b 100644 --- a/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310Partys.scala +++ b/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310Partys.scala @@ -30,7 +30,7 @@ package code.api.UKOpenBanking.v3_1_0 import org.json4s._ import cats.data.{Kleisli, OptionT} import cats.effect.IO -import code.api.util.APIUtil.{EmptyBody, ResourceDoc, mockedDataText} +import code.api.util.APIUtil.{Http4sHandler, Http4sRoute, EmptyBody, ResourceDoc, mockedDataText} import code.api.util.ApiTag import code.api.util.ApiTag._ import code.api.util.CustomJsonFormats @@ -59,7 +59,7 @@ object Http4sUKOBv310Partys extends MdcLoggable { val ukV31Prefix = Root / ApiVersion.ukOpenBankingV31.urlPrefix / ApiVersion.ukOpenBankingV31.apiShortVersion private val tag = ApiTag("Partys") :: apiTagMockedData :: Nil - lazy val getAccountsAccountIdParty: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getAccountsAccountIdParty: Http4sRoute = Http4sRoute { case req @ GET -> `ukV31Prefix` / "accounts" / _ / "party" => EndpointHelpers.withUser(req) { (_, _) => Future.successful(ErrorMessages.NotImplemented) } } @@ -119,7 +119,7 @@ object Http4sUKOBv310Partys extends MdcLoggable { http4sPartialFunction = Some(getAccountsAccountIdParty) ) - lazy val getParty: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getParty: Http4sRoute = Http4sRoute { case req @ GET -> `ukV31Prefix` / "party" => EndpointHelpers.withUser(req) { (_, _) => Future.successful(ErrorMessages.NotImplemented) } } @@ -179,7 +179,10 @@ object Http4sUKOBv310Partys extends MdcLoggable { http4sPartialFunction = Some(getParty) ) - val routes: HttpRoutes[IO] = Kleisli[HttpF, Request[IO], Response[IO]] { req => - getAccountsAccountIdParty(req).orElse(getParty(req)) - } + lazy val routesInOrder: List[Http4sHandler] = List( + getAccountsAccountIdParty, + getParty + ) + + lazy val routes: HttpRoutes[IO] = Http4sRoute.chain(routesInOrder) } diff --git a/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310Products.scala b/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310Products.scala index edadbef259..e04cce1943 100644 --- a/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310Products.scala +++ b/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310Products.scala @@ -30,7 +30,7 @@ package code.api.UKOpenBanking.v3_1_0 import org.json4s._ import cats.data.{Kleisli, OptionT} import cats.effect.IO -import code.api.util.APIUtil.{EmptyBody, ResourceDoc, mockedDataText} +import code.api.util.APIUtil.{Http4sHandler, Http4sRoute, EmptyBody, ResourceDoc, mockedDataText} import code.api.util.ApiTag import code.api.util.ApiTag._ import code.api.util.CustomJsonFormats @@ -70,7 +70,7 @@ object Http4sUKOBv310Products extends MdcLoggable { private val tag = ApiTag("Products") :: apiTagMockedData :: Nil // ── GET /accounts/ACCOUNTID/product ──────────────────────────────── - lazy val getAccountsAccountIdProduct: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getAccountsAccountIdProduct: Http4sRoute = Http4sRoute { case req @ GET -> `ukV31Prefix` / "accounts" / _ / "product" => EndpointHelpers.withUser(req) { (_, _) => Future.successful(ErrorMessages.NotImplemented) } } @@ -89,7 +89,7 @@ object Http4sUKOBv310Products extends MdcLoggable { ) // ── GET /products ────────────────────────────────────────────────── - lazy val getProducts: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getProducts: Http4sRoute = Http4sRoute { case req @ GET -> `ukV31Prefix` / "products" => EndpointHelpers.withUser(req) { (_, _) => Future.successful(ErrorMessages.NotImplemented) } } @@ -107,8 +107,10 @@ object Http4sUKOBv310Products extends MdcLoggable { http4sPartialFunction = Some(getProducts) ) - val routes: HttpRoutes[IO] = Kleisli[HttpF, Request[IO], Response[IO]] { req => - getAccountsAccountIdProduct(req) - .orElse(getProducts(req)) - } + lazy val routesInOrder: List[Http4sHandler] = List( + getAccountsAccountIdProduct, + getProducts + ) + + lazy val routes: HttpRoutes[IO] = Http4sRoute.chain(routesInOrder) } diff --git a/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310ScheduledPayments.scala b/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310ScheduledPayments.scala index cdbd0014d7..9ea25de7cc 100644 --- a/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310ScheduledPayments.scala +++ b/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310ScheduledPayments.scala @@ -30,7 +30,7 @@ package code.api.UKOpenBanking.v3_1_0 import org.json4s._ import cats.data.{Kleisli, OptionT} import cats.effect.IO -import code.api.util.APIUtil.{EmptyBody, ResourceDoc, mockedDataText} +import code.api.util.APIUtil.{Http4sHandler, Http4sRoute, EmptyBody, ResourceDoc, mockedDataText} import code.api.util.ApiTag import code.api.util.ApiTag._ import code.api.util.CustomJsonFormats @@ -59,7 +59,7 @@ object Http4sUKOBv310ScheduledPayments extends MdcLoggable { val ukV31Prefix = Root / ApiVersion.ukOpenBankingV31.urlPrefix / ApiVersion.ukOpenBankingV31.apiShortVersion private val tag = ApiTag("Scheduled Payments") :: apiTagMockedData :: Nil - lazy val getAccountsAccountIdScheduledPayments: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getAccountsAccountIdScheduledPayments: Http4sRoute = Http4sRoute { case req @ GET -> `ukV31Prefix` / "accounts" / _ / "scheduled-payments" => EndpointHelpers.withUser(req) { (_, _) => Future.successful(ErrorMessages.NotImplemented) } } @@ -132,7 +132,7 @@ object Http4sUKOBv310ScheduledPayments extends MdcLoggable { http4sPartialFunction = Some(getAccountsAccountIdScheduledPayments) ) - lazy val getScheduledPayments: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getScheduledPayments: Http4sRoute = Http4sRoute { case req @ GET -> `ukV31Prefix` / "scheduled-payments" => EndpointHelpers.withUser(req) { (_, _) => Future.successful(ErrorMessages.NotImplemented) } } @@ -205,7 +205,10 @@ object Http4sUKOBv310ScheduledPayments extends MdcLoggable { http4sPartialFunction = Some(getScheduledPayments) ) - val routes: HttpRoutes[IO] = Kleisli[HttpF, Request[IO], Response[IO]] { req => - getAccountsAccountIdScheduledPayments(req).orElse(getScheduledPayments(req)) - } + lazy val routesInOrder: List[Http4sHandler] = List( + getAccountsAccountIdScheduledPayments, + getScheduledPayments + ) + + lazy val routes: HttpRoutes[IO] = Http4sRoute.chain(routesInOrder) } diff --git a/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310StandingOrders.scala b/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310StandingOrders.scala index 000dcb593d..82a47558d9 100644 --- a/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310StandingOrders.scala +++ b/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310StandingOrders.scala @@ -30,7 +30,7 @@ package code.api.UKOpenBanking.v3_1_0 import org.json4s._ import cats.data.{Kleisli, OptionT} import cats.effect.IO -import code.api.util.APIUtil.{EmptyBody, ResourceDoc, mockedDataText} +import code.api.util.APIUtil.{Http4sHandler, Http4sRoute, EmptyBody, ResourceDoc, mockedDataText} import code.api.util.ApiTag import code.api.util.ApiTag._ import code.api.util.CustomJsonFormats @@ -59,7 +59,7 @@ object Http4sUKOBv310StandingOrders extends MdcLoggable { val ukV31Prefix = Root / ApiVersion.ukOpenBankingV31.urlPrefix / ApiVersion.ukOpenBankingV31.apiShortVersion private val tag = ApiTag("Standing Orders") :: apiTagMockedData :: Nil - lazy val getAccountsAccountIdStandingOrders: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getAccountsAccountIdStandingOrders: Http4sRoute = Http4sRoute { case req @ GET -> `ukV31Prefix` / "accounts" / _ / "standing-orders" => EndpointHelpers.withUser(req) { (_, _) => Future.successful(ErrorMessages.NotImplemented) } } @@ -156,7 +156,7 @@ object Http4sUKOBv310StandingOrders extends MdcLoggable { http4sPartialFunction = Some(getAccountsAccountIdStandingOrders) ) - lazy val getStandingOrders: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getStandingOrders: Http4sRoute = Http4sRoute { case req @ GET -> `ukV31Prefix` / "standing-orders" => EndpointHelpers.withUser(req) { (_, _) => Future.successful(ErrorMessages.NotImplemented) } } @@ -253,7 +253,10 @@ object Http4sUKOBv310StandingOrders extends MdcLoggable { http4sPartialFunction = Some(getStandingOrders) ) - val routes: HttpRoutes[IO] = Kleisli[HttpF, Request[IO], Response[IO]] { req => - getAccountsAccountIdStandingOrders(req).orElse(getStandingOrders(req)) - } + lazy val routesInOrder: List[Http4sHandler] = List( + getAccountsAccountIdStandingOrders, + getStandingOrders + ) + + lazy val routes: HttpRoutes[IO] = Http4sRoute.chain(routesInOrder) } diff --git a/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310Statements.scala b/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310Statements.scala index 9e1f3f6d9f..46496472f3 100644 --- a/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310Statements.scala +++ b/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310Statements.scala @@ -30,7 +30,7 @@ package code.api.UKOpenBanking.v3_1_0 import org.json4s._ import cats.data.{Kleisli, OptionT} import cats.effect.IO -import code.api.util.APIUtil.{EmptyBody, ResourceDoc, mockedDataText} +import code.api.util.APIUtil.{Http4sHandler, Http4sRoute, EmptyBody, ResourceDoc, mockedDataText} import code.api.util.ApiTag import code.api.util.ApiTag._ import code.api.util.CustomJsonFormats @@ -63,7 +63,7 @@ object Http4sUKOBv310Statements extends MdcLoggable { val ukV31Prefix = Root / ApiVersion.ukOpenBankingV31.urlPrefix / ApiVersion.ukOpenBankingV31.apiShortVersion private val tag = ApiTag("Statements") :: apiTagMockedData :: Nil - lazy val getAccountsAccountIdStatements: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getAccountsAccountIdStatements: Http4sRoute = Http4sRoute { case req @ GET -> `ukV31Prefix` / "accounts" / _ / "statements" => EndpointHelpers.withUser(req) { (_, _) => Future.successful(ErrorMessages.NotImplemented) } } @@ -273,7 +273,7 @@ object Http4sUKOBv310Statements extends MdcLoggable { ) // Deeper paths must come before the single-segment path in `routes` - lazy val getAccountsAccountIdStatementsStatementIdFile: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getAccountsAccountIdStatementsStatementIdFile: Http4sRoute = Http4sRoute { case req @ GET -> `ukV31Prefix` / "accounts" / _ / "statements" / _ / "file" => EndpointHelpers.withUser(req) { (_, _) => Future.successful(ErrorMessages.NotImplemented) } } @@ -291,7 +291,7 @@ object Http4sUKOBv310Statements extends MdcLoggable { http4sPartialFunction = Some(getAccountsAccountIdStatementsStatementIdFile) ) - lazy val getAccountsAccountIdStatementsStatementIdTransactions: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getAccountsAccountIdStatementsStatementIdTransactions: Http4sRoute = Http4sRoute { case req @ GET -> `ukV31Prefix` / "accounts" / _ / "statements" / _ / "transactions" => EndpointHelpers.withUser(req) { (_, _) => Future.successful(ErrorMessages.NotImplemented) } } @@ -532,7 +532,7 @@ object Http4sUKOBv310Statements extends MdcLoggable { http4sPartialFunction = Some(getAccountsAccountIdStatementsStatementIdTransactions) ) - lazy val getAccountsAccountIdStatementsStatementId: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getAccountsAccountIdStatementsStatementId: Http4sRoute = Http4sRoute { case req @ GET -> `ukV31Prefix` / "accounts" / _ / "statements" / _ => EndpointHelpers.withUser(req) { (_, _) => Future.successful(ErrorMessages.NotImplemented) } } @@ -741,7 +741,7 @@ object Http4sUKOBv310Statements extends MdcLoggable { http4sPartialFunction = Some(getAccountsAccountIdStatementsStatementId) ) - lazy val getStatements: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getStatements: Http4sRoute = Http4sRoute { case req @ GET -> `ukV31Prefix` / "statements" => EndpointHelpers.withUser(req) { (_, _) => Future.successful(ErrorMessages.NotImplemented) } } @@ -951,11 +951,13 @@ object Http4sUKOBv310Statements extends MdcLoggable { ) // Routes ordered deep-first to avoid the single-wildcard pattern swallowing deeper paths - val routes: HttpRoutes[IO] = Kleisli[HttpF, Request[IO], Response[IO]] { req => - getAccountsAccountIdStatementsStatementIdFile(req) - .orElse(getAccountsAccountIdStatementsStatementIdTransactions(req)) - .orElse(getAccountsAccountIdStatementsStatementId(req)) - .orElse(getAccountsAccountIdStatements(req)) - .orElse(getStatements(req)) - } + lazy val routesInOrder: List[Http4sHandler] = List( + getAccountsAccountIdStatementsStatementIdFile, + getAccountsAccountIdStatementsStatementIdTransactions, + getAccountsAccountIdStatementsStatementId, + getAccountsAccountIdStatements, + getStatements + ) + + lazy val routes: HttpRoutes[IO] = Http4sRoute.chain(routesInOrder) } diff --git a/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310Transactions.scala b/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310Transactions.scala index bf78200355..443d0ef839 100644 --- a/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310Transactions.scala +++ b/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310Transactions.scala @@ -34,7 +34,7 @@ import cats.data.{Kleisli, OptionT} import cats.effect.IO import code.api.APIFailureNewStyle import code.api.Constant -import code.api.util.APIUtil.{HTTPParam, EmptyBody, ResourceDoc, createQueriesByHttpParams, defaultBankId, fullBoxOrException, mockedDataText, passesPsd2Aisp, unboxFull} +import code.api.util.APIUtil.{Http4sHandler, Http4sRoute, HTTPParam, EmptyBody, ResourceDoc, createQueriesByHttpParams, defaultBankId, fullBoxOrException, mockedDataText, passesPsd2Aisp, unboxFull} import code.api.util.ApiTag import code.api.util.ApiTag._ import code.api.util.CustomJsonFormats @@ -86,7 +86,7 @@ object Http4sUKOBv310Transactions extends MdcLoggable { // entry appears under the "Transactions" tag (matching the Lift source of truth) // in addition to the "Statements" entry. // ----------------------------------------------------------------------- - lazy val getAccountsAccountIdStatementsStatementIdTransactions: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getAccountsAccountIdStatementsStatementIdTransactions: Http4sRoute = Http4sRoute { case req @ GET -> `ukV31Prefix` / "accounts" / _ / "statements" / _ / "transactions" => EndpointHelpers.withUser(req) { (_, _) => Future.successful(ErrorMessages.NotImplemented) } } @@ -331,7 +331,7 @@ object Http4sUKOBv310Transactions extends MdcLoggable { // ----------------------------------------------------------------------- // getAccountsAccountIdTransactions — real business logic // ----------------------------------------------------------------------- - lazy val getAccountsAccountIdTransactions: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getAccountsAccountIdTransactions: Http4sRoute = Http4sRoute { case req @ GET -> `ukV31Prefix` / "accounts" / accountIdStr / "transactions" => EndpointHelpers.withUser(req) { (u, cc) => val accountId = AccountId(accountIdStr) @@ -583,7 +583,7 @@ object Http4sUKOBv310Transactions extends MdcLoggable { // ----------------------------------------------------------------------- // getTransactions — real business logic (bulk, no consent check) // ----------------------------------------------------------------------- - lazy val getTransactions: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getTransactions: Http4sRoute = Http4sRoute { case req @ GET -> `ukV31Prefix` / "transactions" => EndpointHelpers.withUser(req) { (u, cc) => for { @@ -851,9 +851,11 @@ object Http4sUKOBv310Transactions extends MdcLoggable { ) // Routes ordered deep-first: 5-segment path before 3-segment before 1-segment - val routes: HttpRoutes[IO] = Kleisli[HttpF, Request[IO], Response[IO]] { req => - getAccountsAccountIdStatementsStatementIdTransactions(req) - .orElse(getAccountsAccountIdTransactions(req)) - .orElse(getTransactions(req)) - } + lazy val routesInOrder: List[Http4sHandler] = List( + getAccountsAccountIdStatementsStatementIdTransactions, + getAccountsAccountIdTransactions, + getTransactions + ) + + lazy val routes: HttpRoutes[IO] = Http4sRoute.chain(routesInOrder) } diff --git a/obp-api/src/main/scala/code/api/UKOpenBanking/v4_0_1/Http4sUKOBv401.scala b/obp-api/src/main/scala/code/api/UKOpenBanking/v4_0_1/Http4sUKOBv401.scala index 05d57caf61..dcea4e255b 100644 --- a/obp-api/src/main/scala/code/api/UKOpenBanking/v4_0_1/Http4sUKOBv401.scala +++ b/obp-api/src/main/scala/code/api/UKOpenBanking/v4_0_1/Http4sUKOBv401.scala @@ -30,8 +30,8 @@ package code.api.UKOpenBanking.v4_0_1 import cats.data.{Kleisli, OptionT} import cats.effect._ import code.api.util.APIUtil -import code.api.util.APIUtil.ResourceDoc -import code.api.util.http4s.ResourceDocMiddleware +import code.api.util.APIUtil.{Http4sHandler, Http4sRoute, ResourceDoc} +import code.api.util.http4s.{ResourceDocMatcher, ResourceDocMiddleware} import code.api.util.http4s.IdempotencyMiddleware import code.util.Helper.MdcLoggable import com.openbankproject.commons.util.ApiVersion @@ -67,14 +67,17 @@ object Http4sUKOBv401 extends MdcLoggable { Http4sUKOBv401Events.resourceDocs ++ Http4sUKOBv401Vrp.resourceDocs - val allRoutes: HttpRoutes[IO] = Kleisli[HttpF, Request[IO], Response[IO]] { req => - Http4sUKOBv401AccountInfo.routes(req) - .orElse(Http4sUKOBv401PaymentInitiation.routes(req)) - .orElse(Http4sUKOBv401ConfirmationFunds.routes(req)) - .orElse(Http4sUKOBv401EventNotifications.routes(req)) - .orElse(Http4sUKOBv401Events.routes(req)) - .orElse(Http4sUKOBv401Vrp.routes(req)) - } + lazy val routesInOrder: List[Http4sHandler] = + Http4sUKOBv401AccountInfo.routesInOrder ++ + Http4sUKOBv401PaymentInitiation.routesInOrder ++ + Http4sUKOBv401ConfirmationFunds.routesInOrder ++ + Http4sUKOBv401EventNotifications.routesInOrder ++ + Http4sUKOBv401Events.routesInOrder ++ + Http4sUKOBv401Vrp.routesInOrder + + lazy val orderedResourceDocs: ArrayBuffer[ResourceDoc] = ResourceDocMatcher.orderByRoutes(resourceDocs, routesInOrder) + + lazy val allRoutes: HttpRoutes[IO] = Http4sRoute.chain(routesInOrder) private val fapiInteractionIdHeader = CIString("x-fapi-interaction-id") @@ -90,5 +93,5 @@ object Http4sUKOBv401 extends MdcLoggable { routes(req).map(_.putHeaders(Header.Raw(fapiInteractionIdHeader, interactionId))) } - val wrappedRoutes: HttpRoutes[IO] = withFapiInteractionId(ResourceDocMiddleware.apply(resourceDocs)(IdempotencyMiddleware(allRoutes))) + lazy val wrappedRoutes: HttpRoutes[IO] = withFapiInteractionId(ResourceDocMiddleware.apply(orderedResourceDocs, routes => IdempotencyMiddleware(routes))(IdempotencyMiddleware(allRoutes))) } diff --git a/obp-api/src/main/scala/code/api/UKOpenBanking/v4_0_1/Http4sUKOBv401AccountInfo.scala b/obp-api/src/main/scala/code/api/UKOpenBanking/v4_0_1/Http4sUKOBv401AccountInfo.scala index 2a9c173702..7cb11a3b8d 100644 --- a/obp-api/src/main/scala/code/api/UKOpenBanking/v4_0_1/Http4sUKOBv401AccountInfo.scala +++ b/obp-api/src/main/scala/code/api/UKOpenBanking/v4_0_1/Http4sUKOBv401AccountInfo.scala @@ -34,7 +34,7 @@ import cats.effect.IO import code.api.APIFailureNewStyle import code.api.Constant import code.api.UKOpenBanking.v3_1_0.JSONFactory_UKOpenBanking_310.ConsentPostBodyUKV310 -import code.api.util.APIUtil.{EmptyBody, ResourceDoc, HTTPParam, UserOrApplication, connectorEmptyResponse, createQueriesByHttpParams, defaultBankId, fullBoxOrException, passesPsd2Aisp, unboxFull, unboxFullOrFail, parseIso8601OrDayDate} +import code.api.util.APIUtil.{Http4sHandler, Http4sRoute, EmptyBody, ResourceDoc, HTTPParam, UserOrApplication, connectorEmptyResponse, createQueriesByHttpParams, defaultBankId, fullBoxOrException, passesPsd2Aisp, unboxFull, unboxFullOrFail, parseIso8601OrDayDate} import code.api.util.ApiTag import code.api.util.CallContext import code.api.util.CustomJsonFormats @@ -117,7 +117,7 @@ object Http4sUKOBv401AccountInfo extends MdcLoggable { "LastAvailableDateTime": "2020-01-01T00:00:00+00:00" } }""" - lazy val createAccountAccessConsents: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val createAccountAccessConsents: Http4sRoute = Http4sRoute { case req @ POST -> `ukV401Prefix` / "aisp" / "account-access-consents" => // Check auth FIRST (before body parsing) to mirror Lift's wrappedWithAuthCheck behaviour: // unauthenticated -> 401, invalid body -> 400. @@ -248,7 +248,7 @@ object Http4sUKOBv401AccountInfo extends MdcLoggable { "LastAvailableDateTime": "2020-01-01T00:00:00+00:00" } }""" - lazy val getAccountAccessConsentsConsentId: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getAccountAccessConsentsConsentId: Http4sRoute = Http4sRoute { case req @ GET -> `ukV401Prefix` / "aisp" / "account-access-consents" / consentId => // Not withUser: the standard has the AISP poll its own consent with a client-credentials // token, which carries no PSU. Consent.checkUKConsentAccess decides who may read it from @@ -295,7 +295,7 @@ object Http4sUKOBv401AccountInfo extends MdcLoggable { ) private val EX_deleteAccountAccessConsentsConsentId: String = """{}""" - lazy val deleteAccountAccessConsentsConsentId: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val deleteAccountAccessConsentsConsentId: Http4sRoute = Http4sRoute { case req @ DELETE -> `ukV401Prefix` / "aisp" / "account-access-consents" / consentId => // Not withUserDelete -- see the GET twin above. EndpointHelpers.executeDelete(req) { cc => @@ -399,7 +399,7 @@ object Http4sUKOBv401AccountInfo extends MdcLoggable { "LastAvailableDateTime": "2020-01-01T00:00:00+00:00" } }""" - lazy val getAccounts: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getAccounts: Http4sRoute = Http4sRoute { case req @ GET -> `ukV401Prefix` / "aisp" / "accounts" => EndpointHelpers.withUser(req) { (u, cc) => val detailViewId = ViewId(Constant.SYSTEM_READ_ACCOUNTS_DETAIL_VIEW_ID) @@ -509,7 +509,7 @@ object Http4sUKOBv401AccountInfo extends MdcLoggable { "LastAvailableDateTime": "2020-01-01T00:00:00+00:00" } }""" - lazy val getAccountsAccountId: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getAccountsAccountId: Http4sRoute = Http4sRoute { case req @ GET -> `ukV401Prefix` / "aisp" / "accounts" / accountIdStr => EndpointHelpers.withUser(req) { (u, cc) => val accountId = AccountId(accountIdStr) @@ -598,7 +598,7 @@ object Http4sUKOBv401AccountInfo extends MdcLoggable { "LastAvailableDateTime": "2020-01-01T00:00:00+00:00" } }""" - lazy val getAccountsAccountIdBalances: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getAccountsAccountIdBalances: Http4sRoute = Http4sRoute { case req @ GET -> `ukV401Prefix` / "aisp" / "accounts" / accountIdStr / "balances" => EndpointHelpers.withUser(req) { (u, cc) => val accountId = AccountId(accountIdStr) @@ -690,7 +690,7 @@ object Http4sUKOBv401AccountInfo extends MdcLoggable { "LastAvailableDateTime": "2020-01-01T00:00:00+00:00" } }""" - lazy val getAccountsAccountIdBeneficiaries: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getAccountsAccountIdBeneficiaries: Http4sRoute = Http4sRoute { case req @ GET -> `ukV401Prefix` / "aisp" / "accounts" / accountId / "beneficiaries" => EndpointHelpers.withUser(req) { (u, cc) => Future.successful(parseBody(EX_getAccountsAccountIdBeneficiaries)) } } @@ -751,7 +751,7 @@ object Http4sUKOBv401AccountInfo extends MdcLoggable { "LastAvailableDateTime": "2020-01-01T00:00:00+00:00" } }""" - lazy val getAccountsAccountIdDirectDebits: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getAccountsAccountIdDirectDebits: Http4sRoute = Http4sRoute { case req @ GET -> `ukV401Prefix` / "aisp" / "accounts" / accountId / "direct-debits" => EndpointHelpers.withUser(req) { (u, cc) => Future.successful(parseBody(EX_getAccountsAccountIdDirectDebits)) } } @@ -807,7 +807,7 @@ object Http4sUKOBv401AccountInfo extends MdcLoggable { "LastAvailableDateTime": "2020-01-01T00:00:00+00:00" } }""" - lazy val getAccountsAccountIdOffers: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getAccountsAccountIdOffers: Http4sRoute = Http4sRoute { case req @ GET -> `ukV401Prefix` / "aisp" / "accounts" / accountId / "offers" => EndpointHelpers.withUser(req) { (u, cc) => Future.successful(parseBody(EX_getAccountsAccountIdOffers)) } } @@ -887,7 +887,7 @@ object Http4sUKOBv401AccountInfo extends MdcLoggable { "LastAvailableDateTime": "2020-01-01T00:00:00+00:00" } }""" - lazy val getAccountsAccountIdParties: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getAccountsAccountIdParties: Http4sRoute = Http4sRoute { case req @ GET -> `ukV401Prefix` / "aisp" / "accounts" / accountId / "parties" => EndpointHelpers.withUser(req) { (u, cc) => Future.successful(parseBody(EX_getAccountsAccountIdParties)) } } @@ -965,7 +965,7 @@ object Http4sUKOBv401AccountInfo extends MdcLoggable { "LastAvailableDateTime": "2020-01-01T00:00:00+00:00" } }""" - lazy val getAccountsAccountIdParty: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getAccountsAccountIdParty: Http4sRoute = Http4sRoute { case req @ GET -> `ukV401Prefix` / "aisp" / "accounts" / accountId / "party" => EndpointHelpers.withUser(req) { (u, cc) => Future.successful(parseBody(EX_getAccountsAccountIdParty)) } } @@ -1156,7 +1156,7 @@ object Http4sUKOBv401AccountInfo extends MdcLoggable { "LastAvailableDateTime": "2020-01-01T00:00:00+00:00" } }""" - lazy val getAccountsAccountIdProduct: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getAccountsAccountIdProduct: Http4sRoute = Http4sRoute { case req @ GET -> `ukV401Prefix` / "aisp" / "accounts" / accountId / "product" => EndpointHelpers.withUser(req) { (u, cc) => Future.successful(parseBody(EX_getAccountsAccountIdProduct)) } } @@ -1243,7 +1243,7 @@ object Http4sUKOBv401AccountInfo extends MdcLoggable { "LastAvailableDateTime": "2020-01-01T00:00:00+00:00" } }""" - lazy val getAccountsAccountIdScheduledPayments: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getAccountsAccountIdScheduledPayments: Http4sRoute = Http4sRoute { case req @ GET -> `ukV401Prefix` / "aisp" / "accounts" / accountId / "scheduled-payments" => EndpointHelpers.withUser(req) { (u, cc) => Future.successful(parseBody(EX_getAccountsAccountIdScheduledPayments)) } } @@ -1373,7 +1373,7 @@ object Http4sUKOBv401AccountInfo extends MdcLoggable { "LastAvailableDateTime": "2020-01-01T00:00:00+00:00" } }""" - lazy val getAccountsAccountIdStandingOrders: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getAccountsAccountIdStandingOrders: Http4sRoute = Http4sRoute { case req @ GET -> `ukV401Prefix` / "aisp" / "accounts" / accountId / "standing-orders" => EndpointHelpers.withUser(req) { (u, cc) => Future.successful(parseBody(EX_getAccountsAccountIdStandingOrders)) } } @@ -1496,7 +1496,7 @@ object Http4sUKOBv401AccountInfo extends MdcLoggable { "LastAvailableDateTime": "2020-01-01T00:00:00+00:00" } }""" - lazy val getAccountsAccountIdStatements: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getAccountsAccountIdStatements: Http4sRoute = Http4sRoute { case req @ GET -> `ukV401Prefix` / "aisp" / "accounts" / accountId / "statements" => EndpointHelpers.withUser(req) { (u, cc) => Future.successful(parseBody(EX_getAccountsAccountIdStatements)) } } @@ -1619,7 +1619,7 @@ object Http4sUKOBv401AccountInfo extends MdcLoggable { "LastAvailableDateTime": "2020-01-01T00:00:00+00:00" } }""" - lazy val getAccountsAccountIdStatementsStatementId: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getAccountsAccountIdStatementsStatementId: Http4sRoute = Http4sRoute { case req @ GET -> `ukV401Prefix` / "aisp" / "accounts" / accountId / "statements" / statementId => EndpointHelpers.withUser(req) { (u, cc) => Future.successful(parseBody(EX_getAccountsAccountIdStatementsStatementId)) } } @@ -1638,7 +1638,7 @@ object Http4sUKOBv401AccountInfo extends MdcLoggable { ) private val EX_getAccountsAccountIdStatementsStatementIdFile: String = """{}""" - lazy val getAccountsAccountIdStatementsStatementIdFile: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getAccountsAccountIdStatementsStatementIdFile: Http4sRoute = Http4sRoute { case req @ GET -> `ukV401Prefix` / "aisp" / "accounts" / accountId / "statements" / statementId / "file" => EndpointHelpers.withUser(req) { (u, cc) => Future.successful(parseBody(EX_getAccountsAccountIdStatementsStatementIdFile)) } } @@ -1966,7 +1966,7 @@ object Http4sUKOBv401AccountInfo extends MdcLoggable { "LastAvailableDateTime": "2020-01-01T00:00:00+00:00" } }""" - lazy val getAccountsAccountIdStatementsStatementIdTransactions: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getAccountsAccountIdStatementsStatementIdTransactions: Http4sRoute = Http4sRoute { case req @ GET -> `ukV401Prefix` / "aisp" / "accounts" / accountId / "statements" / statementId / "transactions" => EndpointHelpers.withUser(req) { (u, cc) => Future.successful(parseBody(EX_getAccountsAccountIdStatementsStatementIdTransactions)) } } @@ -2294,7 +2294,7 @@ object Http4sUKOBv401AccountInfo extends MdcLoggable { "LastAvailableDateTime": "2020-01-01T00:00:00+00:00" } }""" - lazy val getAccountsAccountIdTransactions: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getAccountsAccountIdTransactions: Http4sRoute = Http4sRoute { case req @ GET -> `ukV401Prefix` / "aisp" / "accounts" / accountIdStr / "transactions" => EndpointHelpers.withUser(req) { (u, cc) => val accountId = AccountId(accountIdStr) @@ -2367,7 +2367,7 @@ object Http4sUKOBv401AccountInfo extends MdcLoggable { "LastAvailableDateTime": "2020-01-01T00:00:00+00:00" } }""" - lazy val getBalances: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getBalances: Http4sRoute = Http4sRoute { case req @ GET -> `ukV401Prefix` / "aisp" / "balances" => EndpointHelpers.withUser(req) { (u, cc) => val balancesViewId = ViewId(Constant.SYSTEM_READ_BALANCES_VIEW_ID) @@ -2467,7 +2467,7 @@ object Http4sUKOBv401AccountInfo extends MdcLoggable { "LastAvailableDateTime": "2020-01-01T00:00:00+00:00" } }""" - lazy val getBeneficiaries: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getBeneficiaries: Http4sRoute = Http4sRoute { case req @ GET -> `ukV401Prefix` / "aisp" / "beneficiaries" => EndpointHelpers.withUser(req) { (u, cc) => Future.successful(parseBody(EX_getBeneficiaries)) } } @@ -2528,7 +2528,7 @@ object Http4sUKOBv401AccountInfo extends MdcLoggable { "LastAvailableDateTime": "2020-01-01T00:00:00+00:00" } }""" - lazy val getDirectDebits: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getDirectDebits: Http4sRoute = Http4sRoute { case req @ GET -> `ukV401Prefix` / "aisp" / "direct-debits" => EndpointHelpers.withUser(req) { (u, cc) => Future.successful(parseBody(EX_getDirectDebits)) } } @@ -2584,7 +2584,7 @@ object Http4sUKOBv401AccountInfo extends MdcLoggable { "LastAvailableDateTime": "2020-01-01T00:00:00+00:00" } }""" - lazy val getOffers: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getOffers: Http4sRoute = Http4sRoute { case req @ GET -> `ukV401Prefix` / "aisp" / "offers" => EndpointHelpers.withUser(req) { (u, cc) => Future.successful(parseBody(EX_getOffers)) } } @@ -2662,7 +2662,7 @@ object Http4sUKOBv401AccountInfo extends MdcLoggable { "LastAvailableDateTime": "2020-01-01T00:00:00+00:00" } }""" - lazy val getParty: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getParty: Http4sRoute = Http4sRoute { case req @ GET -> `ukV401Prefix` / "aisp" / "party" => EndpointHelpers.withUser(req) { (u, cc) => Future.successful(parseBody(EX_getParty)) } } @@ -2853,7 +2853,7 @@ object Http4sUKOBv401AccountInfo extends MdcLoggable { "LastAvailableDateTime": "2020-01-01T00:00:00+00:00" } }""" - lazy val getProducts: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getProducts: Http4sRoute = Http4sRoute { case req @ GET -> `ukV401Prefix` / "aisp" / "products" => EndpointHelpers.withUser(req) { (u, cc) => Future.successful(parseBody(EX_getProducts)) } } @@ -2940,7 +2940,7 @@ object Http4sUKOBv401AccountInfo extends MdcLoggable { "LastAvailableDateTime": "2020-01-01T00:00:00+00:00" } }""" - lazy val getScheduledPayments: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getScheduledPayments: Http4sRoute = Http4sRoute { case req @ GET -> `ukV401Prefix` / "aisp" / "scheduled-payments" => EndpointHelpers.withUser(req) { (u, cc) => Future.successful(parseBody(EX_getScheduledPayments)) } } @@ -3070,7 +3070,7 @@ object Http4sUKOBv401AccountInfo extends MdcLoggable { "LastAvailableDateTime": "2020-01-01T00:00:00+00:00" } }""" - lazy val getStandingOrders: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getStandingOrders: Http4sRoute = Http4sRoute { case req @ GET -> `ukV401Prefix` / "aisp" / "standing-orders" => EndpointHelpers.withUser(req) { (u, cc) => Future.successful(parseBody(EX_getStandingOrders)) } } @@ -3193,7 +3193,7 @@ object Http4sUKOBv401AccountInfo extends MdcLoggable { "LastAvailableDateTime": "2020-01-01T00:00:00+00:00" } }""" - lazy val getStatements: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getStatements: Http4sRoute = Http4sRoute { case req @ GET -> `ukV401Prefix` / "aisp" / "statements" => EndpointHelpers.withUser(req) { (u, cc) => Future.successful(parseBody(EX_getStatements)) } } @@ -3521,7 +3521,7 @@ object Http4sUKOBv401AccountInfo extends MdcLoggable { "LastAvailableDateTime": "2020-01-01T00:00:00+00:00" } }""" - lazy val getTransactions: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getTransactions: Http4sRoute = Http4sRoute { case req @ GET -> `ukV401Prefix` / "aisp" / "transactions" => EndpointHelpers.withUser(req) { (u, cc) => for { @@ -3585,35 +3585,37 @@ object Http4sUKOBv401AccountInfo extends MdcLoggable { http4sPartialFunction = Some(getTransactions) ) - val routes: HttpRoutes[IO] = Kleisli[HttpF, Request[IO], Response[IO]] { req => - createAccountAccessConsents(req) - .orElse(getAccountAccessConsentsConsentId(req) - .orElse(deleteAccountAccessConsentsConsentId(req) - .orElse(getAccounts(req) - .orElse(getAccountsAccountId(req) - .orElse(getAccountsAccountIdBalances(req) - .orElse(getAccountsAccountIdBeneficiaries(req) - .orElse(getAccountsAccountIdDirectDebits(req) - .orElse(getAccountsAccountIdOffers(req) - .orElse(getAccountsAccountIdParties(req) - .orElse(getAccountsAccountIdParty(req) - .orElse(getAccountsAccountIdProduct(req) - .orElse(getAccountsAccountIdScheduledPayments(req) - .orElse(getAccountsAccountIdStandingOrders(req) - .orElse(getAccountsAccountIdStatements(req) - .orElse(getAccountsAccountIdStatementsStatementId(req) - .orElse(getAccountsAccountIdStatementsStatementIdFile(req) - .orElse(getAccountsAccountIdStatementsStatementIdTransactions(req) - .orElse(getAccountsAccountIdTransactions(req) - .orElse(getBalances(req) - .orElse(getBeneficiaries(req) - .orElse(getDirectDebits(req) - .orElse(getOffers(req) - .orElse(getParty(req) - .orElse(getProducts(req) - .orElse(getScheduledPayments(req) - .orElse(getStandingOrders(req) - .orElse(getStatements(req) - .orElse(getTransactions(req))))))))))))))))))))))))))))) - } + lazy val routesInOrder: List[Http4sHandler] = List( + createAccountAccessConsents, + getAccountAccessConsentsConsentId, + deleteAccountAccessConsentsConsentId, + getAccounts, + getAccountsAccountId, + getAccountsAccountIdBalances, + getAccountsAccountIdBeneficiaries, + getAccountsAccountIdDirectDebits, + getAccountsAccountIdOffers, + getAccountsAccountIdParties, + getAccountsAccountIdParty, + getAccountsAccountIdProduct, + getAccountsAccountIdScheduledPayments, + getAccountsAccountIdStandingOrders, + getAccountsAccountIdStatements, + getAccountsAccountIdStatementsStatementId, + getAccountsAccountIdStatementsStatementIdFile, + getAccountsAccountIdStatementsStatementIdTransactions, + getAccountsAccountIdTransactions, + getBalances, + getBeneficiaries, + getDirectDebits, + getOffers, + getParty, + getProducts, + getScheduledPayments, + getStandingOrders, + getStatements, + getTransactions + ) + + lazy val routes: HttpRoutes[IO] = Http4sRoute.chain(routesInOrder) } diff --git a/obp-api/src/main/scala/code/api/UKOpenBanking/v4_0_1/Http4sUKOBv401ConfirmationFunds.scala b/obp-api/src/main/scala/code/api/UKOpenBanking/v4_0_1/Http4sUKOBv401ConfirmationFunds.scala index 6d437b1221..2cfc454bbe 100644 --- a/obp-api/src/main/scala/code/api/UKOpenBanking/v4_0_1/Http4sUKOBv401ConfirmationFunds.scala +++ b/obp-api/src/main/scala/code/api/UKOpenBanking/v4_0_1/Http4sUKOBv401ConfirmationFunds.scala @@ -29,7 +29,7 @@ package code.api.UKOpenBanking.v4_0_1 import cats.data.{Kleisli, OptionT} import cats.effect.IO -import code.api.util.APIUtil.{EmptyBody, ResourceDoc} +import code.api.util.APIUtil.{Http4sHandler, Http4sRoute, EmptyBody, ResourceDoc} import code.api.util.ApiTag import code.api.util.CustomJsonFormats import code.api.util.ErrorMessages.{AuthenticatedUserIsRequired, UnknownError} @@ -113,7 +113,7 @@ object Http4sUKOBv401ConfirmationFunds extends MdcLoggable { "LastAvailableDateTime": "2020-01-01T00:00:00+00:00" } }""" - lazy val createFundsConfirmationConsents: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val createFundsConfirmationConsents: Http4sRoute = Http4sRoute { case req @ POST -> `ukV401Prefix` / "cbpii" / "funds-confirmation-consents" => EndpointHelpers.executeFutureCreated(req)(Future.successful(parseBody(EX_createFundsConfirmationConsents))) } @@ -170,7 +170,7 @@ object Http4sUKOBv401ConfirmationFunds extends MdcLoggable { "LastAvailableDateTime": "2020-01-01T00:00:00+00:00" } }""" - lazy val getFundsConfirmationConsentsConsentId: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getFundsConfirmationConsentsConsentId: Http4sRoute = Http4sRoute { case req @ GET -> `ukV401Prefix` / "cbpii" / "funds-confirmation-consents" / consentId => EndpointHelpers.withUser(req) { (u, cc) => Future.successful(parseBody(EX_getFundsConfirmationConsentsConsentId)) } } @@ -189,7 +189,7 @@ object Http4sUKOBv401ConfirmationFunds extends MdcLoggable { ) private val EX_deleteFundsConfirmationConsentsConsentId: String = """{}""" - lazy val deleteFundsConfirmationConsentsConsentId: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val deleteFundsConfirmationConsentsConsentId: Http4sRoute = Http4sRoute { case req @ DELETE -> `ukV401Prefix` / "cbpii" / "funds-confirmation-consents" / consentId => EndpointHelpers.executeDelete(req) { cc => Future.successful(()) } } @@ -242,7 +242,7 @@ object Http4sUKOBv401ConfirmationFunds extends MdcLoggable { "LastAvailableDateTime": "2020-01-01T00:00:00+00:00" } }""" - lazy val createFundsConfirmations: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val createFundsConfirmations: Http4sRoute = Http4sRoute { case req @ POST -> `ukV401Prefix` / "cbpii" / "funds-confirmations" => EndpointHelpers.executeFutureCreated(req)(Future.successful(parseBody(EX_createFundsConfirmations))) } @@ -260,10 +260,12 @@ object Http4sUKOBv401ConfirmationFunds extends MdcLoggable { http4sPartialFunction = Some(createFundsConfirmations) ) - val routes: HttpRoutes[IO] = Kleisli[HttpF, Request[IO], Response[IO]] { req => - createFundsConfirmationConsents(req) - .orElse(getFundsConfirmationConsentsConsentId(req) - .orElse(deleteFundsConfirmationConsentsConsentId(req) - .orElse(createFundsConfirmations(req)))) - } + lazy val routesInOrder: List[Http4sHandler] = List( + createFundsConfirmationConsents, + getFundsConfirmationConsentsConsentId, + deleteFundsConfirmationConsentsConsentId, + createFundsConfirmations + ) + + lazy val routes: HttpRoutes[IO] = Http4sRoute.chain(routesInOrder) } diff --git a/obp-api/src/main/scala/code/api/UKOpenBanking/v4_0_1/Http4sUKOBv401EventNotifications.scala b/obp-api/src/main/scala/code/api/UKOpenBanking/v4_0_1/Http4sUKOBv401EventNotifications.scala index c00aee97b1..3f4bc6b43f 100644 --- a/obp-api/src/main/scala/code/api/UKOpenBanking/v4_0_1/Http4sUKOBv401EventNotifications.scala +++ b/obp-api/src/main/scala/code/api/UKOpenBanking/v4_0_1/Http4sUKOBv401EventNotifications.scala @@ -29,7 +29,7 @@ package code.api.UKOpenBanking.v4_0_1 import cats.data.{Kleisli, OptionT} import cats.effect.IO -import code.api.util.APIUtil.{EmptyBody, ResourceDoc} +import code.api.util.APIUtil.{Http4sHandler, Http4sRoute, EmptyBody, ResourceDoc} import code.api.util.ApiTag import code.api.util.CustomJsonFormats import code.api.util.ErrorMessages.{AuthenticatedUserIsRequired, UnknownError} @@ -59,7 +59,7 @@ object Http4sUKOBv401EventNotifications extends MdcLoggable { val ukV401Prefix = Root / ApiVersion.ukOpenBankingV401.urlPrefix / ApiVersion.ukOpenBankingV401.apiShortVersion private val EX_createEventNotification: String = """{}""" - lazy val createEventNotification: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val createEventNotification: Http4sRoute = Http4sRoute { case req @ POST -> `ukV401Prefix` / "event-notifications" => EndpointHelpers.executeFutureCreated(req)(Future.successful(parseBody(EX_createEventNotification))) } @@ -77,7 +77,9 @@ object Http4sUKOBv401EventNotifications extends MdcLoggable { http4sPartialFunction = Some(createEventNotification) ) - val routes: HttpRoutes[IO] = Kleisli[HttpF, Request[IO], Response[IO]] { req => - createEventNotification(req) - } + lazy val routesInOrder: List[Http4sHandler] = List( + createEventNotification + ) + + lazy val routes: HttpRoutes[IO] = Http4sRoute.chain(routesInOrder) } diff --git a/obp-api/src/main/scala/code/api/UKOpenBanking/v4_0_1/Http4sUKOBv401Events.scala b/obp-api/src/main/scala/code/api/UKOpenBanking/v4_0_1/Http4sUKOBv401Events.scala index 57f0927a23..56a9f28616 100644 --- a/obp-api/src/main/scala/code/api/UKOpenBanking/v4_0_1/Http4sUKOBv401Events.scala +++ b/obp-api/src/main/scala/code/api/UKOpenBanking/v4_0_1/Http4sUKOBv401Events.scala @@ -29,7 +29,7 @@ package code.api.UKOpenBanking.v4_0_1 import cats.data.{Kleisli, OptionT} import cats.effect.IO -import code.api.util.APIUtil.{EmptyBody, ResourceDoc} +import code.api.util.APIUtil.{Http4sHandler, Http4sRoute, EmptyBody, ResourceDoc} import code.api.util.ApiTag import code.api.util.CustomJsonFormats import code.api.util.ErrorMessages.{AuthenticatedUserIsRequired, UnknownError} @@ -84,7 +84,7 @@ object Http4sUKOBv401Events extends MdcLoggable { "LastAvailableDateTime": "2020-01-01T00:00:00+00:00" } }""" - lazy val getEventSubscriptions: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getEventSubscriptions: Http4sRoute = Http4sRoute { case req @ GET -> `ukV401Prefix` / "event-subscriptions" => EndpointHelpers.withUser(req) { (u, cc) => Future.successful(parseBody(EX_getEventSubscriptions)) } } @@ -133,7 +133,7 @@ object Http4sUKOBv401Events extends MdcLoggable { "LastAvailableDateTime": "2020-01-01T00:00:00+00:00" } }""" - lazy val createEventSubscriptions: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val createEventSubscriptions: Http4sRoute = Http4sRoute { case req @ POST -> `ukV401Prefix` / "event-subscriptions" => EndpointHelpers.executeFutureCreated(req)(Future.successful(parseBody(EX_createEventSubscriptions))) } @@ -195,7 +195,7 @@ object Http4sUKOBv401Events extends MdcLoggable { "LastAvailableDateTime": "2020-01-01T00:00:00+00:00" } }""" - lazy val changeEventSubscriptionsEventSubscriptionId: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val changeEventSubscriptionsEventSubscriptionId: Http4sRoute = Http4sRoute { case req @ PUT -> `ukV401Prefix` / "event-subscriptions" / eventSubscriptionId => EndpointHelpers.executeFutureCreated(req)(Future.successful(parseBody(EX_changeEventSubscriptionsEventSubscriptionId))) } @@ -214,7 +214,7 @@ object Http4sUKOBv401Events extends MdcLoggable { ) private val EX_deleteEventSubscriptionsEventSubscriptionId: String = """{}""" - lazy val deleteEventSubscriptionsEventSubscriptionId: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val deleteEventSubscriptionsEventSubscriptionId: Http4sRoute = Http4sRoute { case req @ DELETE -> `ukV401Prefix` / "event-subscriptions" / eventSubscriptionId => EndpointHelpers.executeDelete(req) { cc => Future.successful(()) } } @@ -244,7 +244,7 @@ object Http4sUKOBv401Events extends MdcLoggable { "moreAvailable": true, "sets": {} }""" - lazy val createEvents: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val createEvents: Http4sRoute = Http4sRoute { case req @ POST -> `ukV401Prefix` / "events" => EndpointHelpers.executeFutureCreated(req)(Future.successful(parseBody(EX_createEvents))) } @@ -262,11 +262,13 @@ object Http4sUKOBv401Events extends MdcLoggable { http4sPartialFunction = Some(createEvents) ) - val routes: HttpRoutes[IO] = Kleisli[HttpF, Request[IO], Response[IO]] { req => - getEventSubscriptions(req) - .orElse(createEventSubscriptions(req) - .orElse(changeEventSubscriptionsEventSubscriptionId(req) - .orElse(deleteEventSubscriptionsEventSubscriptionId(req) - .orElse(createEvents(req))))) - } + lazy val routesInOrder: List[Http4sHandler] = List( + getEventSubscriptions, + createEventSubscriptions, + changeEventSubscriptionsEventSubscriptionId, + deleteEventSubscriptionsEventSubscriptionId, + createEvents + ) + + lazy val routes: HttpRoutes[IO] = Http4sRoute.chain(routesInOrder) } diff --git a/obp-api/src/main/scala/code/api/UKOpenBanking/v4_0_1/Http4sUKOBv401PaymentInitiation.scala b/obp-api/src/main/scala/code/api/UKOpenBanking/v4_0_1/Http4sUKOBv401PaymentInitiation.scala index 683dad265a..fe908dd9b2 100644 --- a/obp-api/src/main/scala/code/api/UKOpenBanking/v4_0_1/Http4sUKOBv401PaymentInitiation.scala +++ b/obp-api/src/main/scala/code/api/UKOpenBanking/v4_0_1/Http4sUKOBv401PaymentInitiation.scala @@ -29,7 +29,7 @@ package code.api.UKOpenBanking.v4_0_1 import cats.data.{Kleisli, OptionT} import cats.effect.IO -import code.api.util.APIUtil.{EmptyBody, ResourceDoc} +import code.api.util.APIUtil.{Http4sHandler, Http4sRoute, EmptyBody, ResourceDoc} import code.api.util.ApiTag import code.api.util.CustomJsonFormats import code.api.util.ErrorMessages.{AuthenticatedUserIsRequired, UnknownError} @@ -553,7 +553,7 @@ object Http4sUKOBv401PaymentInitiation extends MdcLoggable { "LastAvailableDateTime": "2020-01-01T00:00:00+00:00" } }""" - lazy val createDomesticPaymentConsents: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val createDomesticPaymentConsents: Http4sRoute = Http4sRoute { case req @ POST -> `ukV401Prefix` / "pisp" / "domestic-payment-consents" => EndpointHelpers.executeFutureCreated(req)(Future.successful(parseBody(EX_createDomesticPaymentConsents))) } @@ -840,7 +840,7 @@ object Http4sUKOBv401PaymentInitiation extends MdcLoggable { "LastAvailableDateTime": "2020-01-01T00:00:00+00:00" } }""" - lazy val getDomesticPaymentConsentsConsentId: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getDomesticPaymentConsentsConsentId: Http4sRoute = Http4sRoute { case req @ GET -> `ukV401Prefix` / "pisp" / "domestic-payment-consents" / consentId => EndpointHelpers.withUser(req) { (u, cc) => Future.successful(parseBody(EX_getDomesticPaymentConsentsConsentId)) } } @@ -879,7 +879,7 @@ object Http4sUKOBv401PaymentInitiation extends MdcLoggable { "LastAvailableDateTime": "2020-01-01T00:00:00+00:00" } }""" - lazy val getDomesticPaymentConsentsConsentIdFundsConfirmation: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getDomesticPaymentConsentsConsentIdFundsConfirmation: Http4sRoute = Http4sRoute { case req @ GET -> `ukV401Prefix` / "pisp" / "domestic-payment-consents" / consentId / "funds-confirmation" => EndpointHelpers.withUser(req) { (u, cc) => Future.successful(parseBody(EX_getDomesticPaymentConsentsConsentIdFundsConfirmation)) } } @@ -1356,7 +1356,7 @@ object Http4sUKOBv401PaymentInitiation extends MdcLoggable { "LastAvailableDateTime": "2020-01-01T00:00:00+00:00" } }""" - lazy val createDomesticPayments: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val createDomesticPayments: Http4sRoute = Http4sRoute { case req @ POST -> `ukV401Prefix` / "pisp" / "domestic-payments" => EndpointHelpers.executeFutureCreated(req)(Future.successful(parseBody(EX_createDomesticPayments))) } @@ -1616,7 +1616,7 @@ object Http4sUKOBv401PaymentInitiation extends MdcLoggable { "LastAvailableDateTime": "2020-01-01T00:00:00+00:00" } }""" - lazy val getDomesticPaymentsDomesticPaymentId: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getDomesticPaymentsDomesticPaymentId: Http4sRoute = Http4sRoute { case req @ GET -> `ukV401Prefix` / "pisp" / "domestic-payments" / domesticPaymentId => EndpointHelpers.withUser(req) { (u, cc) => Future.successful(parseBody(EX_getDomesticPaymentsDomesticPaymentId)) } } @@ -1663,7 +1663,7 @@ object Http4sUKOBv401PaymentInitiation extends MdcLoggable { "LastAvailableDateTime": "2020-01-01T00:00:00+00:00" } }""" - lazy val getDomesticPaymentsDomesticPaymentIdPaymentDetails: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getDomesticPaymentsDomesticPaymentIdPaymentDetails: Http4sRoute = Http4sRoute { case req @ GET -> `ukV401Prefix` / "pisp" / "domestic-payments" / domesticPaymentId / "payment-details" => EndpointHelpers.withUser(req) { (u, cc) => Future.successful(parseBody(EX_getDomesticPaymentsDomesticPaymentIdPaymentDetails)) } } @@ -2124,7 +2124,7 @@ object Http4sUKOBv401PaymentInitiation extends MdcLoggable { "LastAvailableDateTime": "2020-01-01T00:00:00+00:00" } }""" - lazy val createDomesticScheduledPaymentConsents: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val createDomesticScheduledPaymentConsents: Http4sRoute = Http4sRoute { case req @ POST -> `ukV401Prefix` / "pisp" / "domestic-scheduled-payment-consents" => EndpointHelpers.executeFutureCreated(req)(Future.successful(parseBody(EX_createDomesticScheduledPaymentConsents))) } @@ -2385,7 +2385,7 @@ object Http4sUKOBv401PaymentInitiation extends MdcLoggable { "LastAvailableDateTime": "2020-01-01T00:00:00+00:00" } }""" - lazy val getDomesticScheduledPaymentConsentsConsentId: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getDomesticScheduledPaymentConsentsConsentId: Http4sRoute = Http4sRoute { case req @ GET -> `ukV401Prefix` / "pisp" / "domestic-scheduled-payment-consents" / consentId => EndpointHelpers.withUser(req) { (u, cc) => Future.successful(parseBody(EX_getDomesticScheduledPaymentConsentsConsentId)) } } @@ -2808,7 +2808,7 @@ object Http4sUKOBv401PaymentInitiation extends MdcLoggable { "LastAvailableDateTime": "2020-01-01T00:00:00+00:00" } }""" - lazy val createDomesticScheduledPayments: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val createDomesticScheduledPayments: Http4sRoute = Http4sRoute { case req @ POST -> `ukV401Prefix` / "pisp" / "domestic-scheduled-payments" => EndpointHelpers.executeFutureCreated(req)(Future.successful(parseBody(EX_createDomesticScheduledPayments))) } @@ -3041,7 +3041,7 @@ object Http4sUKOBv401PaymentInitiation extends MdcLoggable { "LastAvailableDateTime": "2020-01-01T00:00:00+00:00" } }""" - lazy val getDomesticScheduledPaymentsDomesticScheduledPaymentId: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getDomesticScheduledPaymentsDomesticScheduledPaymentId: Http4sRoute = Http4sRoute { case req @ GET -> `ukV401Prefix` / "pisp" / "domestic-scheduled-payments" / domesticScheduledPaymentId => EndpointHelpers.withUser(req) { (u, cc) => Future.successful(parseBody(EX_getDomesticScheduledPaymentsDomesticScheduledPaymentId)) } } @@ -3088,7 +3088,7 @@ object Http4sUKOBv401PaymentInitiation extends MdcLoggable { "LastAvailableDateTime": "2020-01-01T00:00:00+00:00" } }""" - lazy val getDomesticScheduledPaymentsDomesticScheduledPaymentIdPaymentDetails: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getDomesticScheduledPaymentsDomesticScheduledPaymentIdPaymentDetails: Http4sRoute = Http4sRoute { case req @ GET -> `ukV401Prefix` / "pisp" / "domestic-scheduled-payments" / domesticScheduledPaymentId / "payment-details" => EndpointHelpers.withUser(req) { (u, cc) => Future.successful(parseBody(EX_getDomesticScheduledPaymentsDomesticScheduledPaymentIdPaymentDetails)) } } @@ -3541,7 +3541,7 @@ object Http4sUKOBv401PaymentInitiation extends MdcLoggable { "LastAvailableDateTime": "2020-01-01T00:00:00+00:00" } }""" - lazy val createDomesticStandingOrderConsents: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val createDomesticStandingOrderConsents: Http4sRoute = Http4sRoute { case req @ POST -> `ukV401Prefix` / "pisp" / "domestic-standing-order-consents" => EndpointHelpers.executeFutureCreated(req)(Future.successful(parseBody(EX_createDomesticStandingOrderConsents))) } @@ -3797,7 +3797,7 @@ object Http4sUKOBv401PaymentInitiation extends MdcLoggable { "LastAvailableDateTime": "2020-01-01T00:00:00+00:00" } }""" - lazy val getDomesticStandingOrderConsentsConsentId: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getDomesticStandingOrderConsentsConsentId: Http4sRoute = Http4sRoute { case req @ GET -> `ukV401Prefix` / "pisp" / "domestic-standing-order-consents" / consentId => EndpointHelpers.withUser(req) { (u, cc) => Future.successful(parseBody(EX_getDomesticStandingOrderConsentsConsentId)) } } @@ -4212,7 +4212,7 @@ object Http4sUKOBv401PaymentInitiation extends MdcLoggable { "LastAvailableDateTime": "2020-01-01T00:00:00+00:00" } }""" - lazy val createDomesticStandingOrders: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val createDomesticStandingOrders: Http4sRoute = Http4sRoute { case req @ POST -> `ukV401Prefix` / "pisp" / "domestic-standing-orders" => EndpointHelpers.executeFutureCreated(req)(Future.successful(parseBody(EX_createDomesticStandingOrders))) } @@ -4440,7 +4440,7 @@ object Http4sUKOBv401PaymentInitiation extends MdcLoggable { "LastAvailableDateTime": "2020-01-01T00:00:00+00:00" } }""" - lazy val getDomesticStandingOrdersDomesticStandingOrderId: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getDomesticStandingOrdersDomesticStandingOrderId: Http4sRoute = Http4sRoute { case req @ GET -> `ukV401Prefix` / "pisp" / "domestic-standing-orders" / domesticStandingOrderId => EndpointHelpers.withUser(req) { (u, cc) => Future.successful(parseBody(EX_getDomesticStandingOrdersDomesticStandingOrderId)) } } @@ -4487,7 +4487,7 @@ object Http4sUKOBv401PaymentInitiation extends MdcLoggable { "LastAvailableDateTime": "2020-01-01T00:00:00+00:00" } }""" - lazy val getDomesticStandingOrdersDomesticStandingOrderIdPaymentDetails: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getDomesticStandingOrdersDomesticStandingOrderIdPaymentDetails: Http4sRoute = Http4sRoute { case req @ GET -> `ukV401Prefix` / "pisp" / "domestic-standing-orders" / domesticStandingOrderId / "payment-details" => EndpointHelpers.withUser(req) { (u, cc) => Future.successful(parseBody(EX_getDomesticStandingOrdersDomesticStandingOrderIdPaymentDetails)) } } @@ -4774,7 +4774,7 @@ object Http4sUKOBv401PaymentInitiation extends MdcLoggable { "LastAvailableDateTime": "2020-01-01T00:00:00+00:00" } }""" - lazy val createFilePaymentConsents: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val createFilePaymentConsents: Http4sRoute = Http4sRoute { case req @ POST -> `ukV401Prefix` / "pisp" / "file-payment-consents" => EndpointHelpers.executeFutureCreated(req)(Future.successful(parseBody(EX_createFilePaymentConsents))) } @@ -4947,7 +4947,7 @@ object Http4sUKOBv401PaymentInitiation extends MdcLoggable { "LastAvailableDateTime": "2020-01-01T00:00:00+00:00" } }""" - lazy val getFilePaymentConsentsConsentId: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getFilePaymentConsentsConsentId: Http4sRoute = Http4sRoute { case req @ GET -> `ukV401Prefix` / "pisp" / "file-payment-consents" / consentId => EndpointHelpers.withUser(req) { (u, cc) => Future.successful(parseBody(EX_getFilePaymentConsentsConsentId)) } } @@ -4966,7 +4966,7 @@ object Http4sUKOBv401PaymentInitiation extends MdcLoggable { ) private val EX_getFilePaymentConsentsConsentIdFile: String = """{}""" - lazy val getFilePaymentConsentsConsentIdFile: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getFilePaymentConsentsConsentIdFile: Http4sRoute = Http4sRoute { case req @ GET -> `ukV401Prefix` / "pisp" / "file-payment-consents" / consentId / "file" => EndpointHelpers.withUser(req) { (u, cc) => Future.successful(parseBody(EX_getFilePaymentConsentsConsentIdFile)) } } @@ -4985,7 +4985,7 @@ object Http4sUKOBv401PaymentInitiation extends MdcLoggable { ) private val EX_createFilePaymentConsentsConsentIdFile: String = """{}""" - lazy val createFilePaymentConsentsConsentIdFile: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val createFilePaymentConsentsConsentIdFile: Http4sRoute = Http4sRoute { case req @ POST -> `ukV401Prefix` / "pisp" / "file-payment-consents" / consentId / "file" => EndpointHelpers.executeFutureCreated(req)(Future.successful(parseBody(EX_createFilePaymentConsentsConsentIdFile))) } @@ -5262,7 +5262,7 @@ object Http4sUKOBv401PaymentInitiation extends MdcLoggable { "LastAvailableDateTime": "2020-01-01T00:00:00+00:00" } }""" - lazy val createFilePayments: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val createFilePayments: Http4sRoute = Http4sRoute { case req @ POST -> `ukV401Prefix` / "pisp" / "file-payments" => EndpointHelpers.executeFutureCreated(req)(Future.successful(parseBody(EX_createFilePayments))) } @@ -5433,7 +5433,7 @@ object Http4sUKOBv401PaymentInitiation extends MdcLoggable { "LastAvailableDateTime": "2020-01-01T00:00:00+00:00" } }""" - lazy val getFilePaymentsFilePaymentId: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getFilePaymentsFilePaymentId: Http4sRoute = Http4sRoute { case req @ GET -> `ukV401Prefix` / "pisp" / "file-payments" / filePaymentId => EndpointHelpers.withUser(req) { (u, cc) => Future.successful(parseBody(EX_getFilePaymentsFilePaymentId)) } } @@ -5480,7 +5480,7 @@ object Http4sUKOBv401PaymentInitiation extends MdcLoggable { "LastAvailableDateTime": "2020-01-01T00:00:00+00:00" } }""" - lazy val getFilePaymentsFilePaymentIdPaymentDetails: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getFilePaymentsFilePaymentIdPaymentDetails: Http4sRoute = Http4sRoute { case req @ GET -> `ukV401Prefix` / "pisp" / "file-payments" / filePaymentId / "payment-details" => EndpointHelpers.withUser(req) { (u, cc) => Future.successful(parseBody(EX_getFilePaymentsFilePaymentIdPaymentDetails)) } } @@ -5499,7 +5499,7 @@ object Http4sUKOBv401PaymentInitiation extends MdcLoggable { ) private val EX_getFilePaymentsFilePaymentIdReportFile: String = """{}""" - lazy val getFilePaymentsFilePaymentIdReportFile: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getFilePaymentsFilePaymentIdReportFile: Http4sRoute = Http4sRoute { case req @ GET -> `ukV401Prefix` / "pisp" / "file-payments" / filePaymentId / "report-file" => EndpointHelpers.withUser(req) { (u, cc) => Future.successful(parseBody(EX_getFilePaymentsFilePaymentIdReportFile)) } } @@ -6049,7 +6049,7 @@ object Http4sUKOBv401PaymentInitiation extends MdcLoggable { "LastAvailableDateTime": "2020-01-01T00:00:00+00:00" } }""" - lazy val createInternationalPaymentConsents: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val createInternationalPaymentConsents: Http4sRoute = Http4sRoute { case req @ POST -> `ukV401Prefix` / "pisp" / "international-payment-consents" => EndpointHelpers.executeFutureCreated(req)(Future.successful(parseBody(EX_createInternationalPaymentConsents))) } @@ -6358,7 +6358,7 @@ object Http4sUKOBv401PaymentInitiation extends MdcLoggable { "LastAvailableDateTime": "2020-01-01T00:00:00+00:00" } }""" - lazy val getInternationalPaymentConsentsConsentId: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getInternationalPaymentConsentsConsentId: Http4sRoute = Http4sRoute { case req @ GET -> `ukV401Prefix` / "pisp" / "international-payment-consents" / consentId => EndpointHelpers.withUser(req) { (u, cc) => Future.successful(parseBody(EX_getInternationalPaymentConsentsConsentId)) } } @@ -6397,7 +6397,7 @@ object Http4sUKOBv401PaymentInitiation extends MdcLoggable { "LastAvailableDateTime": "2020-01-01T00:00:00+00:00" } }""" - lazy val getInternationalPaymentConsentsConsentIdFundsConfirmation: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getInternationalPaymentConsentsConsentIdFundsConfirmation: Http4sRoute = Http4sRoute { case req @ GET -> `ukV401Prefix` / "pisp" / "international-payment-consents" / consentId / "funds-confirmation" => EndpointHelpers.withUser(req) { (u, cc) => Future.successful(parseBody(EX_getInternationalPaymentConsentsConsentIdFundsConfirmation)) } } @@ -6911,7 +6911,7 @@ object Http4sUKOBv401PaymentInitiation extends MdcLoggable { "LastAvailableDateTime": "2020-01-01T00:00:00+00:00" } }""" - lazy val createInternationalPayments: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val createInternationalPayments: Http4sRoute = Http4sRoute { case req @ POST -> `ukV401Prefix` / "pisp" / "international-payments" => EndpointHelpers.executeFutureCreated(req)(Future.successful(parseBody(EX_createInternationalPayments))) } @@ -7193,7 +7193,7 @@ object Http4sUKOBv401PaymentInitiation extends MdcLoggable { "LastAvailableDateTime": "2020-01-01T00:00:00+00:00" } }""" - lazy val getInternationalPaymentsInternationalPaymentId: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getInternationalPaymentsInternationalPaymentId: Http4sRoute = Http4sRoute { case req @ GET -> `ukV401Prefix` / "pisp" / "international-payments" / internationalPaymentId => EndpointHelpers.withUser(req) { (u, cc) => Future.successful(parseBody(EX_getInternationalPaymentsInternationalPaymentId)) } } @@ -7240,7 +7240,7 @@ object Http4sUKOBv401PaymentInitiation extends MdcLoggable { "LastAvailableDateTime": "2020-01-01T00:00:00+00:00" } }""" - lazy val getInternationalPaymentsInternationalPaymentIdPaymentDetails: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getInternationalPaymentsInternationalPaymentIdPaymentDetails: Http4sRoute = Http4sRoute { case req @ GET -> `ukV401Prefix` / "pisp" / "international-payments" / internationalPaymentId / "payment-details" => EndpointHelpers.withUser(req) { (u, cc) => Future.successful(parseBody(EX_getInternationalPaymentsInternationalPaymentIdPaymentDetails)) } } @@ -7794,7 +7794,7 @@ object Http4sUKOBv401PaymentInitiation extends MdcLoggable { "LastAvailableDateTime": "2020-01-01T00:00:00+00:00" } }""" - lazy val createInternationalScheduledPaymentConsents: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val createInternationalScheduledPaymentConsents: Http4sRoute = Http4sRoute { case req @ POST -> `ukV401Prefix` / "pisp" / "international-scheduled-payment-consents" => EndpointHelpers.executeFutureCreated(req)(Future.successful(parseBody(EX_createInternationalScheduledPaymentConsents))) } @@ -8105,7 +8105,7 @@ object Http4sUKOBv401PaymentInitiation extends MdcLoggable { "LastAvailableDateTime": "2020-01-01T00:00:00+00:00" } }""" - lazy val getInternationalScheduledPaymentConsentsConsentId: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getInternationalScheduledPaymentConsentsConsentId: Http4sRoute = Http4sRoute { case req @ GET -> `ukV401Prefix` / "pisp" / "international-scheduled-payment-consents" / consentId => EndpointHelpers.withUser(req) { (u, cc) => Future.successful(parseBody(EX_getInternationalScheduledPaymentConsentsConsentId)) } } @@ -8144,7 +8144,7 @@ object Http4sUKOBv401PaymentInitiation extends MdcLoggable { "LastAvailableDateTime": "2020-01-01T00:00:00+00:00" } }""" - lazy val getInternationalScheduledPaymentConsentsConsentIdFundsConfirmation: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getInternationalScheduledPaymentConsentsConsentIdFundsConfirmation: Http4sRoute = Http4sRoute { case req @ GET -> `ukV401Prefix` / "pisp" / "international-scheduled-payment-consents" / consentId / "funds-confirmation" => EndpointHelpers.withUser(req) { (u, cc) => Future.successful(parseBody(EX_getInternationalScheduledPaymentConsentsConsentIdFundsConfirmation)) } } @@ -8660,7 +8660,7 @@ object Http4sUKOBv401PaymentInitiation extends MdcLoggable { "LastAvailableDateTime": "2020-01-01T00:00:00+00:00" } }""" - lazy val createInternationalScheduledPayments: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val createInternationalScheduledPayments: Http4sRoute = Http4sRoute { case req @ POST -> `ukV401Prefix` / "pisp" / "international-scheduled-payments" => EndpointHelpers.executeFutureCreated(req)(Future.successful(parseBody(EX_createInternationalScheduledPayments))) } @@ -8943,7 +8943,7 @@ object Http4sUKOBv401PaymentInitiation extends MdcLoggable { "LastAvailableDateTime": "2020-01-01T00:00:00+00:00" } }""" - lazy val getInternationalScheduledPaymentsInternationalScheduledPaymentId: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getInternationalScheduledPaymentsInternationalScheduledPaymentId: Http4sRoute = Http4sRoute { case req @ GET -> `ukV401Prefix` / "pisp" / "international-scheduled-payments" / internationalScheduledPaymentId => EndpointHelpers.withUser(req) { (u, cc) => Future.successful(parseBody(EX_getInternationalScheduledPaymentsInternationalScheduledPaymentId)) } } @@ -8990,7 +8990,7 @@ object Http4sUKOBv401PaymentInitiation extends MdcLoggable { "LastAvailableDateTime": "2020-01-01T00:00:00+00:00" } }""" - lazy val getInternationalScheduledPaymentsInternationalScheduledPaymentIdPaymentDetails: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getInternationalScheduledPaymentsInternationalScheduledPaymentIdPaymentDetails: Http4sRoute = Http4sRoute { case req @ GET -> `ukV401Prefix` / "pisp" / "international-scheduled-payments" / internationalScheduledPaymentId / "payment-details" => EndpointHelpers.withUser(req) { (u, cc) => Future.successful(parseBody(EX_getInternationalScheduledPaymentsInternationalScheduledPaymentIdPaymentDetails)) } } @@ -9543,7 +9543,7 @@ object Http4sUKOBv401PaymentInitiation extends MdcLoggable { "LastAvailableDateTime": "2020-01-01T00:00:00+00:00" } }""" - lazy val createInternationalStandingOrderConsents: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val createInternationalStandingOrderConsents: Http4sRoute = Http4sRoute { case req @ POST -> `ukV401Prefix` / "pisp" / "international-standing-order-consents" => EndpointHelpers.executeFutureCreated(req)(Future.successful(parseBody(EX_createInternationalStandingOrderConsents))) } @@ -9849,7 +9849,7 @@ object Http4sUKOBv401PaymentInitiation extends MdcLoggable { "LastAvailableDateTime": "2020-01-01T00:00:00+00:00" } }""" - lazy val getInternationalStandingOrderConsentsConsentId: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getInternationalStandingOrderConsentsConsentId: Http4sRoute = Http4sRoute { case req @ GET -> `ukV401Prefix` / "pisp" / "international-standing-order-consents" / consentId => EndpointHelpers.withUser(req) { (u, cc) => Future.successful(parseBody(EX_getInternationalStandingOrderConsentsConsentId)) } } @@ -10364,7 +10364,7 @@ object Http4sUKOBv401PaymentInitiation extends MdcLoggable { "LastAvailableDateTime": "2020-01-01T00:00:00+00:00" } }""" - lazy val createInternationalStandingOrders: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val createInternationalStandingOrders: Http4sRoute = Http4sRoute { case req @ POST -> `ukV401Prefix` / "pisp" / "international-standing-orders" => EndpointHelpers.executeFutureCreated(req)(Future.successful(parseBody(EX_createInternationalStandingOrders))) } @@ -10642,7 +10642,7 @@ object Http4sUKOBv401PaymentInitiation extends MdcLoggable { "LastAvailableDateTime": "2020-01-01T00:00:00+00:00" } }""" - lazy val getInternationalStandingOrdersInternationalStandingOrderPaymentId: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getInternationalStandingOrdersInternationalStandingOrderPaymentId: Http4sRoute = Http4sRoute { case req @ GET -> `ukV401Prefix` / "pisp" / "international-standing-orders" / internationalStandingOrderPaymentId => EndpointHelpers.withUser(req) { (u, cc) => Future.successful(parseBody(EX_getInternationalStandingOrdersInternationalStandingOrderPaymentId)) } } @@ -10689,7 +10689,7 @@ object Http4sUKOBv401PaymentInitiation extends MdcLoggable { "LastAvailableDateTime": "2020-01-01T00:00:00+00:00" } }""" - lazy val getInternationalStandingOrdersInternationalStandingOrderPaymentIdPaymentDetails: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val getInternationalStandingOrdersInternationalStandingOrderPaymentIdPaymentDetails: Http4sRoute = Http4sRoute { case req @ GET -> `ukV401Prefix` / "pisp" / "international-standing-orders" / internationalStandingOrderPaymentId / "payment-details" => EndpointHelpers.withUser(req) { (u, cc) => Future.successful(parseBody(EX_getInternationalStandingOrdersInternationalStandingOrderPaymentIdPaymentDetails)) } } @@ -10707,47 +10707,49 @@ object Http4sUKOBv401PaymentInitiation extends MdcLoggable { http4sPartialFunction = Some(getInternationalStandingOrdersInternationalStandingOrderPaymentIdPaymentDetails) ) - val routes: HttpRoutes[IO] = Kleisli[HttpF, Request[IO], Response[IO]] { req => - createDomesticPaymentConsents(req) - .orElse(getDomesticPaymentConsentsConsentId(req) - .orElse(getDomesticPaymentConsentsConsentIdFundsConfirmation(req) - .orElse(createDomesticPayments(req) - .orElse(getDomesticPaymentsDomesticPaymentId(req) - .orElse(getDomesticPaymentsDomesticPaymentIdPaymentDetails(req) - .orElse(createDomesticScheduledPaymentConsents(req) - .orElse(getDomesticScheduledPaymentConsentsConsentId(req) - .orElse(createDomesticScheduledPayments(req) - .orElse(getDomesticScheduledPaymentsDomesticScheduledPaymentId(req) - .orElse(getDomesticScheduledPaymentsDomesticScheduledPaymentIdPaymentDetails(req) - .orElse(createDomesticStandingOrderConsents(req) - .orElse(getDomesticStandingOrderConsentsConsentId(req) - .orElse(createDomesticStandingOrders(req) - .orElse(getDomesticStandingOrdersDomesticStandingOrderId(req) - .orElse(getDomesticStandingOrdersDomesticStandingOrderIdPaymentDetails(req) - .orElse(createFilePaymentConsents(req) - .orElse(getFilePaymentConsentsConsentId(req) - .orElse(getFilePaymentConsentsConsentIdFile(req) - .orElse(createFilePaymentConsentsConsentIdFile(req) - .orElse(createFilePayments(req) - .orElse(getFilePaymentsFilePaymentId(req) - .orElse(getFilePaymentsFilePaymentIdPaymentDetails(req) - .orElse(getFilePaymentsFilePaymentIdReportFile(req) - .orElse(createInternationalPaymentConsents(req) - .orElse(getInternationalPaymentConsentsConsentId(req) - .orElse(getInternationalPaymentConsentsConsentIdFundsConfirmation(req) - .orElse(createInternationalPayments(req) - .orElse(getInternationalPaymentsInternationalPaymentId(req) - .orElse(getInternationalPaymentsInternationalPaymentIdPaymentDetails(req) - .orElse(createInternationalScheduledPaymentConsents(req) - .orElse(getInternationalScheduledPaymentConsentsConsentId(req) - .orElse(getInternationalScheduledPaymentConsentsConsentIdFundsConfirmation(req) - .orElse(createInternationalScheduledPayments(req) - .orElse(getInternationalScheduledPaymentsInternationalScheduledPaymentId(req) - .orElse(getInternationalScheduledPaymentsInternationalScheduledPaymentIdPaymentDetails(req) - .orElse(createInternationalStandingOrderConsents(req) - .orElse(getInternationalStandingOrderConsentsConsentId(req) - .orElse(createInternationalStandingOrders(req) - .orElse(getInternationalStandingOrdersInternationalStandingOrderPaymentId(req) - .orElse(getInternationalStandingOrdersInternationalStandingOrderPaymentIdPaymentDetails(req))))))))))))))))))))))))))))))))))))))))) - } + lazy val routesInOrder: List[Http4sHandler] = List( + createDomesticPaymentConsents, + getDomesticPaymentConsentsConsentId, + getDomesticPaymentConsentsConsentIdFundsConfirmation, + createDomesticPayments, + getDomesticPaymentsDomesticPaymentId, + getDomesticPaymentsDomesticPaymentIdPaymentDetails, + createDomesticScheduledPaymentConsents, + getDomesticScheduledPaymentConsentsConsentId, + createDomesticScheduledPayments, + getDomesticScheduledPaymentsDomesticScheduledPaymentId, + getDomesticScheduledPaymentsDomesticScheduledPaymentIdPaymentDetails, + createDomesticStandingOrderConsents, + getDomesticStandingOrderConsentsConsentId, + createDomesticStandingOrders, + getDomesticStandingOrdersDomesticStandingOrderId, + getDomesticStandingOrdersDomesticStandingOrderIdPaymentDetails, + createFilePaymentConsents, + getFilePaymentConsentsConsentId, + getFilePaymentConsentsConsentIdFile, + createFilePaymentConsentsConsentIdFile, + createFilePayments, + getFilePaymentsFilePaymentId, + getFilePaymentsFilePaymentIdPaymentDetails, + getFilePaymentsFilePaymentIdReportFile, + createInternationalPaymentConsents, + getInternationalPaymentConsentsConsentId, + getInternationalPaymentConsentsConsentIdFundsConfirmation, + createInternationalPayments, + getInternationalPaymentsInternationalPaymentId, + getInternationalPaymentsInternationalPaymentIdPaymentDetails, + createInternationalScheduledPaymentConsents, + getInternationalScheduledPaymentConsentsConsentId, + getInternationalScheduledPaymentConsentsConsentIdFundsConfirmation, + createInternationalScheduledPayments, + getInternationalScheduledPaymentsInternationalScheduledPaymentId, + getInternationalScheduledPaymentsInternationalScheduledPaymentIdPaymentDetails, + createInternationalStandingOrderConsents, + getInternationalStandingOrderConsentsConsentId, + createInternationalStandingOrders, + getInternationalStandingOrdersInternationalStandingOrderPaymentId, + getInternationalStandingOrdersInternationalStandingOrderPaymentIdPaymentDetails + ) + + lazy val routes: HttpRoutes[IO] = Http4sRoute.chain(routesInOrder) } diff --git a/obp-api/src/main/scala/code/api/UKOpenBanking/v4_0_1/Http4sUKOBv401Vrp.scala b/obp-api/src/main/scala/code/api/UKOpenBanking/v4_0_1/Http4sUKOBv401Vrp.scala index a85fc33719..598a469b33 100644 --- a/obp-api/src/main/scala/code/api/UKOpenBanking/v4_0_1/Http4sUKOBv401Vrp.scala +++ b/obp-api/src/main/scala/code/api/UKOpenBanking/v4_0_1/Http4sUKOBv401Vrp.scala @@ -29,7 +29,7 @@ package code.api.UKOpenBanking.v4_0_1 import cats.data.{Kleisli, OptionT} import cats.effect.IO -import code.api.util.APIUtil.{EmptyBody, ResourceDoc} +import code.api.util.APIUtil.{Http4sHandler, Http4sRoute, EmptyBody, ResourceDoc} import code.api.util.ApiTag import code.api.util.CustomJsonFormats import code.api.util.ErrorMessages.{AuthenticatedUserIsRequired, UnknownError} @@ -502,7 +502,7 @@ object Http4sUKOBv401Vrp extends MdcLoggable { }, "Meta": {} }""" - lazy val domesticVrpConsentsPost: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val domesticVrpConsentsPost: Http4sRoute = Http4sRoute { case req @ POST -> `ukV401Prefix` / "pisp" / "domestic-vrp-consents" => EndpointHelpers.executeFutureCreated(req)(Future.successful(parseBody(EX_domesticVrpConsentsPost))) } @@ -757,7 +757,7 @@ object Http4sUKOBv401Vrp extends MdcLoggable { }, "Meta": {} }""" - lazy val domesticVrpConsentsGet: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val domesticVrpConsentsGet: Http4sRoute = Http4sRoute { case req @ GET -> `ukV401Prefix` / "pisp" / "domestic-vrp-consents" / consentId => EndpointHelpers.withUser(req) { (u, cc) => Future.successful(parseBody(EX_domesticVrpConsentsGet)) } } @@ -1219,7 +1219,7 @@ object Http4sUKOBv401Vrp extends MdcLoggable { }, "Meta": {} }""" - lazy val domesticVrpConsentsPut: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val domesticVrpConsentsPut: Http4sRoute = Http4sRoute { case req @ PUT -> `ukV401Prefix` / "pisp" / "domestic-vrp-consents" / consentId => EndpointHelpers.executeFutureCreated(req)(Future.successful(parseBody(EX_domesticVrpConsentsPut))) } @@ -1240,7 +1240,7 @@ This endpoint **must** only be used for the migration of Domestic VRP Consent re ) private val EX_domesticVrpConsentsDelete: String = """{}""" - lazy val domesticVrpConsentsDelete: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val domesticVrpConsentsDelete: Http4sRoute = Http4sRoute { case req @ DELETE -> `ukV401Prefix` / "pisp" / "domestic-vrp-consents" / consentId => EndpointHelpers.executeDelete(req) { cc => Future.successful(()) } } @@ -1495,7 +1495,7 @@ This endpoint **must** only be used for the migration of Domestic VRP Consent re }, "Meta": {} }""" - lazy val domesticVrpConsentsPatch: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val domesticVrpConsentsPatch: Http4sRoute = Http4sRoute { case req @ PATCH -> `ukV401Prefix` / "pisp" / "domestic-vrp-consents" / consentId => EndpointHelpers.executeFutureCreated(req)(Future.successful(parseBody(EX_domesticVrpConsentsPatch))) } @@ -1541,7 +1541,7 @@ This endpoint **must** only be used for the migration of Domestic VRP Consent re } } }""" - lazy val domesticVrpConsentsFundsConfirmation: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val domesticVrpConsentsFundsConfirmation: Http4sRoute = Http4sRoute { case req @ POST -> `ukV401Prefix` / "pisp" / "domestic-vrp-consents" / consentId / "funds-confirmation" => EndpointHelpers.executeFutureCreated(req)(Future.successful(parseBody(EX_domesticVrpConsentsFundsConfirmation))) } @@ -2164,7 +2164,7 @@ This endpoint **must** only be used for the migration of Domestic VRP Consent re }, "Meta": {} }""" - lazy val domesticVrpPost: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val domesticVrpPost: Http4sRoute = Http4sRoute { case req @ POST -> `ukV401Prefix` / "pisp" / "domestic-vrps" => EndpointHelpers.executeFutureCreated(req)(Future.successful(parseBody(EX_domesticVrpPost))) } @@ -2508,7 +2508,7 @@ This endpoint **must** only be used for the migration of Domestic VRP Consent re }, "Meta": {} }""" - lazy val domesticVrpGet: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val domesticVrpGet: Http4sRoute = Http4sRoute { case req @ GET -> `ukV401Prefix` / "pisp" / "domestic-vrps" / domesticVRPId => EndpointHelpers.withUser(req) { (u, cc) => Future.successful(parseBody(EX_domesticVrpGet)) } } @@ -2543,7 +2543,7 @@ This endpoint **must** only be used for the migration of Domestic VRP Consent re ] } }""" - lazy val domesticVrpPaymentDetailsGet: HttpRoutes[IO] = HttpRoutes.of[IO] { + lazy val domesticVrpPaymentDetailsGet: Http4sRoute = Http4sRoute { case req @ GET -> `ukV401Prefix` / "pisp" / "domestic-vrps" / domesticVRPId / "payment-details" => EndpointHelpers.withUser(req) { (u, cc) => Future.successful(parseBody(EX_domesticVrpPaymentDetailsGet)) } } @@ -2561,15 +2561,17 @@ This endpoint **must** only be used for the migration of Domestic VRP Consent re http4sPartialFunction = Some(domesticVrpPaymentDetailsGet) ) - val routes: HttpRoutes[IO] = Kleisli[HttpF, Request[IO], Response[IO]] { req => - domesticVrpConsentsPost(req) - .orElse(domesticVrpConsentsGet(req) - .orElse(domesticVrpConsentsPut(req) - .orElse(domesticVrpConsentsDelete(req) - .orElse(domesticVrpConsentsPatch(req) - .orElse(domesticVrpConsentsFundsConfirmation(req) - .orElse(domesticVrpPost(req) - .orElse(domesticVrpGet(req) - .orElse(domesticVrpPaymentDetailsGet(req))))))))) - } + lazy val routesInOrder: List[Http4sHandler] = List( + domesticVrpConsentsPost, + domesticVrpConsentsGet, + domesticVrpConsentsPut, + domesticVrpConsentsDelete, + domesticVrpConsentsPatch, + domesticVrpConsentsFundsConfirmation, + domesticVrpPost, + domesticVrpGet, + domesticVrpPaymentDetailsGet + ) + + lazy val routes: HttpRoutes[IO] = Http4sRoute.chain(routesInOrder) } From 0dee96eb03d283aa221c171505878772aaf380a1 Mon Sep 17 00:00:00 2001 From: Hongwei Date: Wed, 7 Oct 2026 17:29:59 +0200 Subject: [PATCH 19/29] test: check every ResourceDoc catalog is bound to its routes, and select real URLs ResourceDocRouteBindingTest covers all 18 catalogs (v1.2.1 to v7.0.0, Berlin Group v1.3 and v2, UK Open Banking v2.0.0, v3.1.0, v4.0.1): each doc's own route must serve its own URL and selection must return that doc. ResourceDocRouteOrderTest covers orderByRoutes and Http4sRoute.chain. ResourceDocRealCatalogSelectionTest selects real URLs that used to be given another endpoint's doc: the v4.0.0 and v6.0.0 transaction-request types, the types v7.0.0 has no route for, and the Berlin Group signing-basket status and authorisations. --- .../ResourceDocRealCatalogSelectionTest.scala | 123 +++++++++++++++ .../http4s/ResourceDocRouteBindingTest.scala | 42 ++++- .../http4s/ResourceDocRouteOrderTest.scala | 147 ++++++++++++++++++ 3 files changed, 308 insertions(+), 4 deletions(-) create mode 100644 obp-api/src/test/scala/code/api/util/http4s/ResourceDocRealCatalogSelectionTest.scala create mode 100644 obp-api/src/test/scala/code/api/util/http4s/ResourceDocRouteOrderTest.scala diff --git a/obp-api/src/test/scala/code/api/util/http4s/ResourceDocRealCatalogSelectionTest.scala b/obp-api/src/test/scala/code/api/util/http4s/ResourceDocRealCatalogSelectionTest.scala new file mode 100644 index 0000000000..b4b1bf6a20 --- /dev/null +++ b/obp-api/src/test/scala/code/api/util/http4s/ResourceDocRealCatalogSelectionTest.scala @@ -0,0 +1,123 @@ +/** +Open Bank Project - API +Copyright (C) 2011-2026, TESOBE GmbH. + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . + +Email: contact@tesobe.com +TESOBE GmbH. +Osloer Strasse 16/17 +Berlin 13359, Germany + +This product includes software developed at +TESOBE (http://www.tesobe.com/) + + */ + +package code.api.util.http4s + +import cats.effect.IO +import code.api.util.APIUtil.ResourceDoc +import code.setup.ServerSetup +import org.http4s.{Method, Request, Uri} +import org.scalatest.Tag + +import scala.collection.mutable.ArrayBuffer + +/** + * Selecting the doc for real request URLs against the real catalogs. + * + * These are the requests that used to be given another endpoint's doc, because the matcher read + * a fixed word written in capitals (a transaction-request type) as a placeholder, or a template made + * only of placeholders as a wildcard. Each now gets the doc of the route that serves it. + */ +class ResourceDocRealCatalogSelectionTest extends ServerSetup { + + object ResourceDocRealCatalogSelectionTag extends Tag("ResourceDocRealCatalogSelection") + + private def select(docs: ArrayBuffer[ResourceDoc], method: Method, path: String): Option[String] = + ResourceDocMatcher.selectByRoute(Request[IO](method, Uri.unsafeFromString(path)), docs).map(_.partialFunctionName) + + private val v4 = code.api.v4_0_0.Http4s400.Implementations4_0_0.orderedResourceDocs + private val v6 = code.api.v6_0_0.Http4s600.Implementations6_0_0.orderedResourceDocs + private val v7 = code.api.v7_0_0.Http4s700.Implementations7_0_0.orderedResourceDocs + private val bg13 = code.api.berlin.group.v1_3.Http4sBGv13.orderedResourceDocs + + private def transactionRequest(version: String, viewId: String, typeName: String) = + s"/obp/$version/banks/gh.29.uk/accounts/8ca8a7e4/$viewId/transaction-request-types/$typeName/transaction-requests" + + feature("v4.0.0: every transaction-request type gets its own doc") { + Map( + "ACCOUNT" -> "createTransactionRequestAccount", + "ACCOUNT_OTP" -> "createTransactionRequestAccountOtp", + "SEPA" -> "createTransactionRequestSepa", + "COUNTERPARTY" -> "createTransactionRequestCounterparty", + "REFUND" -> "createTransactionRequestRefund", + "FREE_FORM" -> "createTransactionRequestFreeForm", + "SIMPLE" -> "createTransactionRequestSimple", + "AGENT_CASH_WITHDRAWAL" -> "createTransactionRequestAgentCashWithDrawal" + ).foreach { case (typeName, docName) => + scenario(s"$typeName is validated under $docName", ResourceDocRealCatalogSelectionTag) { + select(v4, Method.POST, transactionRequest("v4.0.0", "owner", typeName)) shouldBe Some(docName) + } + } + + scenario("A type with no doc of its own is validated under the first doc of the shared route", ResourceDocRealCatalogSelectionTag) { + select(v4, Method.POST, transactionRequest("v4.0.0", "owner", "HOLD")) shouldBe Some("createTransactionRequestAccount") + } + } + + feature("v6.0.0: HOLD does not claim the other types") { + Map( + "HOLD" -> "createTransactionRequestHold", + "CARDANO" -> "createTransactionRequestCardano", + "ETH_SEND_TRANSACTION" -> "createTransactionRequestEthereumeSendTransaction", + "ETH_SEND_RAW_TRANSACTION" -> "createTransactionRequestEthSendRawTransaction" + ).foreach { case (typeName, docName) => + scenario(s"$typeName is validated under $docName", ResourceDocRealCatalogSelectionTag) { + select(v6, Method.POST, transactionRequest("v6.0.0", "owner", typeName)) shouldBe Some(docName) + } + } + } + + feature("v7.0.0: a type v7 has no route for is not claimed by another type's doc") { + Seq("HOLD", "CARDANO", "ETH_SEND_TRANSACTION", "ETH_SEND_RAW_TRANSACTION").foreach { typeName => + scenario(s"$typeName selects no v7 doc, so the request can fall through to v6", ResourceDocRealCatalogSelectionTag) { + select(v7, Method.POST, transactionRequest("v7.0.0", "owner", typeName)) shouldBe None + } + } + + scenario("UTILITY, BULK and OPEN_CORRIDOR_PROMISE are not claimed by MOBILE_WALLET", ResourceDocRealCatalogSelectionTag) { + val mobileWallet = select(v7, Method.POST, transactionRequest("v7.0.0", "owner", "MOBILE_WALLET")) + mobileWallet.map(_.contains("MobileWallet")) shouldBe Some(true) + Seq("UTILITY", "BULK", "OPEN_CORRIDOR_PROMISE").foreach { typeName => + select(v7, Method.POST, transactionRequest("v7.0.0", "owner", typeName)) should not be mobileWallet + } + } + } + + feature("Berlin Group v1.3: a template of placeholders does not claim other URLs") { + scenario("a signing basket's status is its own endpoint, not getPaymentInformation", ResourceDocRealCatalogSelectionTag) { + select(bg13, Method.GET, "/berlin-group/v1.3/signing-baskets/BASKET1/status") shouldBe Some("getSigningBasketStatus") + } + + scenario("a signing basket's authorisations are their own endpoint, not getPaymentInformation", ResourceDocRealCatalogSelectionTag) { + select(bg13, Method.GET, "/berlin-group/v1.3/signing-baskets/BASKET1/authorisations") shouldBe Some("getSigningBasketAuthorisation") + } + + scenario("a payment of a real service and product is getPaymentInformation", ResourceDocRealCatalogSelectionTag) { + select(bg13, Method.GET, "/berlin-group/v1.3/payments/sepa-credit-transfers/PAYMENT1") shouldBe Some("getPaymentInformation") + } + } +} diff --git a/obp-api/src/test/scala/code/api/util/http4s/ResourceDocRouteBindingTest.scala b/obp-api/src/test/scala/code/api/util/http4s/ResourceDocRouteBindingTest.scala index 1d8e3d512d..fed0322bc2 100644 --- a/obp-api/src/test/scala/code/api/util/http4s/ResourceDocRouteBindingTest.scala +++ b/obp-api/src/test/scala/code/api/util/http4s/ResourceDocRouteBindingTest.scala @@ -56,17 +56,51 @@ class ResourceDocRouteBindingTest extends ServerSetup { * registration order could pass while production picks another doc. */ private def convertedCatalogs: List[(String, ArrayBuffer[ResourceDoc])] = List( - "v7.0.0" -> code.api.v7_0_0.Http4s700.Implementations7_0_0.orderedResourceDocs + "v1.2.1" -> code.api.v1_2_1.Http4s121.Implementations1_2_1.orderedResourceDocs, + "v1.3.0" -> code.api.v1_3_0.Http4s130.Implementations1_3_0.orderedResourceDocs, + "v1.4.0" -> code.api.v1_4_0.Http4s140.Implementations1_4_0.orderedResourceDocs, + "v2.0.0" -> code.api.v2_0_0.Http4s200.Implementations2_0_0.orderedResourceDocs, + "v2.1.0" -> code.api.v2_1_0.Http4s210.Implementations2_1_0.orderedResourceDocs, + "v2.2.0" -> code.api.v2_2_0.Http4s220.Implementations2_2_0.orderedResourceDocs, + "v3.0.0" -> code.api.v3_0_0.Http4s300.Implementations3_0_0.orderedResourceDocs, + "v3.1.0" -> code.api.v3_1_0.Http4s310.Implementations3_1_0.orderedResourceDocs, + "v4.0.0" -> code.api.v4_0_0.Http4s400.Implementations4_0_0.orderedResourceDocs, + "v5.0.0" -> code.api.v5_0_0.Http4s500.Implementations5_0_0.orderedResourceDocs, + "v5.1.0" -> code.api.v5_1_0.Http4s510.Implementations5_1_0.orderedResourceDocs, + "v6.0.0" -> code.api.v6_0_0.Http4s600.Implementations6_0_0.orderedResourceDocs, + "v7.0.0" -> code.api.v7_0_0.Http4s700.Implementations7_0_0.orderedResourceDocs, + "Berlin Group v1.3" -> code.api.berlin.group.v1_3.Http4sBGv13.orderedResourceDocs, + "Berlin Group v2" -> code.api.berlin.group.v2.Http4sBGv2.orderedResourceDocs, + "UK Open Banking v2.0.0" -> code.api.UKOpenBanking.v2_0_0.Http4sUKOBv200.orderedResourceDocs, + "UK Open Banking v3.1.0" -> code.api.UKOpenBanking.v3_1_0.Http4sUKOBv310.orderedResourceDocs, + "UK Open Banking v4.0.1" -> code.api.UKOpenBanking.v4_0_1.Http4sUKOBv401.orderedResourceDocs ) private def describe(doc: ResourceDoc): String = s"${doc.partialFunctionName} (${doc.requestVerb} ${doc.requestUrl})" + /** + * Docs whose route serves a longer or narrower URL than the template documents, or accepts only some + * values of a segment, so the template text itself is not a request the route serves. The documented + * URL is kept as it is (resource-docs and Swagger show it); this gives the URL to test instead. + */ + private val sampleUrlOf: Map[String, String] = Map( + // Lift documented /search/warehouse, and served /search/warehouse/{query} + "elasticSearchWarehouse" -> "/search/warehouse/QUERY", + "elasticSearchMetrics" -> "/search/metrics/QUERY", + // (the Berlin Group payment routes accept only real payment services and products, see below) + ) + private def requestFor(doc: ResourceDoc): Request[IO] = { val version = doc.implementedInApiVersion + // Berlin Group payment routes accept only the values of PaymentServiceTypes and TransactionRequestTypes + // in their first two segments, so the template's placeholders are given real values. + val url = sampleUrlOf.getOrElse( + doc.partialFunctionName, + doc.requestUrl.replace("/PAYMENT_SERVICE/", "/payments/").replace("/PAYMENT_PRODUCT/", "/sepa-credit-transfers/")) Request[IO]( Method.fromString(doc.requestVerb.toUpperCase).fold(throw _, identity), - Uri.unsafeFromString(s"/${version.urlPrefix}/$version${doc.requestUrl}") + Uri.unsafeFromString(s"/${version.urlPrefix}/${version.apiShortVersion}$url") ) } @@ -74,8 +108,8 @@ class ResourceDocRouteBindingTest extends ServerSetup { private def unbound(docs: ArrayBuffer[ResourceDoc]): List[String] = { val sameVerbAndUrlCount = docs.groupBy(doc => (doc.requestVerb.toUpperCase, doc.requestUrl)).view.mapValues(_.size).toMap docs.toList.flatMap { doc => - doc.http4sPartialFunction.flatMap(_.route) match { - case None => Some(s"${describe(doc)} carries no Http4sRoute") + doc.http4sPartialFunction.flatMap(h => Option(h)).flatMap(_.route) match { + case None => Some(s"${describe(doc)} carries no Http4sRoute (or its route val is declared after the resourceDocs += line)") case Some(route) => val req = requestFor(doc) if (!route.isDefinedAt(req)) Some(s"${describe(doc)}: its own route does not serve its own URL") diff --git a/obp-api/src/test/scala/code/api/util/http4s/ResourceDocRouteOrderTest.scala b/obp-api/src/test/scala/code/api/util/http4s/ResourceDocRouteOrderTest.scala new file mode 100644 index 0000000000..b3a94d7ad7 --- /dev/null +++ b/obp-api/src/test/scala/code/api/util/http4s/ResourceDocRouteOrderTest.scala @@ -0,0 +1,147 @@ +/** +Open Bank Project - API +Copyright (C) 2011-2026, TESOBE GmbH. + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . + +Email: contact@tesobe.com +TESOBE GmbH. +Osloer Strasse 16/17 +Berlin 13359, Germany + +This product includes software developed at +TESOBE (http://www.tesobe.com/) + + */ + +package code.api.util.http4s + +import cats.effect.IO +import code.api.util.APIUtil.{Http4sHandler, Http4sRoute, ResourceDoc} +import code.api.util.ApiTag.ResourceDocTag +import com.openbankproject.commons.util.ApiVersion +import org.http4s._ +import org.http4s.dsl.io._ +import org.json4s.JsonAST.JObject +import org.scalatest.{FeatureSpec, GivenWhenThen, Matchers, Tag} + +/** + * Unit tests for ResourceDocMatcher.orderByRoutes and Http4sRoute.chain. + * + * The middleware selects the doc of the first route that serves a request, so the docs it is given + * must be in the order the routes are tried in. orderByRoutes derives that order from the route chain. + */ +class ResourceDocRouteOrderTest extends FeatureSpec with Matchers with GivenWhenThen { + + object ResourceDocRouteOrderTag extends Tag("ResourceDocRouteOrder") + + private def route(segment: String): Http4sRoute = + Http4sRoute { case GET -> Root / "obp" / "v7.0.0" / `segment` => IO.pure(Response[IO]()) } + + private def doc(name: String, handler: Option[Http4sHandler]): ResourceDoc = + ResourceDoc( + implementedInApiVersion = ApiVersion.v7_0_0, + partialFunctionName = name, + requestVerb = "GET", + requestUrl = s"/$name", + summary = "Test endpoint", + description = "Test description", + exampleRequestBody = JObject(Nil), + successResponseBody = JObject(Nil), + errorResponseBodies = List.empty, + tags = List(ResourceDocTag("test")), + roles = None, + http4sPartialFunction = handler + ) + + feature("ResourceDocMatcher.orderByRoutes - the docs in the order their routes are tried") { + + scenario("Docs follow the route chain, whatever order they were registered in", ResourceDocRouteOrderTag) { + val (a, b, c) = (route("a"), route("b"), route("c")) + val registered = List(doc("c", Some(c)), doc("a", Some(a)), doc("b", Some(b))) + + val ordered = ResourceDocMatcher.orderByRoutes(registered, List(a, b, c)) + + ordered.map(_.partialFunctionName) shouldBe List("a", "b", "c") + } + + scenario("Docs that share a route stay together, in registration order", ResourceDocRouteOrderTag) { + val (a, b) = (route("a"), route("b")) + val registered = List(doc("b1", Some(b)), doc("a", Some(a)), doc("b2", Some(b))) + + val ordered = ResourceDocMatcher.orderByRoutes(registered, List(a, b)) + + ordered.map(_.partialFunctionName) shouldBe List("a", "b1", "b2") + } + + scenario("An alias of a route is the same route", ResourceDocRouteOrderTag) { + val (a, b) = (route("a"), route("b")) + val aliasOfB: Http4sRoute = b + val registered = List(doc("viaAlias", Some(aliasOfB)), doc("a", Some(a))) + + val ordered = ResourceDocMatcher.orderByRoutes(registered, List(a, b)) + + ordered.map(_.partialFunctionName) shouldBe List("a", "viaAlias") + } + + scenario("A doc whose route is deliberately outside the chain is left out", ResourceDocRouteOrderTag) { + val (a, outside) = (route("a"), route("outside")) + val registered = List(doc("outside", Some(outside)), doc("a", Some(a))) + + val ordered = ResourceDocMatcher.orderByRoutes(registered, List(a), outsideTheChain = List(outside)) + + ordered.map(_.partialFunctionName) shouldBe List("a") + } + + scenario("A doc whose route is missing from the chain is an error naming the doc", ResourceDocRouteOrderTag) { + val (a, forgotten) = (route("a"), route("forgotten")) + val registered = List(doc("a", Some(a)), doc("forgotten", Some(forgotten))) + + val thrown = the[IllegalStateException] thrownBy ResourceDocMatcher.orderByRoutes(registered, List(a)) + + thrown.getMessage should include("forgotten") + thrown.getMessage should not include "(GET /a)" + } + + scenario("A doc with no route documents an endpoint served elsewhere and is left out", ResourceDocRouteOrderTag) { + val a = route("a") + val registered = List(doc("noRoute", None), doc("a", Some(a))) + + val ordered = ResourceDocMatcher.orderByRoutes(registered, List(a)) + + ordered.map(_.partialFunctionName) shouldBe List("a") + } + } + + feature("Http4sRoute.chain - the first route that serves a request answers it") { + + scenario("Routes are tried in the order given", ResourceDocRouteOrderTag) { + import cats.effect.unsafe.implicits.global + val first = Http4sRoute { case GET -> Root / "x" => IO.pure(Response[IO](Status.Ok)) } + val second = Http4sRoute { case GET -> Root / "x" => IO.pure(Response[IO](Status.Accepted)) } + val req = Request[IO](Method.GET, Uri.unsafeFromString("/x")) + + Http4sRoute.chain(List(first, second)).run(req).value.unsafeRunSync().map(_.status) shouldBe Some(Status.Ok) + Http4sRoute.chain(List(second, first)).run(req).value.unsafeRunSync().map(_.status) shouldBe Some(Status.Accepted) + } + + scenario("A request no route serves is not answered", ResourceDocRouteOrderTag) { + import cats.effect.unsafe.implicits.global + val only = Http4sRoute { case GET -> Root / "x" => IO.pure(Response[IO]()) } + val req = Request[IO](Method.GET, Uri.unsafeFromString("/y")) + + Http4sRoute.chain(List(only)).run(req).value.unsafeRunSync() shouldBe None + } + } +} From 228289854989ad074b71f8ede4619471e019c2ad Mon Sep 17 00:00:00 2001 From: Hongwei Date: Wed, 7 Oct 2026 17:46:18 +0200 Subject: [PATCH 20/29] chore: record the new v4.0.0 createTransactionRequestAccount URL in the parity allowlist The doc now documents the ACCOUNT URL of the Lift baseline; only the VIEW_ID to GRANT_VIEW_ID difference remains, and its digest is recomputed with allowlist_helper.py. --- .../http4s/ResourceDocSelfResolveTest.scala | 119 ------------------ .../parity_allowlist.json | 4 +- 2 files changed, 2 insertions(+), 121 deletions(-) delete mode 100644 obp-api/src/test/scala/code/api/util/http4s/ResourceDocSelfResolveTest.scala diff --git a/obp-api/src/test/scala/code/api/util/http4s/ResourceDocSelfResolveTest.scala b/obp-api/src/test/scala/code/api/util/http4s/ResourceDocSelfResolveTest.scala deleted file mode 100644 index e35016df93..0000000000 --- a/obp-api/src/test/scala/code/api/util/http4s/ResourceDocSelfResolveTest.scala +++ /dev/null @@ -1,119 +0,0 @@ -/** -Open Bank Project - API -Copyright (C) 2011-2026, TESOBE GmbH. - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . - -Email: contact@tesobe.com -TESOBE GmbH. -Osloer Strasse 16/17 -Berlin 13359, Germany - -This product includes software developed at -TESOBE (http://www.tesobe.com/) - - */ - -package code.api.util.http4s - -import code.api.util.APIUtil.ResourceDoc -import code.setup.ServerSetup -import org.http4s.Uri -import org.scalatest.Tag - -import scala.collection.mutable.ArrayBuffer - -/** - * This test checks that every registered ResourceDoc is the doc the middleware picks for - * a request to that doc's own URL. - * - * ResourceDocMiddleware does not know which http4s route will serve a request: it re-matches - * the URL against the ResourceDoc templates and takes the doc it finds for roles, the login - * requirement, the bank/account/view lookups, the operationId (telemetry, enable/disable, - * JSON-schema and auth-type validation), Force-Error and currency upper-casing. If one - * template also matches another doc's URL and wins, the other endpoint is validated and - * reported under the wrong doc while its own handler still runs, so its own tests pass. - * That happened with `.../transaction-request-types/MOBILE_WALLET/transaction-requests` in - * v7.0.0, which matched UTILITY, BULK and OPEN_CORRIDOR_PROMISE requests, and with HOLD in - * v6.0.0, which matched CARDANO and Ethereum requests. - * - * For each version's middleware catalog, the test sends the doc's template itself as the - * request path (placeholders such as `BANK_ID` stand in for values, and every template - * matches them) and asserts the matcher returns that same doc. Docs that share a verb and - * URL with another doc in the same catalog are skipped, as no matcher could tell them apart. - */ -class ResourceDocSelfResolveTest extends ServerSetup { - - object ResourceDocSelfResolveTag extends Tag("ResourceDocSelfResolve") - - /** - * Each OBP version's `resourceDocs` buffer is filled when its nested `Implementations` object - * initialises, which normally waits for the first request. Naming the object here forces that. - */ - private def loaded(implementations: Any, docs: ArrayBuffer[ResourceDoc]): ArrayBuffer[ResourceDoc] = docs - - /** Each catalog is the `resourceDocs` buffer a version passes to `ResourceDocMiddleware.apply`. */ - private def catalogs: List[(String, ArrayBuffer[ResourceDoc])] = List( - "v1.2.1" -> loaded(code.api.v1_2_1.Http4s121.Implementations1_2_1, code.api.v1_2_1.Http4s121.resourceDocs), - "v1.3.0" -> loaded(code.api.v1_3_0.Http4s130.Implementations1_3_0, code.api.v1_3_0.Http4s130.resourceDocs), - "v1.4.0" -> loaded(code.api.v1_4_0.Http4s140.Implementations1_4_0, code.api.v1_4_0.Http4s140.resourceDocs), - "v2.0.0" -> loaded(code.api.v2_0_0.Http4s200.Implementations2_0_0, code.api.v2_0_0.Http4s200.resourceDocs), - "v2.1.0" -> loaded(code.api.v2_1_0.Http4s210.Implementations2_1_0, code.api.v2_1_0.Http4s210.resourceDocs), - "v2.2.0" -> loaded(code.api.v2_2_0.Http4s220.Implementations2_2_0, code.api.v2_2_0.Http4s220.resourceDocs), - "v3.0.0" -> loaded(code.api.v3_0_0.Http4s300.Implementations3_0_0, code.api.v3_0_0.Http4s300.resourceDocs), - "v3.1.0" -> loaded(code.api.v3_1_0.Http4s310.Implementations3_1_0, code.api.v3_1_0.Http4s310.resourceDocs), - "v4.0.0" -> loaded(code.api.v4_0_0.Http4s400.Implementations4_0_0, code.api.v4_0_0.Http4s400.resourceDocs), - "v5.0.0" -> loaded(code.api.v5_0_0.Http4s500.Implementations5_0_0, code.api.v5_0_0.Http4s500.resourceDocs), - "v5.1.0" -> loaded(code.api.v5_1_0.Http4s510.Implementations5_1_0, code.api.v5_1_0.Http4s510.resourceDocs), - "v6.0.0" -> loaded(code.api.v6_0_0.Http4s600.Implementations6_0_0, code.api.v6_0_0.Http4s600.resourceDocs), - "v7.0.0" -> loaded(code.api.v7_0_0.Http4s700.Implementations7_0_0, code.api.v7_0_0.Http4s700.resourceDocs), - "Berlin Group v1.3" -> code.api.berlin.group.v1_3.Http4sBGv13.resourceDocs, - "Berlin Group v2" -> code.api.berlin.group.v2.Http4sBGv2.resourceDocs, - "UK Open Banking v2.0.0" -> code.api.UKOpenBanking.v2_0_0.Http4sUKOBv200.resourceDocs, - "UK Open Banking v3.1.0" -> code.api.UKOpenBanking.v3_1_0.Http4sUKOBv310.resourceDocs, - "UK Open Banking v4.0.1" -> code.api.UKOpenBanking.v4_0_1.Http4sUKOBv401.resourceDocs - ) - - private def describe(doc: ResourceDoc): String = - s"${doc.partialFunctionName} (${doc.requestVerb} ${doc.requestUrl})" - - /** Returns one line per doc whose own URL resolves to a different doc, or to none. */ - private def misresolved(docs: ArrayBuffer[ResourceDoc]): List[String] = { - val index = ResourceDocMatcher.buildIndex(docs) - val sameVerbAndUrlCount = docs.groupBy(doc => (doc.requestVerb.toUpperCase, doc.requestUrl)).mapValues(_.size) - docs.toList - .filter(doc => sameVerbAndUrlCount((doc.requestVerb.toUpperCase, doc.requestUrl)) == 1) - .flatMap { doc => - val version = doc.implementedInApiVersion - val path = Uri.Path.unsafeFromString(s"/${version.urlPrefix}/${version}${doc.requestUrl}") - ResourceDocMatcher.findResourceDoc(doc.requestVerb, path, index) match { - case Some(resolved) if resolved eq doc => None - case Some(resolved) => Some(s"${describe(doc)} resolves to ${describe(resolved)}") - case None => Some(s"${describe(doc)} resolves to no doc") - } - } - } - - feature("Every ResourceDoc is the doc the middleware picks for its own URL") { - catalogs.foreach { case (label, docs) => - scenario(s"$label: each doc's own URL resolves to that doc", ResourceDocSelfResolveTag) { - docs should not be empty - val problems = misresolved(docs) - withClue(s"\n${problems.mkString("\n")}\n") { - problems shouldBe empty - } - } - } - } -} diff --git a/scripts/resource_doc_baseline/parity_allowlist.json b/scripts/resource_doc_baseline/parity_allowlist.json index e81526abd5..5b239b260f 100644 --- a/scripts/resource_doc_baseline/parity_allowlist.json +++ b/scripts/resource_doc_baseline/parity_allowlist.json @@ -679,9 +679,9 @@ "version": "v4_0_0", "endpoint": "createTransactionRequestAccount", "field": "requestUrl", - "reason": "Literal \"ACCOUNT\" segment generalized to a TRANSACTION_REQUEST_TYPE wildcard + GRANT_VIEW_ID: a single generic http4s handler now serves what were 9 separate per-type Lift doc aliases (confirmed by a matching code comment).", + "reason": "VIEW_ID -> GRANT_VIEW_ID: checkAuthorisationToCreateTransactionRequest handles the view/role authorisation logic internally, matching this repo's documented bypass convention. The literal ACCOUNT segment is as in the Lift baseline.", "lift_digest": "f05f20b524d4f6546c826d4c69d9e40f785c624e77956c9fb0389160a03054ca", - "http4s_digest": "dc93a9444e21bd6540d25ed64b94db928463b2cb6468129c8dd92b8316f53886" + "http4s_digest": "68051da298bb3e52e38bd934987adce04a7f40bcb5bb9f236344eb02010b892b" }, { "version": "v4_0_0", From 8cf59638f7ba72eced8faaa9ba07b6fdafdb2f62 Mon Sep 17 00:00:00 2001 From: Hongwei Date: Wed, 7 Oct 2026 17:46:19 +0200 Subject: [PATCH 21/29] refactor: remove the template matching that chose a ResourceDoc Every doc of every catalog now carries its route, so the middleware selects the doc by asking the routes and passes on a request no route serves. Delete the URL-template index and lookup (buildIndex, findResourceDoc), the guess that a capitalised segment is a placeholder (isTemplateVariable, literalAllCapsSegments), the no-doc branch of the middleware and the caller resolution it needed. ResourceDocMiddleware.apply now returns the routes itself: it runs the route of the selected doc, so it no longer takes the chain as a second argument. A doc with no route, or a null one, fails when the middleware is built and names the doc. Docs that share one route are told apart by the segments the request has in common with each template, with the first doc as the fallback, instead of by a list of enum values. The path parameters read from a template are the four the middleware validates: BANK_ID, ACCOUNT_ID, VIEW_ID and COUNTERPARTY_ID. ResourceDocSelfResolveTest is replaced by ResourceDocRouteBindingTest, which checks the same thing through the routes. ResourceDocMatcherTest keeps the path parameter and CallContext scenarios. --- .../UKOpenBanking/v2_0_0/Http4sUKOBv200.scala | 2 +- .../UKOpenBanking/v3_1_0/Http4sUKOBv310.scala | 2 +- .../UKOpenBanking/v4_0_1/Http4sUKOBv401.scala | 2 +- .../api/berlin/group/v1_3/Http4sBGv13.scala | 2 +- .../code/api/berlin/group/v2/Http4sBGv2.scala | 2 +- .../code/api/util/http4s/Http4sSupport.scala | 162 +----- .../util/http4s/IdempotencyMiddleware.scala | 10 +- .../util/http4s/ResourceDocMiddleware.scala | 109 +--- .../scala/code/api/v1_2_1/Http4s121.scala | 2 +- .../scala/code/api/v1_3_0/Http4s130.scala | 2 +- .../scala/code/api/v1_4_0/Http4s140.scala | 2 +- .../scala/code/api/v2_0_0/Http4s200.scala | 2 +- .../scala/code/api/v2_1_0/Http4s210.scala | 2 +- .../scala/code/api/v2_2_0/Http4s220.scala | 2 +- .../scala/code/api/v3_0_0/Http4s300.scala | 2 +- .../scala/code/api/v3_1_0/Http4s310.scala | 2 +- .../scala/code/api/v4_0_0/Http4s400.scala | 2 +- .../scala/code/api/v5_0_0/Http4s500.scala | 2 +- .../scala/code/api/v5_1_0/Http4s510.scala | 2 +- .../scala/code/api/v6_0_0/Http4s600.scala | 2 +- .../scala/code/api/v7_0_0/Http4s700.scala | 2 +- .../util/http4s/ResourceDocMatcherTest.scala | 524 ++---------------- .../ResourceDocRouteSelectionTest.scala | 33 +- 23 files changed, 130 insertions(+), 744 deletions(-) diff --git a/obp-api/src/main/scala/code/api/UKOpenBanking/v2_0_0/Http4sUKOBv200.scala b/obp-api/src/main/scala/code/api/UKOpenBanking/v2_0_0/Http4sUKOBv200.scala index b2235269bb..ad6391e49c 100644 --- a/obp-api/src/main/scala/code/api/UKOpenBanking/v2_0_0/Http4sUKOBv200.scala +++ b/obp-api/src/main/scala/code/api/UKOpenBanking/v2_0_0/Http4sUKOBv200.scala @@ -64,5 +64,5 @@ object Http4sUKOBv200 extends MdcLoggable { lazy val allRoutes: HttpRoutes[IO] = Http4sRoute.chain(routesInOrder) - lazy val wrappedRoutes: HttpRoutes[IO] = ResourceDocMiddleware.apply(orderedResourceDocs, routes => IdempotencyMiddleware(routes))(IdempotencyMiddleware(allRoutes)) + lazy val wrappedRoutes: HttpRoutes[IO] = ResourceDocMiddleware.apply(orderedResourceDocs, routes => IdempotencyMiddleware(routes)) } diff --git a/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310.scala b/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310.scala index f10dc0e972..ba21c6ed9f 100644 --- a/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310.scala +++ b/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310.scala @@ -105,5 +105,5 @@ object Http4sUKOBv310 extends MdcLoggable { lazy val allRoutes: HttpRoutes[IO] = Http4sRoute.chain(routesInOrder) - lazy val wrappedRoutes: HttpRoutes[IO] = ResourceDocMiddleware.apply(orderedResourceDocs, routes => IdempotencyMiddleware(routes))(IdempotencyMiddleware(allRoutes)) + lazy val wrappedRoutes: HttpRoutes[IO] = ResourceDocMiddleware.apply(orderedResourceDocs, routes => IdempotencyMiddleware(routes)) } diff --git a/obp-api/src/main/scala/code/api/UKOpenBanking/v4_0_1/Http4sUKOBv401.scala b/obp-api/src/main/scala/code/api/UKOpenBanking/v4_0_1/Http4sUKOBv401.scala index dcea4e255b..e1e89cb0df 100644 --- a/obp-api/src/main/scala/code/api/UKOpenBanking/v4_0_1/Http4sUKOBv401.scala +++ b/obp-api/src/main/scala/code/api/UKOpenBanking/v4_0_1/Http4sUKOBv401.scala @@ -93,5 +93,5 @@ object Http4sUKOBv401 extends MdcLoggable { routes(req).map(_.putHeaders(Header.Raw(fapiInteractionIdHeader, interactionId))) } - lazy val wrappedRoutes: HttpRoutes[IO] = withFapiInteractionId(ResourceDocMiddleware.apply(orderedResourceDocs, routes => IdempotencyMiddleware(routes))(IdempotencyMiddleware(allRoutes))) + lazy val wrappedRoutes: HttpRoutes[IO] = withFapiInteractionId(ResourceDocMiddleware.apply(orderedResourceDocs, routes => IdempotencyMiddleware(routes))) } diff --git a/obp-api/src/main/scala/code/api/berlin/group/v1_3/Http4sBGv13.scala b/obp-api/src/main/scala/code/api/berlin/group/v1_3/Http4sBGv13.scala index 275f647fc7..494d774931 100644 --- a/obp-api/src/main/scala/code/api/berlin/group/v1_3/Http4sBGv13.scala +++ b/obp-api/src/main/scala/code/api/berlin/group/v1_3/Http4sBGv13.scala @@ -66,5 +66,5 @@ object Http4sBGv13 extends MdcLoggable { lazy val allRoutes: HttpRoutes[IO] = Http4sRoute.chain(routesInOrder) - lazy val wrappedRoutes: HttpRoutes[IO] = ResourceDocMiddleware.apply(orderedResourceDocs, routes => IdempotencyMiddleware(routes))(IdempotencyMiddleware(allRoutes)) + lazy val wrappedRoutes: HttpRoutes[IO] = ResourceDocMiddleware.apply(orderedResourceDocs, routes => IdempotencyMiddleware(routes)) } diff --git a/obp-api/src/main/scala/code/api/berlin/group/v2/Http4sBGv2.scala b/obp-api/src/main/scala/code/api/berlin/group/v2/Http4sBGv2.scala index 8c51736be9..67577bfa47 100644 --- a/obp-api/src/main/scala/code/api/berlin/group/v2/Http4sBGv2.scala +++ b/obp-api/src/main/scala/code/api/berlin/group/v2/Http4sBGv2.scala @@ -66,5 +66,5 @@ object Http4sBGv2 extends MdcLoggable with ScannedApis { lazy val allRoutes: HttpRoutes[IO] = Http4sRoute.chain(routesInOrder) - lazy val wrappedRoutes: HttpRoutes[IO] = ResourceDocMiddleware.apply(orderedResourceDocs, routes => IdempotencyMiddleware(routes))(IdempotencyMiddleware(allRoutes)) + lazy val wrappedRoutes: HttpRoutes[IO] = ResourceDocMiddleware.apply(orderedResourceDocs, routes => IdempotencyMiddleware(routes)) } diff --git a/obp-api/src/main/scala/code/api/util/http4s/Http4sSupport.scala b/obp-api/src/main/scala/code/api/util/http4s/Http4sSupport.scala index d233e7b535..cfd791906c 100644 --- a/obp-api/src/main/scala/code/api/util/http4s/Http4sSupport.scala +++ b/obp-api/src/main/scala/code/api/util/http4s/Http4sSupport.scala @@ -34,7 +34,6 @@ import code.api.util.ErrorMessages.{AuthenticatedUserIsRequired, InvalidJsonForm import code.api.util.{AuthHeaderParser, CallContext, RemoteIpUtil, WriteMetricUtil} import code.util.Helper.MdcLoggable import com.openbankproject.commons.model.{Bank, BankAccount, CounterpartyTrait, User, View} -import com.openbankproject.commons.model.enums.{StrongCustomerAuthentication, TransactionRequestTypes} import net.liftweb.common.{Box, Empty, Full} import org.http4s._ import org.http4s.dsl.io._ @@ -791,86 +790,17 @@ object Http4sCallContextBuilder { } /** - * Matches http4s requests to ResourceDoc entries. - * - * ResourceDoc entries use URL templates with uppercase variable names: - * - BANK_ID, ACCOUNT_ID, VIEW_ID, COUNTERPARTY_ID - * - * This matcher finds the corresponding ResourceDoc for a given request - * and extracts path parameters. + * Selects the ResourceDoc of the http4s route that serves a request, and reads the path parameters + * the middleware validates (BANK_ID, ACCOUNT_ID, VIEW_ID, COUNTERPARTY_ID) from the doc's template. + * + * A doc is never chosen by comparing the request URL with its template: the template is documentation + * (resource-docs, Swagger), and which route serves a request is asked of the routes themselves. */ object ResourceDocMatcher extends code.util.Helper.MdcLoggable { // API prefix pattern: //v — handles both OBP (/obp/vX.X.X) and BG (/berlin-group/v1.3) private val apiPrefixPattern = """^/[^/]+/v[\d.]+""".r - // Pre-built index type: (VERB, apiVersion, segmentCount) -> candidates - type ResourceDocIndex = Map[(String, String, Int), List[ResourceDoc]] - - /** - * Build a lookup index from a collection of ResourceDocs. - * Call this once at middleware startup; pass the result to findResourceDoc. - */ - def buildIndex(resourceDocs: Iterable[ResourceDoc]): ResourceDocIndex = - resourceDocs.groupBy { doc => - val segCount = doc.requestUrl.split("/").count(_.nonEmpty) - (doc.requestVerb.toUpperCase, doc.implementedInApiVersion.toString, segCount) - }.map { case (k, v) => k -> v.toList } - - /** - * Find ResourceDoc matching the given verb and path using a pre-built index. - * O(1) map lookup + O(k) scan where k is the number of docs with the same - * verb/version/segment-count (typically 1–3 in practice). - * - * @param verb HTTP verb (GET, POST, PUT, DELETE, etc.) - * @param path Request path - * @param index Index built by buildIndex — create once, reuse per request - * @return Option[ResourceDoc] if a match is found - */ - def findResourceDoc( - verb: String, - path: Uri.Path, - index: ResourceDocIndex - ): Option[ResourceDoc] = { - val pathString = path.renderString - val apiVersion = pathString.split("/").filter(_.nonEmpty).drop(1).headOption.getOrElse("") - val strippedPath = apiPrefixPattern.replaceFirstIn(pathString, "") - val segCount = strippedPath.split("/").count(_.nonEmpty) - val lookupKey = (verb.toUpperCase, apiVersion, segCount) - val candidates = index.getOrElse(lookupKey, Nil) - // When several templates match, the one with the most literal segments wins, so - // `.../transaction-request-types/SEPA/...` beats `.../transaction-request-types/TRANSACTION_REQUEST_TYPE/...` - // whichever was registered first. On a tie, the first registered wins (maxBy keeps the first maximum). - val matching = candidates.filter(doc => matchesUrlTemplate(strippedPath, doc.requestUrl)) - val result = if (matching.isEmpty) None else Some(matching.maxBy(doc => literalSegmentCount(doc.requestUrl))) - if (result.isEmpty) { - logger.debug( - s"[ResourceDocMatcher] No match for $verb $pathString. " + - s"lookupKey=$lookupKey strippedPath='$strippedPath'. " + - s"Candidates with that key: ${if (candidates.isEmpty) "(none)" else candidates.map(d => s"${d.requestVerb} ${d.requestUrl}(${d.implementedInApiVersion})").mkString(", ")}. " + - s"Index keys for apiVersion=$apiVersion: ${index.keys.filter(_._2 == apiVersion).mkString(", ")}" - ) - } - result - } - - /** - * Find ResourceDoc matching the given verb and path. - * Builds a transient index on every call — use for tests or one-off lookups. - * For hot paths, prefer buildIndex + findResourceDoc(verb, path, index). - * - * @param verb HTTP verb (GET, POST, PUT, DELETE, etc.) - * @param path Request path - * @param resourceDocs Collection of ResourceDoc entries to search - * @return Option[ResourceDoc] if a match is found - */ - def findResourceDoc( - verb: String, - path: Uri.Path, - resourceDocs: ArrayBuffer[ResourceDoc] - ): Option[ResourceDoc] = - findResourceDoc(verb, path, buildIndex(resourceDocs)) - /** A doc that carries its route, with the docs of the same bucket that share that route. */ final case class RouteBoundDoc( doc: ResourceDoc, @@ -920,7 +850,7 @@ object ResourceDocMatcher extends code.util.Helper.MdcLoggable { ArrayBuffer(kept.sortBy(doc => position.get(keyOf(doc.http4sPartialFunction.get)).intValue): _*) } - /** The API version a request path is for: the segment after the API prefix, as in [[findResourceDoc]]. */ + /** The API version a request path is for: the segment after the API prefix, as in the route index. */ private def apiVersionOf(pathString: String): String = pathString.split("/").filter(_.nonEmpty).drop(1).headOption.getOrElse("") @@ -945,13 +875,13 @@ object ResourceDocMatcher extends code.util.Helper.MdcLoggable { * the pattern match of its handler, so the request is put to that match instead of being compared * with the doc's URL template. The doc that comes back is the doc of the route that will run, * whatever the template says, and nothing about a segment has to be guessed to be a placeholder - * or a fixed word. Docs without a route are not in the index; they are found by [[findResourceDoc]]. + * or a fixed word. * * Only the routes of the request's verb and API version are asked, in the order the docs were given, * which must be the order the routes are tried in when a request could be served by more than one. * Several docs may share one route (a handler that serves every transaction-request type has a doc - * per type); the template then tells those docs apart, the one with the most fixed segments winning. - * When none of their templates matches (an empty segment, a type that has no doc of its own) the + * per type); the segments the request has in common with each doc's template then tell them apart. + * When no doc has a word of the request (an empty segment, a type that has no doc of its own) the * first of them is used: the request is served by that handler either way, so it is still validated * under the roles and errors of the endpoint that serves it rather than run with no validation. */ @@ -962,12 +892,14 @@ object ResourceDocMatcher extends code.util.Helper.MdcLoggable { .map { first => if (first.sharingTheRoute.size <= 1) first.doc else { - val strippedPath = apiPrefixPattern.replaceFirstIn(req.uri.path.renderString, "") - first.sharingTheRoute - .filter(doc => matchesUrlTemplate(strippedPath, doc.requestUrl)) - .sortBy(doc => -literalSegmentCount(doc.requestUrl)) - .headOption - .getOrElse(first.doc) + // The docs that share this route differ in a fixed word of the URL (a transaction-request type, + // an SCA method). The request carries that word as it is, so the doc that agrees with the request + // in the most segments is the doc the request is for; a word no doc has leaves them all level and + // the first doc is used. + val requestSegments = apiPrefixPattern.replaceFirstIn(req.uri.path.renderString, "").split("/").filter(_.nonEmpty) + def agreement(doc: ResourceDoc): Int = + doc.requestUrl.split("/").filter(_.nonEmpty).zip(requestSegments).count { case (template, actual) => template == actual } + first.sharingTheRoute.maxBy(agreement) } } } @@ -977,64 +909,15 @@ object ResourceDocMatcher extends code.util.Helper.MdcLoggable { selectByRoute(req, buildRouteIndex(docs)) /** - * Check if a path matches a URL template - * Template segments in uppercase are treated as variables - */ - private def matchesUrlTemplate(path: String, template: String): Boolean = { - val pathSegments = path.split("/").filter(_.nonEmpty) - val templateSegments = template.split("/").filter(_.nonEmpty) - - if (pathSegments.length != templateSegments.length) { - false - } else { - pathSegments.zip(templateSegments).forall { case (pathSeg, templateSeg) => - // Uppercase segments are variables (BANK_ID, ACCOUNT_ID, etc.) - isTemplateVariable(templateSeg) || pathSeg == templateSeg - } - } - } - - /** - * This set holds the all-caps URL segments that are fixed words, not placeholders. - * - * ResourceDoc templates write placeholders in capitals (`BANK_ID`, `GRANT_VIEW_ID`, - * `TRANSACTION_REQUEST_TYPE`), and OBP writes enum values in capitals too. Where an enum - * value appears in a URL, as in - * `/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/transaction-request-types/MOBILE_WALLET/transaction-requests`, - * the two look the same. If `MOBILE_WALLET` were read as a placeholder, that template would - * match every transaction-request type, and the middleware would validate UTILITY or BULK - * requests with the MOBILE_WALLET doc (its roles, operationId, enable/disable switch), or - * return 404 for a type that version has no handler for instead of letting the request - * fall through to an older version. + * Extract path parameters from the selected ResourceDoc * - * The set used to be kept by hand and fell behind each time a type was added. It is now - * built from the two enums whose values appear as URL segments, transaction-request types - * and SCA methods, so a new enum value is a literal without anyone remembering to list it. - * Any other all-caps segment stays a placeholder, which keeps the deliberate non-standard - * placeholders (NEW_ACCOUNT_ID, FIREHOSE_BANK_ID, ...) working. - */ - private[http4s] val literalAllCapsSegments: Set[String] = - TransactionRequestTypes.values.map(_.toString).toSet ++ - StrongCustomerAuthentication.values.map(_.toString) + - // Used as a fixed segment in consent URLs but not an SCA enum value. - "NOT_EMAIL_NEITHER_SMS" - - private def literalSegmentCount(template: String): Int = - template.split("/").count(segment => segment.nonEmpty && !isTemplateVariable(segment)) - - private def isTemplateVariable(segment: String): Boolean = { - segment.nonEmpty && - segment.forall(c => c.isUpper || c == '_' || c.isDigit) && - !literalAllCapsSegments.contains(segment) - } - - /** - * Extract path parameters from matched ResourceDoc - * * @param path Request path * @param resourceDoc Matched ResourceDoc * @return Map with keys: BANK_ID, ACCOUNT_ID, VIEW_ID, COUNTERPARTY_ID (if present) */ + /** The path parameters the middleware validates; any other capitalised word in a template is documentation. */ + private val pathParamNames = Set("BANK_ID", "ACCOUNT_ID", "VIEW_ID", "COUNTERPARTY_ID") + def extractPathParams( path: Uri.Path, resourceDoc: ResourceDoc @@ -1056,8 +939,7 @@ object ResourceDocMatcher extends code.util.Helper.MdcLoggable { Map.empty } else { pathSegments.zip(templateSegments).collect { - case (pathSeg, templateSeg) if isTemplateVariable(templateSeg) => - templateSeg -> pathSeg + case (pathSeg, templateSeg) if pathParamNames.contains(templateSeg) => templateSeg -> pathSeg }.toMap } } diff --git a/obp-api/src/main/scala/code/api/util/http4s/IdempotencyMiddleware.scala b/obp-api/src/main/scala/code/api/util/http4s/IdempotencyMiddleware.scala index cf9f829619..ae15f6fcff 100644 --- a/obp-api/src/main/scala/code/api/util/http4s/IdempotencyMiddleware.scala +++ b/obp-api/src/main/scala/code/api/util/http4s/IdempotencyMiddleware.scala @@ -139,8 +139,8 @@ object IdempotencyMiddleware extends MdcLoggable { OptionT.liftF(invalidKeyResponse(key)) } else if (!matchedThisTier(req)) { // operationId is set on the CallContext ONLY by ResourceDocMiddleware.attachToCallContext, - // and only for the one version tree whose ResourceDocMatcher actually matched this - // request (see ResourceDocMiddleware.apply: the `case None` branch never attaches one). + // and only for the one version tree with a route that actually serves this + // request (see ResourceDocMiddleware.apply: a request no route of the tree serves is passed on without one). // Every other tree in the `.orElse` chain is about to answer a miss regardless of what // this middleware does, so doing lock/response-key work there is worse than wasted: two // requests that legitimately belong to two DIFFERENT trees, or two genuinely concurrent @@ -266,10 +266,10 @@ object IdempotencyMiddleware extends MdcLoggable { key.length <= MaxKeyLength && key.forall(c => c >= 0x21 && c <= 0x7E) - // True only for the one version tree whose ResourceDocMatcher matched this request -- + // True only for the one version tree with a route that serves this request -- // ResourceDocMiddleware.attachToCallContext is the sole place operationId is ever set, and it - // runs only on a match (see ResourceDocMiddleware.apply's `case Some(resourceDoc)` branch; the - // `case None` branch attaches a CallContext with no operationId). Every other tree in the + // runs only on a selected doc (see ResourceDocMiddleware.apply's `case Some(resourceDoc)` branch; + // a request no route serves is passed on without a CallContext). Every other tree in the // `.orElse` chain sees operationId absent here and skips idempotency handling entirely, because // it is about to answer a miss regardless of what this middleware does. private def matchedThisTier(req: Request[IO]): Boolean = diff --git a/obp-api/src/main/scala/code/api/util/http4s/ResourceDocMiddleware.scala b/obp-api/src/main/scala/code/api/util/http4s/ResourceDocMiddleware.scala index 200833e2a9..21369ec31d 100644 --- a/obp-api/src/main/scala/code/api/util/http4s/ResourceDocMiddleware.scala +++ b/obp-api/src/main/scala/code/api/util/http4s/ResourceDocMiddleware.scala @@ -140,42 +140,36 @@ object ResourceDocMiddleware extends MdcLoggable { (enabledOperationIds.isEmpty || enabledOperationIds.contains(rd.operationId)) /** - * Middleware factory: wraps HttpRoutes with ResourceDoc validation. - * Finds the matching ResourceDoc, validates the request, and enriches CallContext. + * Middleware factory: validates a request under the ResourceDoc of the route that serves it, then runs + * that route. * - * A doc that carries its route ([[code.api.util.APIUtil.Http4sRoute]]) is found by asking the - * routes whether they serve the request (`ResourceDocMatcher.selectByRoute`, docs tried in the - * order given), and that route alone is run, wrapped in `wrap`. A doc without a route is found by - * matching the URL against its template, and the request goes to `routes`. + * Each doc carries its route ([[code.api.util.APIUtil.Http4sRoute]]). The doc is found by asking the + * routes whether they serve the request (`ResourceDocMatcher.selectByRoute`, docs tried in the order + * given, which must be the order the routes are tried in), and that route alone is run, wrapped in + * `wrap`. A request no route serves is passed on to the next link of the chain at once. */ def apply( resourceDocs: ArrayBuffer[ResourceDoc], wrap: HttpRoutes[IO] => HttpRoutes[IO] = identity - ): HttpRoutes[IO] => HttpRoutes[IO] = { routes => + ): HttpRoutes[IO] = { // A route declared after the `resourceDocs +=` line that refers to it is still null when the doc is // built (Rule 5 in CLAUDE.md). Say which endpoint it is now, rather than failing with an NPE here or // on the first request. resourceDocs.foreach { doc => - if (doc.http4sPartialFunction.exists(_ == null)) + if (doc.http4sPartialFunction.forall(_ == null)) throw new IllegalStateException( - s"The route of ${doc.partialFunctionName} (${doc.requestVerb} ${doc.requestUrl}) is null: declare " + - "each endpoint val before the resourceDocs += line that uses it.") + s"The route of ${doc.partialFunctionName} (${doc.requestVerb} ${doc.requestUrl}) is missing or null: declare " + + "each endpoint val before the resourceDocs += line that uses it, and give the doc its route.") } - // Build the lookup structures once per middleware instance (at startup), not per request. - val (routeBound, unbound) = resourceDocs.toList.partition(_.http4sPartialFunction.exists(_.route.isDefined)) - val routeIndex = ResourceDocMatcher.buildRouteIndex(routeBound) - val resourceDocIndex = ResourceDocMatcher.buildIndex(unbound) - // When every doc carries its route, a request no route serves is not this group's: nothing else here - // could serve it, so it can go straight to the next link of the chain. - val everyDocIsRouteBound = routeBound.nonEmpty && unbound.isEmpty + // Build the lookup structure once per middleware instance (at startup), not per request. + val routeIndex = ResourceDocMatcher.buildRouteIndex(resourceDocs) // A ResourceDoc is a case class holding JSON bodies: look it up by identity, not by equality. val routeOfDoc = new java.util.IdentityHashMap[ResourceDoc, HttpRoutes[IO]]() - routeBound.foreach(doc => routeOfDoc.put(doc, wrap(doc.http4sPartialFunction.get.routes))) + resourceDocs.foreach(doc => routeOfDoc.put(doc, wrap(doc.http4sPartialFunction.get.routes))) Kleisli[HttpF, Request[IO], Response[IO]] { req: Request[IO] => val selected: Option[(ResourceDoc, HttpRoutes[IO])] = ResourceDocMatcher.selectByRoute(req, routeIndex).map(doc => doc -> routeOfDoc.get(doc)) - .orElse(ResourceDocMatcher.findResourceDoc(req.method.name, req.uri.path, resourceDocIndex).map(_ -> routes)) - if (selected.isEmpty && everyDocIsRouteBound) OptionT.none[IO, Response[IO]] + if (selected.isEmpty) OptionT.none[IO, Response[IO]] else { // Read enable/disable Props per request so runtime changes (e.g. `setPropsValues` in // tests or live config reloads) take effect immediately. Cost is a few Lift Props @@ -248,27 +242,9 @@ object ResourceDocMiddleware extends MdcLoggable { OptionT(work.timeoutTo(endpointTimeoutMs.millis, endpointTimeoutResponse(req)).flatTap(recordTelemetry(resourceDoc, startNanos))) case None => - // This group has no ResourceDoc for the request. Almost always the request is simply - // not ours: `routes.run` yields None and the request moves to the next link of the - // version fallthrough chain (v7 -> v6 -> v5.1 -> bridges -> ... ). No transaction scope - // is opened. The cached body is carried forward for the later hops. - // - // The one case where the inner routes DO serve such a request is a malformed URL with - // an empty path segment (e.g. `/banks//accounts`) in a version whose docs are matched - // by template: the matcher counts segments and finds no doc, but the http4s pattern - // still matches with an empty id, and its handler needs the caller in the CallContext - // to answer 403/404 rather than a misleading 401. That is why the caller is resolved - // here at all. It is resolved WITHOUT rate limiting and at most ONCE per request (see - // resolveCallerOnce): rate limiting belongs to the hop that serves the request, and - // re-validating the credentials on every hop was pure waste. - // - // A group whose docs all carry their route never gets here: a request one of its routes - // serves has its doc, empty segment or not, and any other request is passed on at once. - OptionT.liftF( - resolveCallerOnce(req, cc).map { resolvedCc => - reqWithOriginalBody.withAttribute(Http4sRequestAttributes.callContextKey, resolvedCc) - } - ).flatMap(routes.run) + // Not reachable: a request no route serves was passed on above. + OptionT.none[IO, Response[IO]] + } } } @@ -291,57 +267,6 @@ object ResourceDocMiddleware extends MdcLoggable { case None => IO.unit } - /** - * Resolve the caller for a hop that has no ResourceDoc for the request, once per request. - * - * The first such hop runs [[APIUtil.resolveCallerWithoutRateLimiting]] and stores the outcome in - * the holder Http4sApp attached to the request (`Http4sRequestAttributes.callerResolvedOnThisRequestKey`); - * every later hop of the same request reads it. Before this, each hop authenticated afresh AND counted the call - * against the Consumer's rate limit: `GET /obp/v5.1.0/users/current` crossed seven hops before - * v3.0.0 served it, so it validated the token seven times, saved the Consumer row seven times, - * and cost seven rate-limit units - a Consumer limited to fewer than seven calls per second was - * refused with 429 OBP-10018 on the second hop. - * - * Failures (bad token, unknown consumer, ...) are kept too - they are the outcome for this - * request - and leave the CallContext without a user, exactly as before. Only an exception - * thrown by the pipeline is not kept; the hop then proceeds with the unresolved context. - */ - private def resolveCallerOnce(req: Request[IO], cc: CallContext): IO[CallContext] = { - val holder = req.attributes.lookup(Http4sRequestAttributes.callerResolvedOnThisRequestKey) - holder.flatMap(_.get()) match { - case Some(resolved) => - IO.pure(withResolvedCaller(cc, resolved)) - case None => - IO.fromFuture(IO(APIUtil.resolveCallerWithoutRateLimiting(cc))).attempt.map { - case Right(resolved) => - holder.foreach(_.set(Some(resolved))) - withResolvedCaller(cc, resolved) - case Left(NonFatal(e)) => - logger.debug(s"[ResourceDocMiddleware] caller resolution threw on a no-ResourceDoc hop for ${req.method.name} ${req.uri.path.renderString}: ${e.getMessage}") - cc - case Left(e) => throw e - } - } - } - - /** - * Merge a resolution into THIS hop's freshly built CallContext. The resolved context may come - * from an earlier hop; the bridges rewrite the path between hops (`/obp/v5.1.0/...` -> - * `/obp/v5.0.0/...`), so the current hop's `url` and `implementedInVersion` are kept while the - * authentication-derived fields (user, consumer, session, rate-limit config, ...) are taken from - * the resolution. - */ - private def withResolvedCaller(cc: CallContext, resolved: Http4sRequestAttributes.ResolvedCaller): CallContext = { - def carryOver(resolvedCc: CallContext): CallContext = - resolvedCc.copy(url = cc.url, implementedInVersion = cc.implementedInVersion) - resolved match { - case (Full(user), Some(resolvedCc)) => carryOver(resolvedCc).copy(user = Full(user)) - case (Full(user), None) => cc.copy(user = Full(user)) - case (_, Some(resolvedCc)) => carryOver(resolvedCc) - case _ => cc - } - } - /** 504 response emitted when endpointTimeoutMs elapses before the handler completes. */ private def endpointTimeoutResponse(req: Request[IO]): IO[Option[Response[IO]]] = IO { logger.warn( diff --git a/obp-api/src/main/scala/code/api/v1_2_1/Http4s121.scala b/obp-api/src/main/scala/code/api/v1_2_1/Http4s121.scala index 06e15f6574..c43addcb0f 100644 --- a/obp-api/src/main/scala/code/api/v1_2_1/Http4s121.scala +++ b/obp-api/src/main/scala/code/api/v1_2_1/Http4s121.scala @@ -2702,7 +2702,7 @@ object Http4s121 { lazy val allRoutes: HttpRoutes[IO] = Http4sRoute.chain(routesInOrder) lazy val allRoutesWithMiddleware: HttpRoutes[IO] = { - val middlewareWrapped = ResourceDocMiddleware.apply(orderedResourceDocs, routes => IdempotencyMiddleware(routes))(IdempotencyMiddleware(allRoutes)) + val middlewareWrapped = ResourceDocMiddleware.apply(orderedResourceDocs, routes => IdempotencyMiddleware(routes)) // bankById runs before middleware so it can return 400 (not 404) for unknown bank Kleisli[HttpF, Request[IO], Response[IO]] { req => bankById.run(req).orElse(middlewareWrapped.run(req)) diff --git a/obp-api/src/main/scala/code/api/v1_3_0/Http4s130.scala b/obp-api/src/main/scala/code/api/v1_3_0/Http4s130.scala index 5ab9911b6c..99b9d95a43 100644 --- a/obp-api/src/main/scala/code/api/v1_3_0/Http4s130.scala +++ b/obp-api/src/main/scala/code/api/v1_3_0/Http4s130.scala @@ -160,7 +160,7 @@ object Http4s130 { private lazy val allOwnRoutes: HttpRoutes[IO] = Http4sRoute.chain(routesInOrder) - lazy val allRoutesWithMiddleware: HttpRoutes[IO] = ResourceDocMiddleware.apply(orderedResourceDocs, routes => IdempotencyMiddleware(routes))(IdempotencyMiddleware(allOwnRoutes)) + lazy val allRoutesWithMiddleware: HttpRoutes[IO] = ResourceDocMiddleware.apply(orderedResourceDocs, routes => IdempotencyMiddleware(routes)) // ─── path-rewriting bridge: /obp/v1.3.0/… → /obp/v1.2.1/… ───────────── // Delegates to Http4s121 so all inherited v1.2.1 endpoints are served diff --git a/obp-api/src/main/scala/code/api/v1_4_0/Http4s140.scala b/obp-api/src/main/scala/code/api/v1_4_0/Http4s140.scala index e444b47381..91865c91e5 100644 --- a/obp-api/src/main/scala/code/api/v1_4_0/Http4s140.scala +++ b/obp-api/src/main/scala/code/api/v1_4_0/Http4s140.scala @@ -518,7 +518,7 @@ object Http4s140 { private lazy val allOwnRoutes: HttpRoutes[IO] = Http4sRoute.chain(routesInOrder) - lazy val allRoutesWithMiddleware: HttpRoutes[IO] = ResourceDocMiddleware.apply(orderedResourceDocs, routes => IdempotencyMiddleware(routes))(IdempotencyMiddleware(allOwnRoutes)) + lazy val allRoutesWithMiddleware: HttpRoutes[IO] = ResourceDocMiddleware.apply(orderedResourceDocs, routes => IdempotencyMiddleware(routes)) // ─── path-rewriting bridge: /obp/v1.4.0/… → /obp/v1.3.0/… ────────────── // Delegates to Http4s130 so all inherited v1.3.0 and v1.2.1 endpoints are diff --git a/obp-api/src/main/scala/code/api/v2_0_0/Http4s200.scala b/obp-api/src/main/scala/code/api/v2_0_0/Http4s200.scala index 4820642d24..12e9846b36 100644 --- a/obp-api/src/main/scala/code/api/v2_0_0/Http4s200.scala +++ b/obp-api/src/main/scala/code/api/v2_0_0/Http4s200.scala @@ -1647,7 +1647,7 @@ object Http4s200 { private lazy val allOwnRoutes: HttpRoutes[IO] = Http4sRoute.chain(routesInOrder) - lazy val allRoutesWithMiddleware: HttpRoutes[IO] = ResourceDocMiddleware.apply(orderedResourceDocs, routes => IdempotencyMiddleware(routes))(IdempotencyMiddleware(allOwnRoutes)) + lazy val allRoutesWithMiddleware: HttpRoutes[IO] = ResourceDocMiddleware.apply(orderedResourceDocs, routes => IdempotencyMiddleware(routes)) // ─── path-rewriting bridge: /obp/v2.0.0/… → /obp/v1.4.0/… ────────────── // Delegates to Http4s140 so all inherited v1.4.0/v1.3.0/v1.2.1 endpoints are diff --git a/obp-api/src/main/scala/code/api/v2_1_0/Http4s210.scala b/obp-api/src/main/scala/code/api/v2_1_0/Http4s210.scala index c20a94f502..d80262a299 100644 --- a/obp-api/src/main/scala/code/api/v2_1_0/Http4s210.scala +++ b/obp-api/src/main/scala/code/api/v2_1_0/Http4s210.scala @@ -1429,7 +1429,7 @@ object Http4s210 { private lazy val allOwnRoutes: HttpRoutes[IO] = Http4sRoute.chain(routesInOrder) - lazy val allRoutesWithMiddleware: HttpRoutes[IO] = ResourceDocMiddleware.apply(orderedResourceDocs, routes => IdempotencyMiddleware(routes))(IdempotencyMiddleware(allOwnRoutes)) + lazy val allRoutesWithMiddleware: HttpRoutes[IO] = ResourceDocMiddleware.apply(orderedResourceDocs, routes => IdempotencyMiddleware(routes)) // ─── path-rewriting bridge: /obp/v2.1.0/… → /obp/v2.0.0/… ────────────── // Delegates to Http4s200 so all inherited v2.0.0/v1.4.0/v1.3.0/v1.2.1 endpoints diff --git a/obp-api/src/main/scala/code/api/v2_2_0/Http4s220.scala b/obp-api/src/main/scala/code/api/v2_2_0/Http4s220.scala index 9eb1c2385a..27dce905f2 100644 --- a/obp-api/src/main/scala/code/api/v2_2_0/Http4s220.scala +++ b/obp-api/src/main/scala/code/api/v2_2_0/Http4s220.scala @@ -1071,7 +1071,7 @@ object Http4s220 { private lazy val allOwnRoutes: HttpRoutes[IO] = Http4sRoute.chain(routesInOrder) - lazy val allRoutesWithMiddleware: HttpRoutes[IO] = ResourceDocMiddleware.apply(orderedResourceDocs, routes => IdempotencyMiddleware(routes))(IdempotencyMiddleware(allOwnRoutes)) + lazy val allRoutesWithMiddleware: HttpRoutes[IO] = ResourceDocMiddleware.apply(orderedResourceDocs, routes => IdempotencyMiddleware(routes)) // ─── path-rewriting bridge: /obp/v2.2.0/… → /obp/v2.1.0/… ────────────── diff --git a/obp-api/src/main/scala/code/api/v3_0_0/Http4s300.scala b/obp-api/src/main/scala/code/api/v3_0_0/Http4s300.scala index 026df0536f..c1fc9746f1 100644 --- a/obp-api/src/main/scala/code/api/v3_0_0/Http4s300.scala +++ b/obp-api/src/main/scala/code/api/v3_0_0/Http4s300.scala @@ -2343,7 +2343,7 @@ object Http4s300 { private lazy val allOwnRoutes: HttpRoutes[IO] = Http4sRoute.chain(routesInOrder) - lazy val allRoutesWithMiddleware: HttpRoutes[IO] = ResourceDocMiddleware.apply(orderedResourceDocs, routes => IdempotencyMiddleware(routes))(IdempotencyMiddleware(allOwnRoutes)) + lazy val allRoutesWithMiddleware: HttpRoutes[IO] = ResourceDocMiddleware.apply(orderedResourceDocs, routes => IdempotencyMiddleware(routes)) // ─── path-rewriting bridge: /obp/v3.0.0/… → /obp/v2.2.0/… ────────────── diff --git a/obp-api/src/main/scala/code/api/v3_1_0/Http4s310.scala b/obp-api/src/main/scala/code/api/v3_1_0/Http4s310.scala index de306d8429..d4df90d307 100644 --- a/obp-api/src/main/scala/code/api/v3_1_0/Http4s310.scala +++ b/obp-api/src/main/scala/code/api/v3_1_0/Http4s310.scala @@ -5135,7 +5135,7 @@ object Http4s310 { private lazy val allOwnRoutes: HttpRoutes[IO] = Http4sRoute.chain(routesInOrder) - lazy val allRoutesWithMiddleware: HttpRoutes[IO] = ResourceDocMiddleware.apply(orderedResourceDocs, routes => IdempotencyMiddleware(routes))(IdempotencyMiddleware(allOwnRoutes)) + lazy val allRoutesWithMiddleware: HttpRoutes[IO] = ResourceDocMiddleware.apply(orderedResourceDocs, routes => IdempotencyMiddleware(routes)) // ─── path-rewriting bridge: /obp/v3.1.0/… → /obp/v3.0.0/… ────────────── diff --git a/obp-api/src/main/scala/code/api/v4_0_0/Http4s400.scala b/obp-api/src/main/scala/code/api/v4_0_0/Http4s400.scala index 7e3e4feef1..5608eb527e 100644 --- a/obp-api/src/main/scala/code/api/v4_0_0/Http4s400.scala +++ b/obp-api/src/main/scala/code/api/v4_0_0/Http4s400.scala @@ -11397,7 +11397,7 @@ object Http4s400 { private lazy val allOwnRoutes: HttpRoutes[IO] = Http4sRoute.chain(routesInOrder) - lazy val allRoutesWithMiddleware: HttpRoutes[IO] = ResourceDocMiddleware.apply(orderedResourceDocs, routes => IdempotencyMiddleware(routes))(IdempotencyMiddleware(allOwnRoutes)) + lazy val allRoutesWithMiddleware: HttpRoutes[IO] = ResourceDocMiddleware.apply(orderedResourceDocs, routes => IdempotencyMiddleware(routes)) // ─── nameOf-compatibility aliases ──────────────────────────────────────── // These vals have no Lift counterpart in Http4s400 but are referenced by diff --git a/obp-api/src/main/scala/code/api/v5_0_0/Http4s500.scala b/obp-api/src/main/scala/code/api/v5_0_0/Http4s500.scala index 63ea7c5320..4bd35fea61 100644 --- a/obp-api/src/main/scala/code/api/v5_0_0/Http4s500.scala +++ b/obp-api/src/main/scala/code/api/v5_0_0/Http4s500.scala @@ -2397,7 +2397,7 @@ object Http4s500 { lazy val allRoutes: HttpRoutes[IO] = Http4sRoute.chain(routesInOrder) lazy val allRoutesWithMiddleware: HttpRoutes[IO] = - ResourceDocMiddleware.apply(orderedResourceDocs, routes => IdempotencyMiddleware(routes))(IdempotencyMiddleware(allRoutes)) + ResourceDocMiddleware.apply(orderedResourceDocs, routes => IdempotencyMiddleware(routes)) // ─── path-rewriting bridge: /obp/v5.0.0/… → /obp/v4.0.0/… ───────────── // Cascades inherited (v1.2.1–v4.0.0) endpoints through the http4s versions diff --git a/obp-api/src/main/scala/code/api/v5_1_0/Http4s510.scala b/obp-api/src/main/scala/code/api/v5_1_0/Http4s510.scala index fe7c78f878..6ea7ae0610 100644 --- a/obp-api/src/main/scala/code/api/v5_1_0/Http4s510.scala +++ b/obp-api/src/main/scala/code/api/v5_1_0/Http4s510.scala @@ -5443,7 +5443,7 @@ object Http4s510 { lazy val allRoutes: HttpRoutes[IO] = Http4sRoute.chain(routesInOrder) lazy val allRoutesWithMiddleware: HttpRoutes[IO] = - ResourceDocMiddleware.apply(orderedResourceDocs, routes => IdempotencyMiddleware(routes))(IdempotencyMiddleware(allRoutes)) + ResourceDocMiddleware.apply(orderedResourceDocs, routes => IdempotencyMiddleware(routes)) // ─── path-rewriting bridge: /obp/v5.1.0/… → /obp/v5.0.0/… ───────────── lazy val v510ToV500Bridge: HttpRoutes[IO] = Kleisli[HttpF, Request[IO], Response[IO]] { req => diff --git a/obp-api/src/main/scala/code/api/v6_0_0/Http4s600.scala b/obp-api/src/main/scala/code/api/v6_0_0/Http4s600.scala index a52f954e0a..24aaafc7db 100644 --- a/obp-api/src/main/scala/code/api/v6_0_0/Http4s600.scala +++ b/obp-api/src/main/scala/code/api/v6_0_0/Http4s600.scala @@ -6700,7 +6700,7 @@ object Http4s600 { // Deferring index construction to first request (post object-init) lets every // registration land before the snapshot is taken. lazy val allRoutesWithMiddleware: HttpRoutes[IO] = - ResourceDocMiddleware.apply(orderedResourceDocs, routes => IdempotencyMiddleware(routes))(IdempotencyMiddleware(allRoutes)) + ResourceDocMiddleware.apply(orderedResourceDocs, routes => IdempotencyMiddleware(routes)) // ─── path-rewriting bridge: /obp/v6.0.0/… → /obp/v5.1.0/… ───────────── // Targets v5.1.0; Http4s510 has its own working cascade down to v5.0.0 → v4.0.0 → … diff --git a/obp-api/src/main/scala/code/api/v7_0_0/Http4s700.scala b/obp-api/src/main/scala/code/api/v7_0_0/Http4s700.scala index 77355090b5..6b9607e58a 100644 --- a/obp-api/src/main/scala/code/api/v7_0_0/Http4s700.scala +++ b/obp-api/src/main/scala/code/api/v7_0_0/Http4s700.scala @@ -7502,7 +7502,7 @@ object Http4s700 { // before the idempotency scope key is computed; on a cache hit the inner // routes (and any DB transaction) are skipped. val allRoutesWithMiddleware: HttpRoutes[IO] = - ResourceDocMiddleware.apply(orderedResourceDocs, routes => IdempotencyMiddleware(routes))(IdempotencyMiddleware(allRoutes)) + ResourceDocMiddleware.apply(orderedResourceDocs, routes => IdempotencyMiddleware(routes)) } // ─── path-rewriting bridge: /obp/v7.0.0/… → /obp/v6.0.0/… ───────────── diff --git a/obp-api/src/test/scala/code/api/util/http4s/ResourceDocMatcherTest.scala b/obp-api/src/test/scala/code/api/util/http4s/ResourceDocMatcherTest.scala index d9b87477bd..fae5a6d65d 100644 --- a/obp-api/src/test/scala/code/api/util/http4s/ResourceDocMatcherTest.scala +++ b/obp-api/src/test/scala/code/api/util/http4s/ResourceDocMatcherTest.scala @@ -39,17 +39,11 @@ import org.scalatest.{FeatureSpec, GivenWhenThen, Matchers, Tag} import scala.collection.mutable.ArrayBuffer /** - * Unit tests for ResourceDocMatcher - * - * Tests ResourceDoc matching and path parameter extraction: - * - Matching by verb and exact path - * - Matching with BANK_ID variable - * - Matching with BANK_ID + ACCOUNT_ID variables - * - Matching with BANK_ID + ACCOUNT_ID + VIEW_ID variables - * - Matching with COUNTERPARTY_ID variable - * - Non-matching requests return None - * - Path parameter extraction for all variable types - * + * Unit tests for ResourceDocMatcher's path parameter extraction and CallContext wiring. + * + * Which doc serves a request is decided by the routes (see ResourceDocRouteSelectionTest and + * ResourceDocRealCatalogSelectionTest); once the doc is known, the middleware reads BANK_ID, ACCOUNT_ID, + * VIEW_ID and COUNTERPARTY_ID from the positions the doc's template gives them. */ class ResourceDocMatcherTest extends FeatureSpec with Matchers with GivenWhenThen { @@ -78,339 +72,50 @@ class ResourceDocMatcherTest extends FeatureSpec with Matchers with GivenWhenThe ) } - feature("ResourceDocMatcher - Exact path matching") { - - scenario("Match GET request with exact path", ResourceDocMatcherTag) { - Given("A ResourceDoc for GET /banks") - val resourceDocs = ArrayBuffer( - createResourceDoc("GET", "/banks", "getBanks") - ) - - When(s"Matching a GET request to $base/banks") - val path = Uri.Path.unsafeFromString(s"$base/banks") - val result = ResourceDocMatcher.findResourceDoc("GET", path, resourceDocs) - - Then("Should find the matching ResourceDoc") - result should be(defined) - result.get.partialFunctionName should equal("getBanks") - } - - scenario("Match POST request with exact path", ResourceDocMatcherTag) { - Given("A ResourceDoc for POST /banks") - val resourceDocs = ArrayBuffer( - createResourceDoc("POST", "/banks", "createBank") - ) - - When(s"Matching a POST request to $base/banks") - val path = Uri.Path.unsafeFromString(s"$base/banks") - val result = ResourceDocMatcher.findResourceDoc("POST", path, resourceDocs) - - Then("Should find the matching ResourceDoc") - result should be(defined) - result.get.partialFunctionName should equal("createBank") - } - - scenario("Match request with multi-segment path", ResourceDocMatcherTag) { - Given("A ResourceDoc for GET /management/metrics") - val resourceDocs = ArrayBuffer( - createResourceDoc("GET", "/management/metrics", "getMetrics") - ) - - When(s"Matching a GET request to $base/management/metrics") - val path = Uri.Path.unsafeFromString(s"$base/management/metrics") - val result = ResourceDocMatcher.findResourceDoc("GET", path, resourceDocs) - - Then("Should find the matching ResourceDoc") - result should be(defined) - result.get.partialFunctionName should equal("getMetrics") - } - - scenario("Verb mismatch returns None", ResourceDocMatcherTag) { - Given("A ResourceDoc for GET /banks") - val resourceDocs = ArrayBuffer( - createResourceDoc("GET", "/banks", "getBanks") - ) - - When(s"Matching a POST request to $base/banks") - val path = Uri.Path.unsafeFromString(s"$base/banks") - val result = ResourceDocMatcher.findResourceDoc("POST", path, resourceDocs) - - Then("Should return None") - result should be(None) - } - - scenario("Path mismatch returns None", ResourceDocMatcherTag) { - Given("A ResourceDoc for GET /banks") - val resourceDocs = ArrayBuffer( - createResourceDoc("GET", "/banks", "getBanks") - ) - - When(s"Matching a GET request to $base/accounts") - val path = Uri.Path.unsafeFromString(s"$base/accounts") - val result = ResourceDocMatcher.findResourceDoc("GET", path, resourceDocs) - - Then("Should return None") - result should be(None) - } - } - - feature("ResourceDocMatcher - BANK_ID variable matching") { - - scenario("Match request with BANK_ID variable", ResourceDocMatcherTag) { - Given("A ResourceDoc for GET /banks/BANK_ID") - val resourceDocs = ArrayBuffer( - createResourceDoc("GET", "/banks/BANK_ID", "getBank") - ) - - When(s"Matching a GET request to $base/banks/gh.29.de") - val path = Uri.Path.unsafeFromString(s"$base/banks/gh.29.de") - val result = ResourceDocMatcher.findResourceDoc("GET", path, resourceDocs) - - Then("Should find the matching ResourceDoc") - result should be(defined) - result.get.partialFunctionName should equal("getBank") - } - - scenario("Match request with BANK_ID and additional segments", ResourceDocMatcherTag) { - Given("A ResourceDoc for GET /banks/BANK_ID/accounts") - val resourceDocs = ArrayBuffer( - createResourceDoc("GET", "/banks/BANK_ID/accounts", "getBankAccounts") - ) - - When(s"Matching a GET request to $base/banks/test-bank-1/accounts") - val path = Uri.Path.unsafeFromString(s"$base/banks/test-bank-1/accounts") - val result = ResourceDocMatcher.findResourceDoc("GET", path, resourceDocs) - - Then("Should find the matching ResourceDoc") - result should be(defined) - result.get.partialFunctionName should equal("getBankAccounts") - } - - scenario("Extract BANK_ID parameter value", ResourceDocMatcherTag) { - Given("A matched ResourceDoc with BANK_ID") - val resourceDoc = createResourceDoc("GET", "/banks/BANK_ID", "getBank") - - When(s"Extracting path parameters from $base/banks/gh.29.de") - val path = Uri.Path.unsafeFromString(s"$base/banks/gh.29.de") - val params = ResourceDocMatcher.extractPathParams(path, resourceDoc) - - Then("Should extract BANK_ID value") - params should contain key "BANK_ID" - params("BANK_ID") should equal("gh.29.de") - } - } - - feature("ResourceDocMatcher - BANK_ID + ACCOUNT_ID variables") { - - scenario("Match request with BANK_ID and ACCOUNT_ID variables", ResourceDocMatcherTag) { - Given("A ResourceDoc for GET /banks/BANK_ID/accounts/ACCOUNT_ID") - val resourceDocs = ArrayBuffer( - createResourceDoc("GET", "/banks/BANK_ID/accounts/ACCOUNT_ID", "getBankAccount") - ) - - When(s"Matching a GET request to $base/banks/gh.29.de/accounts/test1") - val path = Uri.Path.unsafeFromString(s"$base/banks/gh.29.de/accounts/test1") - val result = ResourceDocMatcher.findResourceDoc("GET", path, resourceDocs) - - Then("Should find the matching ResourceDoc") - result should be(defined) - result.get.partialFunctionName should equal("getBankAccount") - } - - scenario("Extract BANK_ID and ACCOUNT_ID parameter values", ResourceDocMatcherTag) { - Given("A matched ResourceDoc with BANK_ID and ACCOUNT_ID") - val resourceDoc = createResourceDoc("GET", "/banks/BANK_ID/accounts/ACCOUNT_ID", "getBankAccount") - - When(s"Extracting path parameters from $base/banks/gh.29.de/accounts/test1") - val path = Uri.Path.unsafeFromString(s"$base/banks/gh.29.de/accounts/test1") - val params = ResourceDocMatcher.extractPathParams(path, resourceDoc) - - Then("Should extract both BANK_ID and ACCOUNT_ID values") - params should contain key "BANK_ID" - params should contain key "ACCOUNT_ID" - params("BANK_ID") should equal("gh.29.de") - params("ACCOUNT_ID") should equal("test1") - } - - scenario("Match request with BANK_ID, ACCOUNT_ID and additional segments", ResourceDocMatcherTag) { - Given("A ResourceDoc for GET /banks/BANK_ID/accounts/ACCOUNT_ID/transactions") - val resourceDocs = ArrayBuffer( - createResourceDoc("GET", "/banks/BANK_ID/accounts/ACCOUNT_ID/transactions", "getTransactions") - ) - - When(s"Matching a GET request to $base/banks/test-bank/accounts/acc-123/transactions") - val path = Uri.Path.unsafeFromString(s"$base/banks/test-bank/accounts/acc-123/transactions") - val result = ResourceDocMatcher.findResourceDoc("GET", path, resourceDocs) - - Then("Should find the matching ResourceDoc") - result should be(defined) - result.get.partialFunctionName should equal("getTransactions") - } - } - - feature("ResourceDocMatcher - BANK_ID + ACCOUNT_ID + VIEW_ID variables") { - - scenario("Match request with BANK_ID, ACCOUNT_ID and VIEW_ID variables", ResourceDocMatcherTag) { - Given("A ResourceDoc for GET /banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/transactions") - val resourceDocs = ArrayBuffer( - createResourceDoc("GET", "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/transactions", "getTransactionsForView") - ) - - When(s"Matching a GET request to $base/banks/gh.29.de/accounts/test1/owner/transactions") - val path = Uri.Path.unsafeFromString(s"$base/banks/gh.29.de/accounts/test1/owner/transactions") - val result = ResourceDocMatcher.findResourceDoc("GET", path, resourceDocs) - - Then("Should find the matching ResourceDoc") - result should be(defined) - result.get.partialFunctionName should equal("getTransactionsForView") - } - - scenario("Extract BANK_ID, ACCOUNT_ID and VIEW_ID parameter values", ResourceDocMatcherTag) { - Given("A matched ResourceDoc with BANK_ID, ACCOUNT_ID and VIEW_ID") - val resourceDoc = createResourceDoc("GET", "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/transactions", "getTransactionsForView") - - When(s"Extracting path parameters from $base/banks/gh.29.de/accounts/test1/owner/transactions") - val path = Uri.Path.unsafeFromString(s"$base/banks/gh.29.de/accounts/test1/owner/transactions") - val params = ResourceDocMatcher.extractPathParams(path, resourceDoc) - - Then("Should extract all three parameter values") - params should contain key "BANK_ID" - params should contain key "ACCOUNT_ID" - params should contain key "VIEW_ID" - params("BANK_ID") should equal("gh.29.de") - params("ACCOUNT_ID") should equal("test1") - params("VIEW_ID") should equal("owner") - } - - scenario("Match request with VIEW_ID in different position", ResourceDocMatcherTag) { - Given("A ResourceDoc for GET /banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/account") - val resourceDocs = ArrayBuffer( - createResourceDoc("GET", "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/account", "getAccountForView") - ) - - When(s"Matching a GET request to $base/banks/test-bank/accounts/acc-1/public/account") - val path = Uri.Path.unsafeFromString(s"$base/banks/test-bank/accounts/acc-1/public/account") - val result = ResourceDocMatcher.findResourceDoc("GET", path, resourceDocs) - - Then("Should find the matching ResourceDoc") - result should be(defined) - result.get.partialFunctionName should equal("getAccountForView") - } - } - - feature("ResourceDocMatcher - COUNTERPARTY_ID variable") { - - scenario("Match request with COUNTERPARTY_ID variable", ResourceDocMatcherTag) { - Given("A ResourceDoc for GET /banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/counterparties/COUNTERPARTY_ID") - val resourceDocs = ArrayBuffer( - createResourceDoc("GET", "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/counterparties/COUNTERPARTY_ID", "getCounterparty") - ) - - When("Matching a GET request with counterparty ID") - val path = Uri.Path.unsafeFromString(s"$base/banks/gh.29.de/accounts/test1/owner/counterparties/ff010868-ac7d-4f96-9fc5-70dd5757e891") - val result = ResourceDocMatcher.findResourceDoc("GET", path, resourceDocs) - - Then("Should find the matching ResourceDoc") - result should be(defined) - result.get.partialFunctionName should equal("getCounterparty") - } - - scenario("Extract COUNTERPARTY_ID parameter value", ResourceDocMatcherTag) { - Given("A matched ResourceDoc with COUNTERPARTY_ID") - val resourceDoc = createResourceDoc("GET", "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/counterparties/COUNTERPARTY_ID", "getCounterparty") - - When("Extracting path parameters") - val path = Uri.Path.unsafeFromString(s"$base/banks/gh.29.de/accounts/test1/owner/counterparties/ff010868-ac7d-4f96-9fc5-70dd5757e891") - val params = ResourceDocMatcher.extractPathParams(path, resourceDoc) - - Then("Should extract all parameter values including COUNTERPARTY_ID") - params should contain key "BANK_ID" - params should contain key "ACCOUNT_ID" - params should contain key "VIEW_ID" - params should contain key "COUNTERPARTY_ID" - params("BANK_ID") should equal("gh.29.de") - params("ACCOUNT_ID") should equal("test1") - params("VIEW_ID") should equal("owner") - params("COUNTERPARTY_ID") should equal("ff010868-ac7d-4f96-9fc5-70dd5757e891") - } - - scenario("Match request with COUNTERPARTY_ID in different URL structure", ResourceDocMatcherTag) { - Given("A ResourceDoc for DELETE /management/counterparties/COUNTERPARTY_ID") - val resourceDocs = ArrayBuffer( - createResourceDoc("DELETE", "/management/counterparties/COUNTERPARTY_ID", "deleteCounterparty") - ) - - When("Matching a DELETE request") - val path = Uri.Path.unsafeFromString(s"$base/management/counterparties/counterparty-123") - val result = ResourceDocMatcher.findResourceDoc("DELETE", path, resourceDocs) - - Then("Should find the matching ResourceDoc") - result should be(defined) - result.get.partialFunctionName should equal("deleteCounterparty") + feature("ResourceDocMatcher - path parameter extraction") { + + scenario("BANK_ID is read from its position", ResourceDocMatcherTag) { + val resourceDoc = createResourceDoc("GET", "/banks/BANK_ID/accounts", "getAccounts") + val params = ResourceDocMatcher.extractPathParams(Uri.Path.unsafeFromString(s"$base/banks/gh.29.uk/accounts"), resourceDoc) + + params should equal(Map("BANK_ID" -> "gh.29.uk")) } - } - - feature("ResourceDocMatcher - Non-matching requests") { - - scenario("Return None when no ResourceDoc matches", ResourceDocMatcherTag) { - Given("ResourceDocs for specific endpoints") - val resourceDocs = ArrayBuffer( - createResourceDoc("GET", "/banks", "getBanks"), - createResourceDoc("GET", "/banks/BANK_ID", "getBank"), - createResourceDoc("POST", "/banks", "createBank") - ) - - When("Matching a request that doesn't match any ResourceDoc") - val path = Uri.Path.unsafeFromString(s"$base/accounts") - val result = ResourceDocMatcher.findResourceDoc("GET", path, resourceDocs) - - Then("Should return None") - result should be(None) + + scenario("BANK_ID and ACCOUNT_ID are read from their positions", ResourceDocMatcherTag) { + val resourceDoc = createResourceDoc("GET", "/banks/BANK_ID/accounts/ACCOUNT_ID/account", "getAccount") + val params = ResourceDocMatcher.extractPathParams( + Uri.Path.unsafeFromString(s"$base/banks/gh.29.uk/accounts/8ca8a7e4/account"), resourceDoc) + + params should equal(Map("BANK_ID" -> "gh.29.uk", "ACCOUNT_ID" -> "8ca8a7e4")) } - - scenario("Return None when verb doesn't match", ResourceDocMatcherTag) { - Given("A ResourceDoc for GET /banks") - val resourceDocs = ArrayBuffer( - createResourceDoc("GET", "/banks", "getBanks") - ) - - When(s"Matching a DELETE request to $base/banks") - val path = Uri.Path.unsafeFromString(s"$base/banks") - val result = ResourceDocMatcher.findResourceDoc("DELETE", path, resourceDocs) - - Then("Should return None") - result should be(None) + + scenario("VIEW_ID is read wherever the template puts it", ResourceDocMatcherTag) { + val resourceDoc = createResourceDoc("GET", "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/transactions", "getTransactions") + val params = ResourceDocMatcher.extractPathParams( + Uri.Path.unsafeFromString(s"$base/banks/gh.29.uk/accounts/8ca8a7e4/owner/transactions"), resourceDoc) + + params should equal(Map("BANK_ID" -> "gh.29.uk", "ACCOUNT_ID" -> "8ca8a7e4", "VIEW_ID" -> "owner")) } - - scenario("Return None when path segment count doesn't match", ResourceDocMatcherTag) { - Given("A ResourceDoc for GET /banks/BANK_ID/accounts") - val resourceDocs = ArrayBuffer( - createResourceDoc("GET", "/banks/BANK_ID/accounts", "getBankAccounts") - ) - - When("Matching a request with different segment count") - val path = Uri.Path.unsafeFromString(s"$base/banks/gh.29.de") - val result = ResourceDocMatcher.findResourceDoc("GET", path, resourceDocs) - - Then("Should return None") - result should be(None) + + scenario("COUNTERPARTY_ID is read from its position", ResourceDocMatcherTag) { + val resourceDoc = createResourceDoc( + "GET", "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/counterparties/COUNTERPARTY_ID", "getCounterparty") + val params = ResourceDocMatcher.extractPathParams( + Uri.Path.unsafeFromString(s"$base/banks/gh.29.uk/accounts/8ca8a7e4/owner/counterparties/cp1"), resourceDoc) + + params.get("COUNTERPARTY_ID") should be(Some("cp1")) } - - scenario("Return None when literal segments don't match", ResourceDocMatcherTag) { - Given("A ResourceDoc for GET /banks/BANK_ID/accounts") - val resourceDocs = ArrayBuffer( - createResourceDoc("GET", "/banks/BANK_ID/accounts", "getBankAccounts") - ) - - When("Matching a request with different literal segment") - val path = Uri.Path.unsafeFromString(s"$base/banks/gh.29.de/transactions") - val result = ResourceDocMatcher.findResourceDoc("GET", path, resourceDocs) - - Then("Should return None") - result should be(None) + + scenario("Any other capitalised word in a template is documentation, not a parameter", ResourceDocMatcherTag) { + val resourceDoc = createResourceDoc( + "POST", "/banks/BANK_ID/accounts/ACCOUNT_ID/GRANT_VIEW_ID/transaction-request-types/SEPA/transaction-requests", "createTransactionRequestSepa") + val params = ResourceDocMatcher.extractPathParams( + Uri.Path.unsafeFromString(s"$base/banks/gh.29.uk/accounts/8ca8a7e4/owner/transaction-request-types/SEPA/transaction-requests"), resourceDoc) + + params should equal(Map("BANK_ID" -> "gh.29.uk", "ACCOUNT_ID" -> "8ca8a7e4")) } } - + feature("ResourceDocMatcher - Path parameter extraction edge cases") { scenario("Extract parameters from path with no variables", ResourceDocMatcherTag) { @@ -504,151 +209,18 @@ class ResourceDocMatcherTest extends FeatureSpec with Matchers with GivenWhenThe } } - feature("ResourceDocMatcher - Multiple ResourceDocs selection") { - - scenario("Select correct ResourceDoc from multiple candidates", ResourceDocMatcherTag) { - Given("Multiple ResourceDocs with different paths") - val resourceDocs = ArrayBuffer( - createResourceDoc("GET", "/banks", "getBanks"), - createResourceDoc("GET", "/banks/BANK_ID", "getBank"), - createResourceDoc("GET", "/banks/BANK_ID/accounts", "getBankAccounts"), - createResourceDoc("POST", "/banks", "createBank") - ) - - When("Matching a specific request") - val path = Uri.Path.unsafeFromString(s"$base/banks/gh.29.de/accounts") - val result = ResourceDocMatcher.findResourceDoc("GET", path, resourceDocs) - - Then("Should select the most specific matching ResourceDoc") - result should be(defined) - result.get.partialFunctionName should equal("getBankAccounts") - } - - scenario("Match first ResourceDoc when multiple exact matches exist", ResourceDocMatcherTag) { - Given("Multiple ResourceDocs with same path and verb") - val resourceDocs = ArrayBuffer( - createResourceDoc("GET", "/banks", "getBanks1"), - createResourceDoc("GET", "/banks", "getBanks2") - ) - - When("Matching a request") - val path = Uri.Path.unsafeFromString(s"$base/banks") - val result = ResourceDocMatcher.findResourceDoc("GET", path, resourceDocs) - - Then("Should return the first matching ResourceDoc") - result should be(defined) - result.get.partialFunctionName should equal("getBanks1") - } - } - - feature("ResourceDocMatcher - Case sensitivity") { - - scenario("HTTP verb matching is case-insensitive", ResourceDocMatcherTag) { - Given("A ResourceDoc with uppercase GET") - val resourceDocs = ArrayBuffer( - createResourceDoc("GET", "/banks", "getBanks") - ) - - When("Matching with lowercase get") - val path = Uri.Path.unsafeFromString(s"$base/banks") - val result = ResourceDocMatcher.findResourceDoc("get", path, resourceDocs) - - Then("Should find the matching ResourceDoc") - result should be(defined) - result.get.partialFunctionName should equal("getBanks") - } - - scenario("Path matching is case-sensitive for literal segments", ResourceDocMatcherTag) { - Given("A ResourceDoc for /banks") - val resourceDocs = ArrayBuffer( - createResourceDoc("GET", "/banks", "getBanks") - ) - - When("Matching with different case /Banks") - val path = Uri.Path.unsafeFromString(s"$base/Banks") - val result = ResourceDocMatcher.findResourceDoc("GET", path, resourceDocs) - - Then("Should not match (case-sensitive)") - result should be(None) - } - } - - feature("ResourceDocMatcher - transaction-request types and other enum values in URLs") { - - // Transaction-request types and SCA methods are written in capitals, like placeholders. - // These scenarios check that the matcher treats them as fixed words, so one type's - // template never claims another type's request. - val transactionRequestsUrl = "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/transaction-request-types/%s/transaction-requests" - def transactionRequestsPath(transactionRequestType: String): Uri.Path = - Uri.Path.unsafeFromString(s"$base/banks/gh.29.uk/accounts/8ca8a7e4/owner/transaction-request-types/$transactionRequestType/transaction-requests") - - scenario("A type registered first does not claim another type's request", ResourceDocMatcherTag) { - Given("a MOBILE_WALLET doc registered before a UTILITY doc that requires a role") - val utilityDoc = createResourceDoc("POST", transactionRequestsUrl.format("UTILITY"), "createTransactionRequestUtility") - .copy(roles = Some(List(code.api.util.ApiRole.canCreateUtilityVendResult))) - val resourceDocs = ArrayBuffer( - createResourceDoc("POST", transactionRequestsUrl.format("MOBILE_WALLET"), "createTransactionRequestMobileWallet"), - utilityDoc - ) - - When("a UTILITY request is matched") - val result = ResourceDocMatcher.findResourceDoc("POST", transactionRequestsPath("UTILITY"), resourceDocs) - - Then("the UTILITY doc is chosen, so the middleware enforces the UTILITY doc's role") - result.map(_.partialFunctionName) shouldBe Some("createTransactionRequestUtility") - result.flatMap(_.roles) shouldBe Some(List(code.api.util.ApiRole.canCreateUtilityVendResult)) - } - - scenario("A type with no doc in this version matches nothing", ResourceDocMatcherTag) { - Given("a catalog with only a MOBILE_WALLET doc") - val resourceDocs = ArrayBuffer( - createResourceDoc("POST", transactionRequestsUrl.format("MOBILE_WALLET"), "createTransactionRequestMobileWallet") - ) - - When("a CARDANO request is matched") - val result = ResourceDocMatcher.findResourceDoc("POST", transactionRequestsPath("CARDANO"), resourceDocs) - - Then("no doc matches, so the request can fall through to an older version that serves CARDANO") - result shouldBe None - } - - scenario("A specific type's doc beats a placeholder doc registered before it", ResourceDocMatcherTag) { - Given("a TRANSACTION_REQUEST_TYPE placeholder doc registered before a SEPA doc") - val resourceDocs = ArrayBuffer( - createResourceDoc("POST", transactionRequestsUrl.format("TRANSACTION_REQUEST_TYPE"), "createTransactionRequestAnyType"), - createResourceDoc("POST", transactionRequestsUrl.format("SEPA"), "createTransactionRequestSepa") - ) - - Then("a SEPA request gets the SEPA doc") - ResourceDocMatcher.findResourceDoc("POST", transactionRequestsPath("SEPA"), resourceDocs) - .map(_.partialFunctionName) shouldBe Some("createTransactionRequestSepa") - - And("a type with no doc of its own still gets the placeholder doc") - ResourceDocMatcher.findResourceDoc("POST", transactionRequestsPath("FREE_FORM"), resourceDocs) - .map(_.partialFunctionName) shouldBe Some("createTransactionRequestAnyType") - } - - scenario("Every transaction-request type and SCA method is a fixed word", ResourceDocMatcherTag) { - val enumValues = - com.openbankproject.commons.model.enums.TransactionRequestTypes.values.map(_.toString) ++ - com.openbankproject.commons.model.enums.StrongCustomerAuthentication.values.map(_.toString) - enumValues.filterNot(ResourceDocMatcher.literalAllCapsSegments.contains) shouldBe empty - } - } - feature("ResourceDocMatcher - path parameters of a path with an empty segment") { scenario("An empty segment is read by position, so the parameters of the other segments keep their place", ResourceDocMatcherTag) { - Given("a doc with BANK_ID before a placeholder, and a request that leaves the placeholder empty") - val resourceDoc = createResourceDoc("GET", "/banks/BANK_ID/api-products/API_PRODUCT_CODE/subscriptions", "getSubscriptions") - val path = Uri.Path.unsafeFromString(s"$base/banks/gh.29.uk/api-products//subscriptions") + Given("a doc with a placeholder between BANK_ID and ACCOUNT_ID, and a request that leaves the placeholder empty") + val resourceDoc = createResourceDoc("GET", "/banks/BANK_ID/api-products/API_PRODUCT_CODE/accounts/ACCOUNT_ID", "getProductAccount") + val path = Uri.Path.unsafeFromString(s"$base/banks/gh.29.uk/api-products//accounts/8ca8a7e4") When("the path parameters are extracted") val params = ResourceDocMatcher.extractPathParams(path, resourceDoc) - Then("BANK_ID is the bank of the request, and the empty segment is an empty value") - params.get("BANK_ID") should be(Some("gh.29.uk")) - params.get("API_PRODUCT_CODE") should be(Some("")) + Then("BANK_ID and ACCOUNT_ID are the values of the request, each at its own position") + params should equal(Map("BANK_ID" -> "gh.29.uk", "ACCOUNT_ID" -> "8ca8a7e4")) } scenario("An empty BANK_ID segment is an empty BANK_ID, not a missing one", ResourceDocMatcherTag) { diff --git a/obp-api/src/test/scala/code/api/util/http4s/ResourceDocRouteSelectionTest.scala b/obp-api/src/test/scala/code/api/util/http4s/ResourceDocRouteSelectionTest.scala index 29536527db..ece005adce 100644 --- a/obp-api/src/test/scala/code/api/util/http4s/ResourceDocRouteSelectionTest.scala +++ b/obp-api/src/test/scala/code/api/util/http4s/ResourceDocRouteSelectionTest.scala @@ -110,7 +110,7 @@ class ResourceDocRouteSelectionTest extends FeatureSpec with Matchers with Given selected.http4sPartialFunction.flatMap(_.route).get.isDefinedAt(req) shouldBe true } - scenario("Docs that share one route are told apart by their templates", ResourceDocRouteSelectionTag) { + scenario("Docs that share one route are told apart by the words of the request their templates carry", ResourceDocRouteSelectionTag) { Given("one route that serves every transaction-request type, and a generic doc and a SEPA doc that share it") val anyType = Http4sRoute { case POST -> Root / "obp" / "v7.0.0" / "types" / _ / "requests" => ok } val generic = doc("createAnyType", "POST", "/types/TRANSACTION_REQUEST_TYPE/requests", Some(anyType)) @@ -120,8 +120,8 @@ class ResourceDocRouteSelectionTest extends FeatureSpec with Matchers with Given ResourceDocMatcher.selectByRoute(request(Method.POST, "/obp/v7.0.0/types/SEPA/requests"), List(generic, sepa)) shouldBe Some(sepa) ResourceDocMatcher.selectByRoute(request(Method.POST, "/obp/v7.0.0/types/SEPA/requests"), List(sepa, generic)) shouldBe Some(sepa) - And("a type with no doc of its own gets the generic doc") - ResourceDocMatcher.selectByRoute(request(Method.POST, "/obp/v7.0.0/types/CARDANO/requests"), List(sepa, generic)) shouldBe Some(generic) + And("a type with no doc of its own gets the first doc of the route, here the generic one") + ResourceDocMatcher.selectByRoute(request(Method.POST, "/obp/v7.0.0/types/CARDANO/requests"), List(generic, sepa)) shouldBe Some(generic) } scenario("A request no route serves selects nothing, so it can fall through to an older version", ResourceDocRouteSelectionTag) { @@ -167,17 +167,17 @@ class ResourceDocRouteSelectionTest extends FeatureSpec with Matchers with Given counting.asked shouldBe 1 } - scenario("The docs that share a route are found when the index is built, in the order given", ResourceDocRouteSelectionTag) { + scenario("The docs that share a route are found when the index is built, in the order given, and the first is the fallback", ResourceDocRouteSelectionTag) { val shared = Http4sRoute { case POST -> Root / "obp" / "v7.0.0" / "types" / _ / "requests" => ok } val sepa = doc("createSepa", "POST", "/types/SEPA/requests", Some(shared)) val generic = doc("createAnyType", "POST", "/types/TRANSACTION_REQUEST_TYPE/requests", Some(shared)) val index = ResourceDocMatcher.buildRouteIndex(List(sepa, generic)) ResourceDocMatcher.selectByRoute(request(Method.POST, "/obp/v7.0.0/types/SEPA/requests"), index) shouldBe Some(sepa) - ResourceDocMatcher.selectByRoute(request(Method.POST, "/obp/v7.0.0/types/CARDANO/requests"), index) shouldBe Some(generic) + ResourceDocMatcher.selectByRoute(request(Method.POST, "/obp/v7.0.0/types/CARDANO/requests"), index) shouldBe Some(sepa) } - scenario("When no template of the docs that share a route matches, the first of them is used", ResourceDocRouteSelectionTag) { + scenario("When no doc of the docs that share a route has a word of the request, the first of them is used", ResourceDocRouteSelectionTag) { val shared = Http4sRoute { case POST -> Root / "obp" / "v7.0.0" / "types" / _ / "requests" => ok } val sepa = doc("createSepa", "POST", "/types/SEPA/requests", Some(shared)) val generic = doc("createAnyType", "POST", "/types/TRANSACTION_REQUEST_TYPE/requests", Some(shared)) @@ -186,23 +186,30 @@ class ResourceDocRouteSelectionTest extends FeatureSpec with Matchers with Given } } - feature("ResourceDocMiddleware - a group whose docs all carry their route") { + feature("ResourceDocMiddleware - every doc carries its route") { scenario("A route that is null is reported by name when the middleware is built", ResourceDocRouteSelectionTag) { val declaredTooLate = doc("getDeclaredTooLate", "GET", "/late", Some(null.asInstanceOf[Http4sRoute])) - val thrown = the[IllegalStateException] thrownBy ResourceDocMiddleware.apply(ArrayBuffer(declaredTooLate))(HttpRoutes.empty[IO]) + val thrown = the[IllegalStateException] thrownBy ResourceDocMiddleware.apply(ArrayBuffer(declaredTooLate)) thrown.getMessage should include("getDeclaredTooLate") } + scenario("A doc with no route at all is reported by name when the middleware is built", ResourceDocRouteSelectionTag) { + val noRoute = doc("getNoRoute", "GET", "/none", None) + + val thrown = the[IllegalStateException] thrownBy ResourceDocMiddleware.apply(ArrayBuffer(noRoute)) + thrown.getMessage should include("getNoRoute") + } + scenario("A request no route serves is passed on without running anything of this group", ResourceDocRouteSelectionTag) { - var ranTheRoutes = false - val routes = HttpRoutes.of[IO] { case _ => IO { ranTheRoutes = true }.as(Response[IO]()) } - val thing = doc("getThing", "GET", "/things", Some(Http4sRoute { case GET -> Root / "obp" / "v7.0.0" / "things" => ok })) - val middleware = ResourceDocMiddleware.apply(ArrayBuffer(thing))(routes) + var ranARoute = false + val thing = doc("getThing", "GET", "/things", + Some(Http4sRoute { case GET -> Root / "obp" / "v7.0.0" / "things" => IO { ranARoute = true }.as(Response[IO]()) })) + val middleware = ResourceDocMiddleware.apply(ArrayBuffer(thing)) middleware.run(request(Method.GET, "/obp/v7.0.0/no-such-thing")).value.unsafeRunSync() shouldBe None - ranTheRoutes shouldBe false + ranARoute shouldBe false } } } From 72fff218f7b21d54b4f1c61c299438914e171c8e Mon Sep 17 00:00:00 2001 From: Hongwei Date: Wed, 7 Oct 2026 17:46:19 +0200 Subject: [PATCH 22/29] docs: describe route-bound ResourceDocs for every version Rewrite the guidance on declaring an endpoint, the route chain, docs that share a route and the empty path segment gotcha, now that no version matches docs by URL template. --- CLAUDE.md | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 38be32eee3..b2f658d285 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -29,7 +29,7 @@ Rules apply regardless of which version file the endpoint lives in. Use v7.0.0 o ### Rule 1 — ResourceDoc registration ```scala // Declare val FIRST, then register — see Rule 5 why order matters -val myEndpoint: HttpRoutes[IO] = HttpRoutes.of[IO] { ... } +val myEndpoint: Http4sRoute = Http4sRoute { ... } resourceDocs += ResourceDoc( implementedInApiVersion, // first param; ResourceDoc.partialFunction (OBPEndpoint) was removed in the Lift teardown @@ -45,7 +45,7 @@ resourceDocs += ResourceDoc( ### Rule 2 — Endpoint signature ```scala -val myEndpoint: HttpRoutes[IO] = HttpRoutes.of[IO] { +val myEndpoint: Http4sRoute = Http4sRoute { case req @ GET -> `prefixPath` / "some" / "path" => EndpointHelpers.executeAndRespond(req) { cc => for { ... } yield json // no HttpCode wrapper @@ -83,8 +83,10 @@ EndpointHelpers.withCounterparty(req) { (user, account, view, cp, cc) => ... } / **PUT → 200**: `executeFutureWithBody[B,A]` / `withUserAndBody[B,A]` / `withUserAndBankAndBody[B,A]` **DELETE → 204**: `executeDelete` / `withUserDelete` / `withUserAndBankDelete` -### Rule 5 — `allRoutes` ordering invariant (critical) -`val myEndpoint` MUST be declared BEFORE its `resourceDocs +=` line. If reversed, Scala's initializer stores `Some(null)` → NPE kills the entire `baseServices` chain → every request returns 500, including v6 fallback routes. +### Rule 5 — declare the route before its doc, and list it in the route chain (critical) +`val myEndpoint` MUST be declared BEFORE its `resourceDocs +=` line, or be a `lazy val`. If reversed, Scala's initializer stores `Some(null)` in the doc and `ResourceDocMiddleware` refuses to build, naming the endpoint (`The route of ... is missing or null`). + +Add the route to the version's `routesInOrder` list. The list is the order the routes are tried in, and `orderedResourceDocs` (built by `ResourceDocMatcher.orderByRoutes`) puts the docs in that same order, so the doc that validates a request is always the doc of the route that serves it. A doc whose route is missing from the list is an error at startup, naming the doc. A route that is deliberately run before the middleware (v1.2.1 `bankById`, so an unknown bank answers 400, not 464) is passed as `outsideTheChain`; a doc with no route documents an endpoint served somewhere else (the message-docs Swagger) and is left out of the middleware's docs. ## Tricky Parts (Gotchas) @@ -137,7 +139,7 @@ But: if the inline check uses the **same** role as the doc (e.g. v5 `createAccou **`NewStyle.function.getBankAccount` returns 404**: The `unboxFullOrFail` inside hardcodes code 404. When your endpoint must return 400 for a missing account (e.g. v1.2.1 tests), bypass it: use `Connector.connector.vend.checkBankAccountExists(bankId, accountId, cc)` then `Future { unboxFullOrFail(rawBox, cc, msg) }` — the default code is 400. -**Middleware URL template bypass** (non-standard uppercase vars): `validateAccount` checks `pathParams.get("ACCOUNT_ID")` and `validateView` checks `pathParams.get("VIEW_ID")` by exact key. Any other all-caps segment (e.g. `BANK_ACCOUNT_ID`, `CUSTOM_VIEW_ID`, `GRANT_VIEW_ID`, `NEW_ACCOUNT_ID`, `VIEW_ACCOUNT_ID`, `UPD_VIEW_ID`) is still matched as a template variable (wildcard) but skips the 404/403 validation. Use this when your handler does inline validation returning 400 but middleware would return 404 or 403 first. +**Middleware URL template bypass** (non-standard uppercase vars): `validateAccount` checks `pathParams.get("ACCOUNT_ID")` and `validateView` checks `pathParams.get("VIEW_ID")` by exact key. Any other all-caps segment (e.g. `BANK_ACCOUNT_ID`, `CUSTOM_VIEW_ID`, `GRANT_VIEW_ID`, `NEW_ACCOUNT_ID`, `VIEW_ACCOUNT_ID`, `UPD_VIEW_ID`) is not read by the middleware, so the route pattern alone decides what the segment matches and the 404/403 validation is skipped. Use this when your handler does inline validation returning 400 but middleware would return 404 or 403 first. For IO-based handlers that bypass `ACCOUNT_ID`, look up the account inline and return 400 for missing accounts (matching Lift behaviour): ```scala @@ -203,7 +205,7 @@ for tc in t.findall('testcase'): ``` The `` element's *text* contains the full stack trace + the lift-json `MappingException` body dump — read that when the message alone (`"500 did not equal 400"`) isn't enough to find the failing assertion. -**Empty path segments fall into http4s patterns that should reject them**: A Lift test like `getSystemView("")` builds URL `/system-views/`. http4s's `Path` keeps the trailing empty segment, so `case GET -> prefixPath / "system-views" / viewIdStr` matches with `viewIdStr = ""`. Meanwhile `ResourceDocMatcher.matchesUrlTemplate` filters empty segments via `.split("/").filter(_.nonEmpty)`, so the matcher sees 1 segment vs the template's 2 — no doc match → middleware skips auth/role validation and falls through to your handler with `viewIdStr = ""`. The handler then throws inside the business logic → 500 (test expected 401/403 from middleware). Fix: add a pattern guard so empty viewId doesn't match and the request falls through to `notFoundCatchAll` (JSON 404): `case req @ GET -> prefixPath / "system-views" / viewIdStr if viewIdStr.nonEmpty =>`. Apply to GET/PUT/DELETE variants. +**Empty path segments fall into http4s patterns that should reject them**: A Lift test like `getSystemView("")` builds URL `/system-views/`. http4s's `Path` keeps the trailing empty segment, so `case GET -> prefixPath / "system-views" / viewIdStr` matches with `viewIdStr = ""`. The middleware selects the doc of the route that serves the request, so that request IS validated, with an empty id (`extractPathParams` keeps the empty segment, so BANK_ID and the others carry the empty value). If the business logic then cannot cope with an empty id, add a pattern guard so it does not match and the request falls through to `notFoundCatchAll` (JSON 404): `case req @ GET -> prefixPath / "system-views" / viewIdStr if viewIdStr.nonEmpty =>`. Apply to GET/PUT/DELETE variants. **Throwing a `RuntimeException` in Lift returns 500, not 400**: When porting Lift code like: ```scala @@ -241,11 +243,11 @@ EndpointHelpers.executeFutureCreated(req) { ``` Note: `executeFutureCreated` returns 201; pair it with `cc.user.openOrThrowException(...)` / `cc.bank.getOrElse(...)` since middleware has already validated auth/bank. -**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. +**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 does not read it and skips the lookup, and the route pattern still matches the path. The handler can check "already exists" inline with `Connector.connector.vend.checkBankAccountExists(...)` and return 409/400 as needed. -**Reserved ALL_CAPS literals — don't use them as placeholders**: `ResourceDocMatcher` in `Http4sSupport.scala` treats every value of the `TransactionRequestTypes` and `StrongCustomerAuthentication` enums (plus `NOT_EMAIL_NEITHER_SMS`) as a **literal** URL segment: `literalAllCapsSegments` is built from those enums, so adding a transaction-request type to the enum is all it takes. Any other ALL_CAPS segment is a wildcard. Until 2026-10 the set was kept by hand, and `MOBILE_WALLET`, `UTILITY`, `BULK`, `HOLD`, `CARDANO` and the `ETH_*` types were never added: the v7 MOBILE_WALLET template then claimed every transaction-request type (UTILITY and BULK were validated under the MOBILE_WALLET doc; HOLD/CARDANO/ETH on v7 got an empty 404 instead of falling through to v6). When several templates match, the one with the most literal segments wins, whatever the registration order. `ResourceDocSelfResolveTest` checks that every registered doc's own URL resolves to that doc. If you migrate an endpoint whose URL template uses an enum value 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). +**Capitalised words in a ResourceDoc template are documentation, not matching rules**: the middleware never compares the request URL with a template. It asks the routes which one serves the request, and the doc of that route validates it. Until 2026-10 it compared the URL with the templates and guessed that a capitalised segment was a placeholder, with a hand-kept list of exceptions; a transaction-request type missing from that list (`MOBILE_WALLET`, `HOLD`, `CARDANO`, ...) made its template claim other types' requests, and a template made only of placeholders (Berlin Group `getPaymentInformation`) claimed every URL of its length. So an enum value or any other capitalised word can be used in a template freely. What the middleware does read from a template are the positions of `BANK_ID`, `ACCOUNT_ID`, `VIEW_ID` and `COUNTERPARTY_ID` (`ResourceDocMatcher.extractPathParams`). When several docs share one route (v4.0.0 `createTransactionRequest`, v3.1.0 `createConsent`), the doc that agrees with the request in the most segments is used, and the first doc of the route when none does; list the doc that should validate the types without a doc of their own first. `getUsersByEmail`-style templates (`/users/email/EMAIL/terminator`) need no rename. -**A ResourceDoc is bound to its route (v7.0.0 so far)**: declare an endpoint as `val x: Http4sRoute = Http4sRoute { case req @ GET -> ... => ... }` (not `HttpRoutes.of[IO]`) and put it in the doc's `http4sPartialFunction = Some(x)`. `ResourceDocMiddleware` then picks the doc by asking the routes whether they serve the request (`ResourceDocMatcher.selectByRoute`, docs tried in the order the version passes them, the same order its `allRoutes` uses) and runs only that route, so the doc that validates a request is always the doc of the handler that serves it. For such docs the ALL_CAPS literal-vs-placeholder rule above is not consulted: the template is documentation (resource-docs, Swagger) and the route pattern decides. `ResourceDocRouteBindingTest` checks that each converted doc's own URL is served by its own route. Versions not yet converted still use the template matching described above; convert one version file at a time, and add the version to `ResourceDocRouteBindingTest.convertedCatalogs`. +**A ResourceDoc is bound to its route**: declare an endpoint as `val x: Http4sRoute = Http4sRoute { case req @ GET -> ... => ... }` (not `HttpRoutes.of[IO]`), put it in the doc's `http4sPartialFunction = Some(x)` and in the version's `routesInOrder` list. `ResourceDocMiddleware` picks the doc by asking the routes whether they serve the request (`ResourceDocMatcher.selectByRoute`, over `orderedResourceDocs`) and runs only that route, so the doc that validates a request is always the doc of the handler that serves it; a request no route serves is passed on to the next link of the chain. This holds for every version: v1.2.1 to v7.0.0, Berlin Group v1.3 and v2, UK Open Banking v2.0.0, v3.1.0 and v4.0.1. The path-rewriting bridges ask `selectByRoute` whether their version serves a path. `ResourceDocRouteBindingTest` checks, for every catalog, that each doc's own URL is served by its own route and that selection returns that doc; a doc whose template documents a different URL from the one its route serves (the Lift baseline's `/search/warehouse` is served at `/search/warehouse/{query}`) needs an entry in its `sampleUrlOf`. When you add a version or a Berlin Group / UK Open Banking group, add its `orderedResourceDocs` to the test's `convertedCatalogs`. **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. From 8553413e7a24ab3190ac86af61884fae0a10d00b Mon Sep 17 00:00:00 2001 From: Hongwei Date: Wed, 7 Oct 2026 18:03:16 +0200 Subject: [PATCH 23/29] fix: document the query segment of the v2.0.0 search endpoints elasticSearchWarehouse and elasticSearchMetrics documented /search/warehouse and /search/metrics, but their routes serve /search/warehouse/{query} and /search/metrics/{query}: the query is a path segment, so the documented URL could not be called. Matching docs by template hid this, because the template validated a URL no route served. The docs now name the segment, and the parity allowlist records the difference from the Lift baseline. --- .../main/scala/code/api/v2_0_0/Http4s200.scala | 4 ++-- .../http4s/ResourceDocRouteBindingTest.scala | 10 +++------- .../resource_doc_baseline/parity_allowlist.json | 16 ++++++++++++++++ 3 files changed, 21 insertions(+), 9 deletions(-) diff --git a/obp-api/src/main/scala/code/api/v2_0_0/Http4s200.scala b/obp-api/src/main/scala/code/api/v2_0_0/Http4s200.scala index 12e9846b36..de9cdbef31 100644 --- a/obp-api/src/main/scala/code/api/v2_0_0/Http4s200.scala +++ b/obp-api/src/main/scala/code/api/v2_0_0/Http4s200.scala @@ -1416,7 +1416,7 @@ object Http4s200 { resourceDocs += ResourceDoc( implementedInApiVersion, nameOf(elasticSearchWarehouse), "GET", - "/search/warehouse", + "/search/warehouse/QUERY", "Search Warehouse Data Via Elasticsearch", """ |Search warehouse data via Elastic Search. @@ -1508,7 +1508,7 @@ object Http4s200 { resourceDocs += ResourceDoc( implementedInApiVersion, nameOf(elasticSearchMetrics), "GET", - "/search/metrics", + "/search/metrics/QUERY", "Search API Metrics via Elasticsearch", """ |Search the API calls made to this API instance via Elastic Search. diff --git a/obp-api/src/test/scala/code/api/util/http4s/ResourceDocRouteBindingTest.scala b/obp-api/src/test/scala/code/api/util/http4s/ResourceDocRouteBindingTest.scala index fed0322bc2..8cb2023a2f 100644 --- a/obp-api/src/test/scala/code/api/util/http4s/ResourceDocRouteBindingTest.scala +++ b/obp-api/src/test/scala/code/api/util/http4s/ResourceDocRouteBindingTest.scala @@ -82,14 +82,10 @@ class ResourceDocRouteBindingTest extends ServerSetup { /** * Docs whose route serves a longer or narrower URL than the template documents, or accepts only some * values of a segment, so the template text itself is not a request the route serves. The documented - * URL is kept as it is (resource-docs and Swagger show it); this gives the URL to test instead. + * URL is kept as it is (resource-docs and Swagger show it); this gives the URL to test instead. Empty + * today: a doc that disagrees with its route should be fixed, as v2.0.0's search docs were. */ - private val sampleUrlOf: Map[String, String] = Map( - // Lift documented /search/warehouse, and served /search/warehouse/{query} - "elasticSearchWarehouse" -> "/search/warehouse/QUERY", - "elasticSearchMetrics" -> "/search/metrics/QUERY", - // (the Berlin Group payment routes accept only real payment services and products, see below) - ) + private val sampleUrlOf: Map[String, String] = Map.empty private def requestFor(doc: ResourceDoc): Request[IO] = { val version = doc.implementedInApiVersion diff --git a/scripts/resource_doc_baseline/parity_allowlist.json b/scripts/resource_doc_baseline/parity_allowlist.json index 5b239b260f..e73863868c 100644 --- a/scripts/resource_doc_baseline/parity_allowlist.json +++ b/scripts/resource_doc_baseline/parity_allowlist.json @@ -1906,6 +1906,22 @@ "reason": "Lists the accepted attribute types from AttributeTypeDocs.typeFieldDescription, which adds DECIMAL and BOOLEAN (new enum values) to the four the Lift text listed and says when to use DECIMAL.", "lift_digest": "d2f72f200b8c01cb57f1e07c608a160c125d3e210f9282fbbfa532c72f802248", "http4s_digest": "789c5598a7af5691282a4192dc5d38199bb068928a392b08314aab3b96d5b6af" + }, + { + "version": "v2_0_0", + "endpoint": "elasticSearchWarehouse", + "field": "requestUrl", + "reason": "The Lift doc named /search/warehouse but the route serves /search/warehouse/{query}: the query is a path segment, so the documented URL could not be called. The URL now includes it.", + "lift_digest": "98b4712c7a2371c331975ff8bf843b479fad54b5d7ca5fb93bc7dd73a6b28f98", + "http4s_digest": "fa25345774e514d9be46005801cf85283aa6931854ad5c688da887d34774a945" + }, + { + "version": "v2_0_0", + "endpoint": "elasticSearchMetrics", + "field": "requestUrl", + "reason": "The Lift doc named /search/metrics but the route serves /search/metrics/{query}: the query is a path segment, so the documented URL could not be called. The URL now includes it.", + "lift_digest": "1132e0dd343990bc0b4b0258f46e9b13bcda43939f6cb99d6b4dc6b34f5080e2", + "http4s_digest": "b79574c091ae746fdd0985366c387504e1a0fc0403438f4be6f64de9d7739585" } ] } From 338a01c7bacbb4559b1c4da5025fc446c4512ded Mon Sep 17 00:00:00 2001 From: Hongwei Date: Wed, 7 Oct 2026 19:21:23 +0200 Subject: [PATCH 24/29] fix: authenticate a dynamic entity write before its transaction opens Authenticating a consent request writes: it creates the consent's user and copies the consent's Roles onto it. Http4sDynamicEntity authenticated inside the write transaction, so those writes were uncommitted when the handler's own checks read them on other connections: a consent that carried the Role was refused with 403 (OBP-20006), and a personal row was owned by the consent user instead of the human who granted the consent, so the consent could not read back what it wrote. The first link of the version chain used to authenticate the request outside any transaction and committed those writes as a side effect; every version now serves only the requests its routes serve, so nothing did. Resolve the caller before the transaction opens, as ResourceDocMiddleware does for every other endpoint. DynamicEntityConsentUserTest covers it: two scenarios failed on the shard that runs v6.0.0. --- .../dynamic/entity/Http4sDynamicEntity.scala | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/obp-api/src/main/scala/code/api/dynamic/entity/Http4sDynamicEntity.scala b/obp-api/src/main/scala/code/api/dynamic/entity/Http4sDynamicEntity.scala index a3a48f7723..750e34d955 100644 --- a/obp-api/src/main/scala/code/api/dynamic/entity/Http4sDynamicEntity.scala +++ b/obp-api/src/main/scala/code/api/dynamic/entity/Http4sDynamicEntity.scala @@ -948,12 +948,29 @@ object Http4sDynamicEntity extends MdcLoggable { val reqWithCc = req.withAttribute(Http4sRequestAttributes.callContextKey, cc) val io = handler(reqWithCc) if (req.method == Method.GET || req.method == Method.HEAD) io - else RequestScopeConnection.withBusinessDBTransaction(io) + else resolveCallerOutsideTheTransaction(cc) *> RequestScopeConnection.withBusinessDBTransaction(io) } } } } + /** + * Authenticate the caller before the write transaction opens. + * + * Authenticating a consent request writes: it creates the consent's user and copies the consent's + * Roles onto it. Inside the transaction those writes stay uncommitted, and the checks the handler runs + * next read on other connections, so they would not see them: the consent's Role looks missing (403), + * and personal rows are owned by the consent user instead of the human who granted the consent. Done + * first, outside any transaction, the writes are committed by the time the handler reads them. The + * handler still authenticates for itself; this pass only has to leave the committed state behind, + * so its outcome is not used and a failure is left for the handler to report. + * + * ResourceDocMiddleware does the same for every other endpoint (it validates before it opens a + * transaction); this service builds its own request scope, so it has to do it here. + */ + private def resolveCallerOutsideTheTransaction(cc: CallContext): IO[Unit] = + IO.fromFuture(IO(code.api.util.APIUtil.resolveCallerWithoutRateLimiting(cc))).attempt.void + /** Entry point wired into Http4sApp.baseServices (before the Lift bridge). */ lazy val wrappedRoutesDynamicEntity: HttpRoutes[IO] = Kleisli[HttpF, Request[IO], Response[IO]] { (req: Request[IO]) => From 02c9728bc8f21049caba03f699dabf2de424f713 Mon Sep 17 00:00:00 2001 From: Hongwei Date: Wed, 7 Oct 2026 19:31:09 +0200 Subject: [PATCH 25/29] refactor: remove the once-per-request caller holder The holder shared a caller resolved by the first version hop that had no ResourceDoc for a request. Every version group now passes on a request none of its routes serves without resolving anything, so nothing fills or reads it. Update the doc comment of resolveCallerWithoutRateLimiting, whose one user is now the dynamic entity service. --- .../main/scala/code/api/util/APIUtil.scala | 14 ++++++------- .../code/api/util/http4s/Http4sApp.scala | 10 +--------- .../code/api/util/http4s/Http4sSupport.scala | 20 ------------------- 3 files changed, 8 insertions(+), 36 deletions(-) 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 8c540e515c..19a1ac00a6 100644 --- a/obp-api/src/main/scala/code/api/util/APIUtil.scala +++ b/obp-api/src/main/scala/code/api/util/APIUtil.scala @@ -3268,14 +3268,14 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{ * Resolve the caller (user and Consumer) from the request credentials WITHOUT applying rate * limiting: the call is neither refused for exceeding a limit nor counted against one. * - * For the http4s version fallthrough chain only (ResourceDocMiddleware.resolveCallerOnce). A hop - * that has no ResourceDoc for the request is almost always about to pass it on to the next - * version, and the hop that finally serves it applies rate limiting itself through - * [[anonymousAccess]] / [[applicationAccess]]. Counting the call on every hop charged one unit per - * hop: `GET /obp/v5.1.0/users/current` is served by v3.0.0 after six hops, so it cost seven - * units and a Consumer with a per-second limit below seven could never call it (429 OBP-10018). + * For a service that opens its own write transaction and so cannot leave the authentication to + * ResourceDocMiddleware (which validates before it opens one): authenticating a consent request + * writes (the consent's user and Roles), and those writes must be committed before the transaction + * opens. Not counting the call against a rate limit is deliberate: the handler authenticates again + * and applies the limit through [[anonymousAccess]] / [[applicationAccess]], so counting here too + * would charge the call twice. * - * A Failure is returned in the Box, never thrown; the middleware decides what to do with it. + * A Failure is returned in the Box, never thrown; the caller decides what to do with it. */ def resolveCallerWithoutRateLimiting(cc: CallContext): OBPReturnType[Box[User]] = accessPipeline(cc, applyRateLimiting = false) diff --git a/obp-api/src/main/scala/code/api/util/http4s/Http4sApp.scala b/obp-api/src/main/scala/code/api/util/http4s/Http4sApp.scala index 0bdbc3277c..713ec7d86e 100644 --- a/obp-api/src/main/scala/code/api/util/http4s/Http4sApp.scala +++ b/obp-api/src/main/scala/code/api/util/http4s/Http4sApp.scala @@ -145,16 +145,8 @@ object Http4sApp extends MdcLoggable { } } - // One empty holder per incoming request for the caller resolved on a no-ResourceDoc hop, shared by - // every link of the fallthrough chain below (see ResourceDocMiddleware.resolveCallerOnce). Bridges - // rewrite the URI between hops with `req.withUri`, which keeps attributes, so the holder travels - // the chain and dies with the request. - private def installCallerResolvedOnThisRequest(req: Request[IO]): Request[IO] = - if (req.attributes.lookup(Http4sRequestAttributes.callerResolvedOnThisRequestKey).isDefined) req - else req.withAttribute(Http4sRequestAttributes.callerResolvedOnThisRequestKey, Http4sRequestAttributes.newCallerResolvedOnThisRequest) - private def baseServices: HttpRoutes[IO] = Kleisli[HttpF, Request[IO], Response[IO]] { req: Request[IO] => - OptionT.liftF(cacheBodyOnce(req).map(installCallerResolvedOnThisRequest)).flatMap { req => + OptionT.liftF(cacheBodyOnce(req)).flatMap { req => corsHandler.run(req) .orElse(AppsPage.routes.run(req)) .orElse(StatusPage.routes.run(req)) diff --git a/obp-api/src/main/scala/code/api/util/http4s/Http4sSupport.scala b/obp-api/src/main/scala/code/api/util/http4s/Http4sSupport.scala index cfd791906c..9294ceceed 100644 --- a/obp-api/src/main/scala/code/api/util/http4s/Http4sSupport.scala +++ b/obp-api/src/main/scala/code/api/util/http4s/Http4sSupport.scala @@ -117,26 +117,6 @@ object Http4sRequestAttributes { val callerCertificateTrustKey: Key[code.api.util.PeerTrust.Resolution] = Key.newKey[IO, code.api.util.PeerTrust.Resolution].unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - /** Outcome of resolving the caller from the request credentials: the user (or a Failure) and the - * CallContext enriched by that resolution (Consumer, session, rate-limit config, ...). */ - type ResolvedCaller = (Box[User], Option[CallContext]) - - /** - * The caller resolved on THIS request by a fallthrough hop that has no ResourceDoc for it (see - * ResourceDocMiddleware.resolveCallerOnce). Http4sApp attaches one empty holder to each incoming - * request; the first such hop fills it and every later link of the version fallthrough chain - * reads it instead of re-validating the credentials (JWKS lookups, Consumer lookup and save). - * The holder is a request attribute: it lives in memory for the duration of that one request, - * is never stored anywhere else and is unreachable from any other request. When it is absent - * (e.g. the middleware used on its own in a test) the middleware still resolves, just on every hop. - */ - val callerResolvedOnThisRequestKey: Key[java.util.concurrent.atomic.AtomicReference[Option[ResolvedCaller]]] = - Key.newKey[IO, java.util.concurrent.atomic.AtomicReference[Option[ResolvedCaller]]].unsafeRunSync()(cats.effect.unsafe.IORuntime.global) - - def newCallerResolvedOnThisRequest: java.util.concurrent.atomic.AtomicReference[Option[ResolvedCaller]] = - new java.util.concurrent.atomic.AtomicReference[Option[ResolvedCaller]](None) - - /** * Implicit class that adds .callContext accessor to Request[IO]. * From 4fc8760bd1932c8e2dc67ec222b184ec42847e33 Mon Sep 17 00:00:00 2001 From: Hongwei Date: Wed, 7 Oct 2026 21:19:37 +0200 Subject: [PATCH 26/29] docs: drop references to the removed template matching The plan documents, the AuthSweep catalog comments and the 64KB note in CLAUDE.md still named literalAllCapsSegments and isTemplateVariable, or told the reader to add a transaction-request type to that set. A doc is now selected by the route that serves the request, so no list needs updating. --- CLAUDE.md | 2 +- DYNAMIC_ENTITY_SPACE_MODEL_PLAN.md | 6 +++--- OPEN_CORRIDOR_INTERFACE_C_PUBLISH_PLAN.md | 8 ++++---- .../scala/code/api/sweep/EndpointCatalog.scala | 14 ++++++++------ 4 files changed, 16 insertions(+), 14 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index b2f658d285..6da0b76baa 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -267,7 +267,7 @@ How to find overrides for a version: grep `lazy val (\w+)` in the target `APIMet Symptoms in tests: a v4-specific assertion fails (e.g. an entitlement should-be-granted check returns false). The HTTP response is usually a successful 200/201, just from the wrong handler — so it can look like a flaky failure on the surface. -**JVM 64KB `` limit in per-version files**: around ~140 endpoints, an `Http4sXxx` object's `` exceeds the JVM 64KB-bytecode-per-method limit and won't compile. Adopt from the start (don't wait for the wall): (1) declare endpoints as `lazy val xxx: HttpRoutes[IO] = HttpRoutes.of[IO] { ... }` (not `val`) so lambda materialisation moves out of `` into per-field `lzycompute` methods; (2) group `resourceDocs += ResourceDoc(...)` calls into `private def initXxxResourceDocs(): Unit` blocks of ~10–15 endpoints, each called once from the object body. Each helper def gets its own 64KB budget. (Pattern shipped in `Http4s600.scala`.) +**JVM 64KB `` limit in per-version files**: around ~140 endpoints, an `Http4sXxx` object's `` exceeds the JVM 64KB-bytecode-per-method limit and won't compile. Adopt from the start (don't wait for the wall): (1) declare endpoints as `lazy val xxx: Http4sRoute = Http4sRoute { ... }` (not `val`) so lambda materialisation moves out of `` into per-field `lzycompute` methods; (2) group `resourceDocs += ResourceDoc(...)` calls into `private def initXxxResourceDocs(): Unit` blocks of ~10–15 endpoints, each called once from the object body. Each helper def gets its own 64KB budget. (Pattern shipped in `Http4s600.scala`.) **`isStatisticallyTooPermissive` is sample-pool-dependent**: a fresh local test DB with a single user trips the ABAC-permissiveness check and causes spurious rejections. Seed enough users in any test exercising ABAC rules — it's a test-data issue, not a regression. diff --git a/DYNAMIC_ENTITY_SPACE_MODEL_PLAN.md b/DYNAMIC_ENTITY_SPACE_MODEL_PLAN.md index 9638603e67..5211370a95 100644 --- a/DYNAMIC_ENTITY_SPACE_MODEL_PLAN.md +++ b/DYNAMIC_ENTITY_SPACE_MODEL_PLAN.md @@ -334,11 +334,11 @@ writing. `/obp/dynamic-entity/…` keeps serving unchanged throughout; the two r by every endpoint in OBP, so relaxing it there would let `/banks/SYS/accounts` past bank validation for endpoints that genuinely need a bank, and they would fail further in with something worse than a 404. Instead the Dynamic Entity ResourceDocs declare their template with `SPACE_ID`, a - non-standard all-caps variable the matcher treats as a wildcard and the middleware skips — the + non-standard all-caps variable the middleware does not read — the documented bypass in CLAUDE.md, already used by `FIREHOSE_BANK_ID` and `NEW_ACCOUNT_ID`. The handler then calls the resolver itself, in place of today's `bankCheck` - (`Http4sDynamicEntity.scala:173`). `SPACE_ID` is not in `ResourceDocMatcher.literalAllCapsSegments`, - so nothing else has to change. + (`Http4sDynamicEntity.scala:173`). The middleware only reads `BANK_ID`, `ACCOUNT_ID`, `VIEW_ID` and + `COUNTERPARTY_ID` from a template, so nothing else has to change. The served URL is unaffected: a caller still writes `/banks/obp1/...` or `/banks/SYS/...`; only the doc's template variable is named differently, which is what the middleware matches on. diff --git a/OPEN_CORRIDOR_INTERFACE_C_PUBLISH_PLAN.md b/OPEN_CORRIDOR_INTERFACE_C_PUBLISH_PLAN.md index b24839e154..d62e7647c5 100644 --- a/OPEN_CORRIDOR_INTERFACE_C_PUBLISH_PLAN.md +++ b/OPEN_CORRIDOR_INTERFACE_C_PUBLISH_PLAN.md @@ -250,10 +250,10 @@ Rename inventory (OBP-API): - prop name `transactionRequests_challenge_threshold_OPEN_CORRIDOR` → `..._OPEN_CORRIDOR_PROMISE`; - `OpenCorridorProcessor.scala:42`'s type constant; -- **add both new names to `literalAllCapsSegments` in `Http4sSupport.scala`** - so the ResourceDoc matcher treats them as literal path segments, not - wildcards. (Latent quirk found 2026-07-18: `OPEN_CORRIDOR` itself is - missing from that set today — the rename must not reproduce that.) +- no matcher list to update: a ResourceDoc is selected by the route that + serves the request, so a new transaction-request type needs only its route + pattern and its doc. (The old `literalAllCapsSegments` set, which `OPEN_CORRIDOR` + was missing from, was removed when docs were bound to their routes.) - tests + any dev/test data rows carrying the old type string. Bank Node side: its submit-TR client URL and any internal type strings diff --git a/obp-api/src/test/scala/code/api/sweep/EndpointCatalog.scala b/obp-api/src/test/scala/code/api/sweep/EndpointCatalog.scala index 35f42840a7..ae4308bf41 100644 --- a/obp-api/src/test/scala/code/api/sweep/EndpointCatalog.scala +++ b/obp-api/src/test/scala/code/api/sweep/EndpointCatalog.scala @@ -102,10 +102,11 @@ object EndpointCatalog { * failure" that is entirely the sweep's own doing. The first run of AuthSweepTest hit exactly * that on nine endpoints across v2.1.0 and v3.1.0. * - * The production rule lives in ResourceDocMatcher.isTemplateVariable, which consults a private - * `literalAllCapsSegments` set. Copying that set here would give us a second copy to keep in - * sync, and a stale copy fails in the direction that is hardest to notice — a literal newly - * added there would be substituted here and the sweep would quietly stop covering that path. + * Production does not decide this from the template: ResourceDocMiddleware asks the routes which + * one serves a request, so a template's capitalised words are documentation. The sweep still has + * to decide which segments to fill in, and keeping a list of literals would give it a copy to + * keep in sync, which fails in the direction that is hardest to notice — a literal newly added + * would be substituted and the sweep would quietly stop covering that path. * * So this asks the opposite question: rather than "is it a literal", "do I have a value for * it". A segment is substituted only when its NAME says it is an id or a code. That happens to @@ -141,8 +142,9 @@ object EndpointCatalog { // an accident of the present call sites, not the actual contract of the segment. seg == "PAYMENT_SERVICE" || seg == "PAYMENT_PRODUCT" || seg == "SCA_METHOD") - // Checked against Http4sSupport's literalAllCapsSegments: not one of the sixteen ends in ID, - // _CODE or _NAME, so the rule above separates the two sets cleanly. + // Checked against the TransactionRequestTypes and StrongCustomerAuthentication enum values, the fixed + // words these templates carry: not one of the sixteen ends in ID, _CODE or _NAME, so the rule above + // separates the two sets cleanly. // // CARDANO, MOBILE_WALLET and ETH_SEND_TRANSACTION are the remaining literals this list does // not carry, and they stay verbatim on purpose -- substituting over them would route to the From f6b702cb351f38f96934c95416ec127f057a3538 Mon Sep 17 00:00:00 2001 From: Hongwei Date: Wed, 7 Oct 2026 21:22:32 +0200 Subject: [PATCH 27/29] refactor: rename the http4sbridge test package to http4sserver The Lift bridge it was named for no longer exists, and its one suite is Http4sServerIntegrationTest. Move the package and update the CI shard filters, shard names, the local runner, the speed report and the docs that list it. --- .github/scripts/test_speed_report.py | 2 +- .github/workflows/build_container.yml | 8 ++++---- .github/workflows/build_pull_request.yml | 8 ++++---- CLAUDE.md | 2 +- docs/testing/EXTRA_TESTS_TODO.md | 2 +- .../Http4sServerIntegrationTest.scala | 2 +- run_tests_parallel.sh | 6 +++--- 7 files changed, 15 insertions(+), 15 deletions(-) rename obp-api/src/test/scala/code/api/{http4sbridge => http4sserver}/Http4sServerIntegrationTest.scala (99%) diff --git a/.github/scripts/test_speed_report.py b/.github/scripts/test_speed_report.py index 194ff69ddf..5255b23988 100644 --- a/.github/scripts/test_speed_report.py +++ b/.github/scripts/test_speed_report.py @@ -43,7 +43,7 @@ HTTP4S_INTEGRATION_SUITES = { "code.api.v7_0_0.Http4s700RoutesTest", "code.api.v7_0_0.Http4s700TransactionTest", - "code.api.http4sbridge.Http4sServerIntegrationTest", + "code.api.http4sserver.Http4sServerIntegrationTest", "code.api.v5_0_0.Http4s500SystemViewsTest", } diff --git a/.github/workflows/build_container.yml b/.github/workflows/build_container.yml index 8bc7fe3465..f936dbaaf2 100644 --- a/.github/workflows/build_container.yml +++ b/.github/workflows/build_container.yml @@ -119,7 +119,7 @@ jobs: # Shard 3 ~155s v6_0_0 only (isolated after v2_x moved to shard 7) # Shard 4 ~183s v5_1_0 v5_0_0 v3_0_0 # Shard 5 ~193s ResourceDocs v3_1_0 v1_4_0 v1_3_0 - # Shard 6 ~168s v7_0_0 http4sbridge UKOpenBanking + # Shard 6 ~168s v7_0_0 http4sserver UKOpenBanking # Shard 7 ~280s model + views + customer + util + berlin + v2_x # Shard 8 ~240s connector + auth + login + mgmt + metrics + catch-all # Shard 9 ~110s v4_0_0 Dynamic* (6 heavy test classes) @@ -170,10 +170,10 @@ jobs: code.api.v1_4_0 code.api.v1_3_0 - shard: 6 - name: "v7 + http4sbridge + UKOpenBanking" + name: "v7 + http4sserver + UKOpenBanking" test_filter: >- code.api.v7_0_0 - code.api.http4sbridge + code.api.http4sserver code.api.UKOpenBanking - shard: 7 name: "model + views + customer + util + small data + berlin + v2_x" @@ -382,7 +382,7 @@ jobs: SHARD3="code.api.v6_0_0" SHARD4="code.api.v5_1_0 code.api.v5_0_0 code.api.v3_0_0" SHARD5="code.api.ResourceDocs1_4_0 code.api.v3_1_0 code.api.v1_4_0 code.api.v1_3_0" - SHARD6="code.api.v7_0_0 code.api.http4sbridge code.api.UKOpenBanking" + SHARD6="code.api.v7_0_0 code.api.http4sserver code.api.UKOpenBanking" SHARD7="code.model code.views code.customer code.usercustomerlinks \ code.api.util code.errormessages code.atms code.branches \ code.products code.crm code.accountHolder code.api.berlin \ diff --git a/.github/workflows/build_pull_request.yml b/.github/workflows/build_pull_request.yml index 659251b6df..9ac9596f01 100644 --- a/.github/workflows/build_pull_request.yml +++ b/.github/workflows/build_pull_request.yml @@ -113,7 +113,7 @@ jobs: # Shard 3 ~250s v6_0_0 (split from v2_x; v2_x moved to shard 7) # Shard 4 ~232s v5_1_0 + v5_0_0 + v3_0_0 # Shard 5 ~252s ResourceDocs + v3_1_0 + v1_4_0 + v1_3_0 - # Shard 6 ~200s v7_0_0 + http4sbridge + UKOpenBanking + # Shard 6 ~200s v7_0_0 + http4sserver + UKOpenBanking # Shard 7 ~280s model + views + customer + util + berlin + small data + v2_x # Shard 8 ~240s connector + auth + login + mgmt + metrics + catch-all # Shard 9 ~300s v4_0_0 Dynamic* (6 classes: 9 400+ lines each) @@ -164,10 +164,10 @@ jobs: code.api.v1_4_0 code.api.v1_3_0 - shard: 6 - name: "v7 + http4sbridge + UKOpenBanking" + name: "v7 + http4sserver + UKOpenBanking" test_filter: >- code.api.v7_0_0 - code.api.http4sbridge + code.api.http4sserver code.api.UKOpenBanking - shard: 7 name: "model + views + customer + util + small data + berlin + v2_x" @@ -387,7 +387,7 @@ jobs: SHARD3="code.api.v6_0_0" SHARD4="code.api.v5_1_0 code.api.v5_0_0 code.api.v3_0_0" SHARD5="code.api.ResourceDocs1_4_0 code.api.v3_1_0 code.api.v1_4_0 code.api.v1_3_0" - SHARD6="code.api.v7_0_0 code.api.http4sbridge code.api.UKOpenBanking" + SHARD6="code.api.v7_0_0 code.api.http4sserver code.api.UKOpenBanking" SHARD7="code.model code.views code.customer code.usercustomerlinks \ code.api.util code.errormessages code.atms code.branches \ code.products code.crm code.accountHolder code.api.berlin \ diff --git a/CLAUDE.md b/CLAUDE.md index 6da0b76baa..f8b7b45e9c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -290,7 +290,7 @@ Shards are defined per-matrix-entry in `.github/workflows/build_pull_request.yml | `code.api.v6_0_0` | 3 | | `code.api.v5_1_0`, `code.api.v5_0_0`, `code.api.v3_0_0` | 4 | | `code.api.ResourceDocs1_4_0`, `code.api.v3_1_0`, `code.api.v1_4_0`, `code.api.v1_3_0` | 5 | -| `code.api.v7_0_0`, `code.api.http4sbridge`, `code.api.UKOpenBanking` | 6 | +| `code.api.v7_0_0`, `code.api.http4sserver`, `code.api.UKOpenBanking` | 6 | | `code.model`, `code.views`, `code.customer`, `code.usercustomerlinks`, `code.api.util`, `code.errormessages`, `code.atms`, `code.branches`, `code.products`, `code.crm`, `code.accountHolder`, `code.api.berlin`, `code.api.v2_*` | 7 | | `code.connector`, `code.util`, `code.api.Authentication*`, `code.api.dauthTest`, `code.api.DirectLoginTest`, `code.api.gateWayloginTest`, `code.api.OBPRestHelperTest`, `code.entitlement`, `code.bankaccountcreation`, `code.bankconnectors`, `code.container`, `code.management`, `code.metrics`, `code.concurrency` | 8 | | anything else | **8** (catch-all) | diff --git a/docs/testing/EXTRA_TESTS_TODO.md b/docs/testing/EXTRA_TESTS_TODO.md index bc0c2e169f..239a72044e 100644 --- a/docs/testing/EXTRA_TESTS_TODO.md +++ b/docs/testing/EXTRA_TESTS_TODO.md @@ -5,7 +5,7 @@ Tracks dependency bumps where compile + the standard 4-suite smoke test passed, Test suites currently used as the smoke gate: - `code.api.v7_0_0.Http4s700RoutesTest` - `code.api.v7_0_0.Http4s700TransactionTest` -- `code.api.http4sbridge.Http4sServerIntegrationTest` +- `code.api.http4sserver.Http4sServerIntegrationTest` Test DB is H2; many integrations are stubbed or absent. diff --git a/obp-api/src/test/scala/code/api/http4sbridge/Http4sServerIntegrationTest.scala b/obp-api/src/test/scala/code/api/http4sserver/Http4sServerIntegrationTest.scala similarity index 99% rename from obp-api/src/test/scala/code/api/http4sbridge/Http4sServerIntegrationTest.scala rename to obp-api/src/test/scala/code/api/http4sserver/Http4sServerIntegrationTest.scala index 9852f32fe9..c417814902 100644 --- a/obp-api/src/test/scala/code/api/http4sbridge/Http4sServerIntegrationTest.scala +++ b/obp-api/src/test/scala/code/api/http4sserver/Http4sServerIntegrationTest.scala @@ -25,7 +25,7 @@ TESOBE (http://www.tesobe.com/) */ -package code.api.http4sbridge +package code.api.http4sserver import org.json4s._ import code.Http4sTestServer diff --git a/run_tests_parallel.sh b/run_tests_parallel.sh index 9e804e2536..52cd43adfd 100755 --- a/run_tests_parallel.sh +++ b/run_tests_parallel.sh @@ -100,7 +100,7 @@ done # Hardcoded ports collide when several project checkouts run this script at the # same time. The un-injected 8087 even collides WITHIN one run, because the # suites that start Http4sTestServer are split across shards (v5_0_0 in shard 2; -# http4sbridge/v7 in shard 4) — both JVMs would bind the default 8087. +# http4sserver/v7 in shard 4) — both JVMs would bind the default 8087. # So we pick random high free ports per shard. A fixed base + upward scan can't # solve simultaneous launches: at fork time no shard has bound yet, so every # concurrent run picks the same base. Random high range + lsof skip (catches @@ -146,7 +146,7 @@ code.management,code.metrics,code.model,code.views,code.usercustomerlinks,\ code.customer,code.errormessages" # Shard 4 base — auth/login/connector/util plus any packages not in shards 1-3 -S4_BASE="code.api.v5_1_0,code.api.v3_1_0,code.api.http4sbridge,code.api.v7_0_0,\ +S4_BASE="code.api.v5_1_0,code.api.v3_1_0,code.api.http4sserver,code.api.v7_0_0,\ code.api.Authentication,code.api.dauthTest,code.api.DirectLoginTest,\ code.api.gateWayloginTest,code.api.OBPRestHelperTest,code.util,code.connector" @@ -184,7 +184,7 @@ S4_6="code.api.ResourceDocs1_4_0,code.api.util,code.api.berlin,\ code.management,code.metrics,code.model,code.views,code.usercustomerlinks,\ code.customer,code.errormessages" -S5_6="code.api.v5_1_0,code.api.v3_1_0,code.api.http4sbridge,code.api.v7_0_0" +S5_6="code.api.v5_1_0,code.api.v3_1_0,code.api.http4sserver,code.api.v7_0_0" S6_6="code.api.Authentication,code.api.dauthTest,code.api.DirectLoginTest,\ code.api.gateWayloginTest,code.api.OBPRestHelperTest,code.util,code.connector" From 5e403a1275975e0ec45cb6c5fb2b11a2bc54a9d7 Mon Sep 17 00:00:00 2001 From: Hongwei Date: Wed, 7 Oct 2026 21:29:15 +0200 Subject: [PATCH 28/29] docs: bring the status doc and bridge notes in line with route-bound ResourceDocs resource_doc_and_endpoint_consistency_status.md still said the routes are ordered by path segment count, that the middleware matches a doc by URL, and that a request with no matching doc is still tried against the group's routes after resolving the caller. None of that holds now: each version lists its routes in routesInOrder, the doc is the one of the route that serves the request, and a request no route serves is passed on at once. CLAUDE.md said every path-rewriting bridge asks selectByRoute. Only v5.1.0, v6.0.0 and v7.0.0 do; v5.0.0 and below test the path prefix and are chained after their own routes. Say so, including that a disabled endpoint falls to the version below in those. Remove the remaining references to the Lift bridge in the comments of the files this change touched. --- CLAUDE.md | 2 +- ...rce_doc_and_endpoint_consistency_status.md | 29 +++++++++++-------- .../util/http4s/ResourceDocMiddleware.scala | 4 +-- .../scala/code/api/v2_1_0/Http4s210.scala | 2 +- .../scala/code/api/v4_0_0/Http4s400.scala | 2 +- .../scala/code/api/v5_0_0/Http4s500.scala | 4 +-- .../scala/code/api/v7_0_0/Http4s700.scala | 2 +- 7 files changed, 25 insertions(+), 20 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index f8b7b45e9c..0e0b01c6f9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -247,7 +247,7 @@ Note: `executeFutureCreated` returns 201; pair it with `cc.user.openOrThrowExcep **Capitalised words in a ResourceDoc template are documentation, not matching rules**: the middleware never compares the request URL with a template. It asks the routes which one serves the request, and the doc of that route validates it. Until 2026-10 it compared the URL with the templates and guessed that a capitalised segment was a placeholder, with a hand-kept list of exceptions; a transaction-request type missing from that list (`MOBILE_WALLET`, `HOLD`, `CARDANO`, ...) made its template claim other types' requests, and a template made only of placeholders (Berlin Group `getPaymentInformation`) claimed every URL of its length. So an enum value or any other capitalised word can be used in a template freely. What the middleware does read from a template are the positions of `BANK_ID`, `ACCOUNT_ID`, `VIEW_ID` and `COUNTERPARTY_ID` (`ResourceDocMatcher.extractPathParams`). When several docs share one route (v4.0.0 `createTransactionRequest`, v3.1.0 `createConsent`), the doc that agrees with the request in the most segments is used, and the first doc of the route when none does; list the doc that should validate the types without a doc of their own first. `getUsersByEmail`-style templates (`/users/email/EMAIL/terminator`) need no rename. -**A ResourceDoc is bound to its route**: declare an endpoint as `val x: Http4sRoute = Http4sRoute { case req @ GET -> ... => ... }` (not `HttpRoutes.of[IO]`), put it in the doc's `http4sPartialFunction = Some(x)` and in the version's `routesInOrder` list. `ResourceDocMiddleware` picks the doc by asking the routes whether they serve the request (`ResourceDocMatcher.selectByRoute`, over `orderedResourceDocs`) and runs only that route, so the doc that validates a request is always the doc of the handler that serves it; a request no route serves is passed on to the next link of the chain. This holds for every version: v1.2.1 to v7.0.0, Berlin Group v1.3 and v2, UK Open Banking v2.0.0, v3.1.0 and v4.0.1. The path-rewriting bridges ask `selectByRoute` whether their version serves a path. `ResourceDocRouteBindingTest` checks, for every catalog, that each doc's own URL is served by its own route and that selection returns that doc; a doc whose template documents a different URL from the one its route serves (the Lift baseline's `/search/warehouse` is served at `/search/warehouse/{query}`) needs an entry in its `sampleUrlOf`. When you add a version or a Berlin Group / UK Open Banking group, add its `orderedResourceDocs` to the test's `convertedCatalogs`. +**A ResourceDoc is bound to its route**: declare an endpoint as `val x: Http4sRoute = Http4sRoute { case req @ GET -> ... => ... }` (not `HttpRoutes.of[IO]`), put it in the doc's `http4sPartialFunction = Some(x)` and in the version's `routesInOrder` list. `ResourceDocMiddleware` picks the doc by asking the routes whether they serve the request (`ResourceDocMatcher.selectByRoute`, over `orderedResourceDocs`) and runs only that route, so the doc that validates a request is always the doc of the handler that serves it; a request no route serves is passed on to the next link of the chain. This holds for every version: v1.2.1 to v7.0.0, Berlin Group v1.3 and v2, UK Open Banking v2.0.0, v3.1.0 and v4.0.1. The path-rewriting bridges pass a request to the version below only when this version did not answer it: v5.1.0, v6.0.0 and v7.0.0 ask `selectByRoute` whether their version serves the path, so an endpoint disabled with `api_disabled_endpoints` is not served again by the version below; v5.0.0 and the versions under it test only the path prefix and are chained after their own routes (`allRoutesWithMiddleware.run(req).orElse(bridge)`), so they see only requests this version's routes did not answer, and a disabled endpoint does fall to the version below. `ResourceDocRouteBindingTest` checks, for every catalog, that each doc's own URL is served by its own route and that selection returns that doc; a doc whose template documents a different URL from the one its route serves (the Lift baseline's `/search/warehouse` is served at `/search/warehouse/{query}`) needs an entry in its `sampleUrlOf`. When you add a version or a Berlin Group / UK Open Banking group, add its `orderedResourceDocs` to the test's `convertedCatalogs`. **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. diff --git a/docs/resource_doc_and_endpoint_consistency_status.md b/docs/resource_doc_and_endpoint_consistency_status.md index 79623c3978..8db8a41838 100644 --- a/docs/resource_doc_and_endpoint_consistency_status.md +++ b/docs/resource_doc_and_endpoint_consistency_status.md @@ -16,13 +16,17 @@ endpoint has one. endpoint discovery and the Glossary's endpoint links are all generated from ResourceDocs. **Building the routes.** In every versioned API group, the routes are built from the ResourceDocs -themselves: `allRoutes` is the list of each doc's `http4sPartialFunction`, ordered by the number of -path segments (for example `Http4s700.scala`, `val allRoutes`). So in those groups a route cannot -exist without a ResourceDoc. The same construction is used in 11 route groups. +themselves: each version lists its routes in `routesInOrder` (for example `Http4s700.scala`, which sorts +them by the number of path segments; the other versions keep the order they were written in), and +`ResourceDocMatcher.orderByRoutes` puts the docs in that same order. A doc whose route is missing from +the list is an error at startup, so in those groups a route cannot exist without a ResourceDoc. +Every versioned group is built this way: v1.2.1 to v7.0.0, Berlin Group v1.3 and v2, and UK Open +Banking v2.0.0, v3.1.0 and v4.0.1. **Checking every request, in `ResourceDocMiddleware`.** Each versioned group is wrapped in -`ResourceDocMiddleware.apply(resourceDocs)` (`obp-api/src/main/scala/code/api/util/http4s/ResourceDocMiddleware.scala`). -For each request it finds the matching ResourceDoc and then, in this order +`ResourceDocMiddleware.apply(orderedResourceDocs, ...)` (`obp-api/src/main/scala/code/api/util/http4s/ResourceDocMiddleware.scala`). +For each request it asks the group's routes which one serves it (`ResourceDocMatcher.selectByRoute`), +takes that route's ResourceDoc, and then, in this order (`validateOnly`, which follows the order Lift used): 1. rejects duplicated query parameters; @@ -55,11 +59,12 @@ Every group below builds its routes from its ResourceDocs and is wrapped by - Berlin Group v1.3 (and its alias path) and v2; - UK Open Banking v2.0, v3.1 and v4.0.1. -One narrow exception inside these groups: when no ResourceDoc matches (for example a URL with an -empty segment such as `/banks//accounts`), the middleware still lets the group's routes try the -request, after resolving the caller but without the doc-based checks. That branch exists so a -malformed URL gets 403 or 404 rather than a misleading 401. It is documented in `CLAUDE.md` -("Empty path segments"). +There is no exception inside these groups. A request that none of a group's routes serves is passed +on to the next link of the chain at once, without resolving the caller. A request a route does serve +is validated under that route's ResourceDoc, including a URL with an empty segment such as +`/banks//accounts`: the empty segment is read as an empty `BANK_ID`, so the Roles and the bank +lookup are checked against it instead of the handler running unvalidated. It is documented in +`CLAUDE.md` ("Empty path segments"). ## 4. Where it does not hold @@ -136,8 +141,8 @@ each of which changes what callers receive: 2. The docs are declared once, at v1.4.0, but the routes answer under every version prefix, and the output depends on the prefix: v4.0.0 and later get the newer shape, v6.0.0 also adds the technology field (`Http4sResourceDocs.scala`, `includeTechnologyForPrefix` and the - `isVersion4OrHigher` choice in `routes`). The middleware matches a doc by the version in the - path, so it would need a copy of each doc in every version group, or a matcher that ignores + `isVersion4OrHigher` choice in `routes`). The middleware selects a doc from the routes of the + version group in the path, so it would need a copy of each doc in every version group, or a matcher that ignores versions, and either change reaches beyond these routes. 3. They are open by default and need a Role only when `resource_docs_requires_role=true`, checked inline (`withOptionalRoleCheck`). A doc can declare a Role conditionally, but the choice is diff --git a/obp-api/src/main/scala/code/api/util/http4s/ResourceDocMiddleware.scala b/obp-api/src/main/scala/code/api/util/http4s/ResourceDocMiddleware.scala index 21369ec31d..91033bdf75 100644 --- a/obp-api/src/main/scala/code/api/util/http4s/ResourceDocMiddleware.scala +++ b/obp-api/src/main/scala/code/api/util/http4s/ResourceDocMiddleware.scala @@ -175,7 +175,7 @@ object ResourceDocMiddleware extends MdcLoggable { // tests or live config reloads) take effect immediately. Cost is a few Lift Props // lookups — negligible per request, but lets disabled endpoints be toggled without // restarting the server. A disabled endpoint yields OptionT.none so the request - // falls through to the next handler in the chain (typically the Lift bridge). + // falls through to the next handler in the chain (the bridge to the version below, then `notFoundCatchAll`). // // Version-level enable/disable is NOT re-checked here — that's enforced once at // startup by `Http4sApp.gate` for the URL prefix the request arrives at, so that @@ -198,7 +198,7 @@ object ResourceDocMiddleware extends MdcLoggable { selected match { case Some((resourceDoc, _)) if !endpointIsEnabled(resourceDoc) => // Disabled by api_disabled_endpoints / api_enabled_endpoints / api_disabled_versions / - // api_enabled_versions. Fall through so the Lift bridge can serve or 404. + // api_enabled_versions. Fall through so the next handler in the chain can serve or 404. OptionT.none[IO, Response[IO]] case Some((resourceDoc, routesToRun)) => Http4sRequestAttributes.trafficNote(req).foreach { note => diff --git a/obp-api/src/main/scala/code/api/v2_1_0/Http4s210.scala b/obp-api/src/main/scala/code/api/v2_1_0/Http4s210.scala index d80262a299..2eac86bc17 100644 --- a/obp-api/src/main/scala/code/api/v2_1_0/Http4s210.scala +++ b/obp-api/src/main/scala/code/api/v2_1_0/Http4s210.scala @@ -290,7 +290,7 @@ object Http4s210 { // (Catch-all ResourceDoc for TRANSACTION_REQUEST_TYPE removed: it caused the v2.1.0 // middleware to auth-check and route every type, including v4-only ones, then return // 400. The four specific docs above cover what v2.1.0 actually supports; v4-only - // types miss the route guard and fall through to the Lift bridge.) + // types miss the route guard and fall through to the version below.) private def createTransactionRequestImpl( jsonBody: String, diff --git a/obp-api/src/main/scala/code/api/v4_0_0/Http4s400.scala b/obp-api/src/main/scala/code/api/v4_0_0/Http4s400.scala index 5608eb527e..9cc8d546b9 100644 --- a/obp-api/src/main/scala/code/api/v4_0_0/Http4s400.scala +++ b/obp-api/src/main/scala/code/api/v4_0_0/Http4s400.scala @@ -3134,7 +3134,7 @@ object Http4s400 { // - Unknown types (e.g. "invalidTransactionRequestType") still hit this route // and get a 400 from the connector's `transactionRequests_supported_types` // check, matching the v210 catch-all behavior the test depends on. Without - // this catch, unknown types fall through to Lift → 404. + // this catch, unknown types fall through to the versions below → 404. // // Use `executeFutureCreated` so the response is 201; extract user/bank/account // from cc manually (middleware populates them via the BANK_ID and ACCOUNT_ID diff --git a/obp-api/src/main/scala/code/api/v5_0_0/Http4s500.scala b/obp-api/src/main/scala/code/api/v5_0_0/Http4s500.scala index 4bd35fea61..e03bed5152 100644 --- a/obp-api/src/main/scala/code/api/v5_0_0/Http4s500.scala +++ b/obp-api/src/main/scala/code/api/v5_0_0/Http4s500.scala @@ -2400,8 +2400,8 @@ object Http4s500 { ResourceDocMiddleware.apply(orderedResourceDocs, routes => IdempotencyMiddleware(routes)) // ─── path-rewriting bridge: /obp/v5.0.0/… → /obp/v4.0.0/… ───────────── - // Cascades inherited (v1.2.1–v4.0.0) endpoints through the http4s versions - // instead of falling all the way through to Http4sLiftWebBridge. + // Cascades inherited (v1.2.1–v4.0.0) endpoints through the http4s versions. + // Chained after this version's own routes, so it only sees requests none of them served. val v500ToV400Bridge: HttpRoutes[IO] = Kleisli[HttpF, Request[IO], Response[IO]] { req => val rawPath = req.uri.path.renderString if (rawPath.startsWith("/obp/v5.0.0/")) { diff --git a/obp-api/src/main/scala/code/api/v7_0_0/Http4s700.scala b/obp-api/src/main/scala/code/api/v7_0_0/Http4s700.scala index 6b9607e58a..61693bbd97 100644 --- a/obp-api/src/main/scala/code/api/v7_0_0/Http4s700.scala +++ b/obp-api/src/main/scala/code/api/v7_0_0/Http4s700.scala @@ -203,7 +203,7 @@ object Http4s700 { // before `val myEndpoint` is initialized, `Some(null)` is stored. The sort+fold then // produces a null-route chain that NPEs on every request — and because OptionT.orElse // only recovers from None (not failed IO), the NPE propagates up and kills the entire - // http4s handler chain, including the Lift bridge fallback. + // http4s handler chain, including the bridge to v6.0.0. // // Convention: val → resourceDocs +=, never the other way around. From 248f0e269806a2adcc7fd0c01494e4c912764dd1 Mon Sep 17 00:00:00 2001 From: Hongwei Date: Wed, 7 Oct 2026 21:39:03 +0200 Subject: [PATCH 29/29] docs: remove the comments that still describe the Lift bridge as a live component Http4sLiftWebBridge is gone; a request no route serves reaches notFoundCatchAll. The comments on the Http4sApp entry points, the resource-docs and dynamic endpoint/entity services, the UK Open Banking and Berlin Group file headers and the v2.1.0 transaction request docs still said routes sit ahead of, or fall through to, the bridge. Say what happens now, and drop the status doc's note about the stale comment. Statements that the bridge was removed are left as they are. --- docs/resource_doc_and_endpoint_consistency_status.md | 7 ++----- obp-api/src/main/scala/code/api/DirectLoginRoutes.scala | 2 +- .../code/api/UKOpenBanking/v2_0_0/Http4sUKOBv200.scala | 2 +- .../api/UKOpenBanking/v2_0_0/OBP_UKOpenBanking_200.scala | 2 +- .../code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310.scala | 2 +- .../api/UKOpenBanking/v3_1_0/OBP_UKOpenBanking_310.scala | 3 +-- .../api/berlin/group/v1_3/OBP_BERLIN_GROUP_1_3.scala | 3 +-- .../api/dynamic/endpoint/Http4sDynamicEndpoint.scala | 6 +++--- .../code/api/dynamic/entity/Http4sDynamicEntity.scala | 2 +- .../src/main/scala/code/api/util/http4s/Http4sApp.scala | 7 ++----- .../scala/code/api/util/http4s/Http4sResourceDocs.scala | 4 ++-- .../code/api/util/http4s/Http4sStandardHeaders.scala | 3 +-- obp-api/src/main/scala/code/api/v2_1_0/Http4s210.scala | 9 ++++----- 13 files changed, 21 insertions(+), 31 deletions(-) diff --git a/docs/resource_doc_and_endpoint_consistency_status.md b/docs/resource_doc_and_endpoint_consistency_status.md index 8db8a41838..a1ef809615 100644 --- a/docs/resource_doc_and_endpoint_consistency_status.md +++ b/docs/resource_doc_and_endpoint_consistency_status.md @@ -142,8 +142,8 @@ each of which changes what callers receive: the output depends on the prefix: v4.0.0 and later get the newer shape, v6.0.0 also adds the technology field (`Http4sResourceDocs.scala`, `includeTechnologyForPrefix` and the `isVersion4OrHigher` choice in `routes`). The middleware selects a doc from the routes of the - version group in the path, so it would need a copy of each doc in every version group, or a matcher that ignores - versions, and either change reaches beyond these routes. + version group in the path, so it would need a copy of each doc in every version group, or a + selection that ignores versions, and either change reaches beyond these routes. 3. They are open by default and need a Role only when `resource_docs_requires_role=true`, checked inline (`withOptionalRoleCheck`). A doc can declare a Role conditionally, but the choice is fixed when the docs are built at start-up (see the shard 10 note in `CLAUDE.md`). @@ -161,6 +161,3 @@ section 13). - `docs/telemetry_conventions.md`, section 13: which of these routes Telemetry does not time. - `CLAUDE.md`: the migration rules (ResourceDoc registration order, the middleware's handling of `BANK_ID`, `ACCOUNT_ID`, `VIEW_ID`, `COUNTERPARTY_ID`, and the gotchas). -- Stale comment: `Http4sDynamicEndpoint.scala`'s header still says an unmatched request falls - through to "the Lift bridge"; since the bridge was removed it reaches `notFoundCatchAll` (JSON - 404). diff --git a/obp-api/src/main/scala/code/api/DirectLoginRoutes.scala b/obp-api/src/main/scala/code/api/DirectLoginRoutes.scala index b269e44b8a..c8aa8455fa 100644 --- a/obp-api/src/main/scala/code/api/DirectLoginRoutes.scala +++ b/obp-api/src/main/scala/code/api/DirectLoginRoutes.scala @@ -48,7 +48,7 @@ import scala.concurrent.Future * paths are served by per-version files (e.g. `Http4s600.directLoginEndpoint`). * * Adding this route lets us retire `LiftRules.statelessDispatch.append(DirectLogin)` - * in `Boot.scala` — it was the last consumer of the bare path on the Lift bridge. + * in `Boot.scala` — it was the last Lift registration serving the bare path. */ object DirectLoginRoutes { diff --git a/obp-api/src/main/scala/code/api/UKOpenBanking/v2_0_0/Http4sUKOBv200.scala b/obp-api/src/main/scala/code/api/UKOpenBanking/v2_0_0/Http4sUKOBv200.scala index ad6391e49c..be0f3fe982 100644 --- a/obp-api/src/main/scala/code/api/UKOpenBanking/v2_0_0/Http4sUKOBv200.scala +++ b/obp-api/src/main/scala/code/api/UKOpenBanking/v2_0_0/Http4sUKOBv200.scala @@ -46,7 +46,7 @@ import scala.collection.mutable.ArrayBuffer * account-scoped ones (/accounts/ID/balances, /accounts/ID/transactions) — are * migrated in Http4sUKOBv200AIS. The Lift ScannedApis aggregator * (OBP_UKOpenBanking_200) registers `routes = Nil`, so no UK v2.0 path is served - * by Lift — nothing falls through to the Lift bridge. + * by Lift. */ object Http4sUKOBv200 extends MdcLoggable { diff --git a/obp-api/src/main/scala/code/api/UKOpenBanking/v2_0_0/OBP_UKOpenBanking_200.scala b/obp-api/src/main/scala/code/api/UKOpenBanking/v2_0_0/OBP_UKOpenBanking_200.scala index 120709d685..8c9578def6 100644 --- a/obp-api/src/main/scala/code/api/UKOpenBanking/v2_0_0/OBP_UKOpenBanking_200.scala +++ b/obp-api/src/main/scala/code/api/UKOpenBanking/v2_0_0/OBP_UKOpenBanking_200.scala @@ -40,7 +40,7 @@ import scala.collection.mutable.ArrayBuffer * This stub is retained for ScannedApis registration (class-path scanning) and * so that external callers (APIUtil, SwaggerJSONFactory) that access * OBP_UKOpenBanking_200.apiVersion / .allResourceDocs continue to compile. - * Routes are served by Http4sUKOBv200.wrappedRoutes in Http4sApp (ahead of the Lift bridge). + * Routes are served by Http4sUKOBv200.wrappedRoutes in Http4sApp. */ object OBP_UKOpenBanking_200 extends OBPRestHelper with MdcLoggable with ScannedApis { diff --git a/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310.scala b/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310.scala index ba21c6ed9f..6ab3e7f05b 100644 --- a/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310.scala +++ b/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310.scala @@ -49,7 +49,7 @@ import scala.collection.mutable.ArrayBuffer * Coverage: all 20 v3.1 categories (~67 endpoints) are migrated to http4s and * composed into allRoutes below. The Lift ScannedApis aggregator * (OBP_UKOpenBanking_310) registers `routes = Nil`, so no UK v3.1 path is served - * by Lift — nothing falls through to the Lift bridge. + * by Lift. */ object Http4sUKOBv310 extends MdcLoggable { diff --git a/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/OBP_UKOpenBanking_310.scala b/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/OBP_UKOpenBanking_310.scala index 33f49ca9e6..3b4b56a1ea 100644 --- a/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/OBP_UKOpenBanking_310.scala +++ b/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/OBP_UKOpenBanking_310.scala @@ -42,8 +42,7 @@ import scala.collection.mutable.ArrayBuffer * This aggregator is retained for ScannedApis registration (class-path scanning) * and so that external callers (APIUtil, SwaggerJSONFactory) that access * OBP_UKOpenBanking_310.apiVersion / .allResourceDocs continue to compile. - * Routes are served by Http4sUKOBv310.wrappedRoutes in Http4sApp (ahead of the - * Lift bridge). + * Routes are served by Http4sUKOBv310.wrappedRoutes in Http4sApp. */ object OBP_UKOpenBanking_310 extends OBPRestHelper with MdcLoggable with ScannedApis { diff --git a/obp-api/src/main/scala/code/api/berlin/group/v1_3/OBP_BERLIN_GROUP_1_3.scala b/obp-api/src/main/scala/code/api/berlin/group/v1_3/OBP_BERLIN_GROUP_1_3.scala index e88a22a5b2..c6faaae98e 100644 --- a/obp-api/src/main/scala/code/api/berlin/group/v1_3/OBP_BERLIN_GROUP_1_3.scala +++ b/obp-api/src/main/scala/code/api/berlin/group/v1_3/OBP_BERLIN_GROUP_1_3.scala @@ -47,8 +47,7 @@ import scala.collection.mutable.ArrayBuffer * This aggregator is retained for ScannedApis registration (class-path scanning) * and so that external callers that access OBP_BERLIN_GROUP_1_3.apiVersion / * .allResourceDocs continue to compile. - * Routes are served by Http4sBGv13.wrappedRoutes in Http4sApp (ahead of the - * Lift bridge). + * Routes are served by Http4sBGv13.wrappedRoutes in Http4sApp. */ object OBP_BERLIN_GROUP_1_3 extends OBPRestHelper with MdcLoggable with ScannedApis { diff --git a/obp-api/src/main/scala/code/api/dynamic/endpoint/Http4sDynamicEndpoint.scala b/obp-api/src/main/scala/code/api/dynamic/endpoint/Http4sDynamicEndpoint.scala index c59f800c16..a3aa0ca6a3 100644 --- a/obp-api/src/main/scala/code/api/dynamic/endpoint/Http4sDynamicEndpoint.scala +++ b/obp-api/src/main/scala/code/api/dynamic/endpoint/Http4sDynamicEndpoint.scala @@ -62,8 +62,8 @@ import org.http4s.{HttpRoutes, Request, Response} * body forcing / early-return recovery (`DynamicCodeBody.force`, inside the compiled handler). * * Piece B is tried first; a non-match falls through to Piece C; a non-match there returns - * `OptionT.none`, so the request falls through the Http4sApp chain (the Lift bridge produces the - * final 404, as before). + * `OptionT.none`, so the request falls through the Http4sApp chain to `notFoundCatchAll`, which + * produces the final 404 (JSON). */ object Http4sDynamicEndpoint extends MdcLoggable { @@ -154,7 +154,7 @@ object Http4sDynamicEndpoint extends MdcLoggable { } } - /** Entry point wired into Http4sApp.baseServices (before the Lift bridge). */ + /** Entry point wired into Http4sApp.baseServices. */ lazy val wrappedRoutesDynamicEndpoint: HttpRoutes[IO] = Kleisli[HttpF, Request[IO], Response[IO]] { (req: Request[IO]) => req.uri.path.segments.map(_.encoded).toList match { diff --git a/obp-api/src/main/scala/code/api/dynamic/entity/Http4sDynamicEntity.scala b/obp-api/src/main/scala/code/api/dynamic/entity/Http4sDynamicEntity.scala index 750e34d955..b1b815a2e1 100644 --- a/obp-api/src/main/scala/code/api/dynamic/entity/Http4sDynamicEntity.scala +++ b/obp-api/src/main/scala/code/api/dynamic/entity/Http4sDynamicEntity.scala @@ -971,7 +971,7 @@ object Http4sDynamicEntity extends MdcLoggable { private def resolveCallerOutsideTheTransaction(cc: CallContext): IO[Unit] = IO.fromFuture(IO(code.api.util.APIUtil.resolveCallerWithoutRateLimiting(cc))).attempt.void - /** Entry point wired into Http4sApp.baseServices (before the Lift bridge). */ + /** Entry point wired into Http4sApp.baseServices. */ lazy val wrappedRoutesDynamicEntity: HttpRoutes[IO] = Kleisli[HttpF, Request[IO], Response[IO]] { (req: Request[IO]) => req.uri.path.segments.map(_.encoded).toList match { diff --git a/obp-api/src/main/scala/code/api/util/http4s/Http4sApp.scala b/obp-api/src/main/scala/code/api/util/http4s/Http4sApp.scala index 713ec7d86e..1b5dbedfe3 100644 --- a/obp-api/src/main/scala/code/api/util/http4s/Http4sApp.scala +++ b/obp-api/src/main/scala/code/api/util/http4s/Http4sApp.scala @@ -93,11 +93,8 @@ object Http4sApp extends MdcLoggable { // OBPAPIDynamicEntity dispatch. private val dynamicEntityRoutes: HttpRoutes[IO] = gate(ApiVersion.`dynamic-entity`, code.api.dynamic.entity.Http4sDynamicEntity.wrappedRoutesDynamicEntity) // DynamicEndpoint dispatch (/obp/dynamic-endpoint/*) — fully-native http4s: proxy (DynamicReq) - // + runtime-compiled resource docs, no Lift dispatch. Replaces the LiftRules.statelessDispatch - // registration. Must sit AHEAD of the Lift bridge (the bridge no longer carries dynamic-endpoint). - // DynamicEndpoint dispatch (/obp/dynamic-endpoint/*) — proxy (DynamicReq) + runtime-compiled - // resource docs / practise. Runs the OBPAPIDynamicEndpoint.routes in-process via an adapter, - // replacing the former LiftRules.statelessDispatch registration. + // + runtime-compiled resource docs, no Lift dispatch. Replaces the former + // LiftRules.statelessDispatch registration. private val dynamicEndpointRoutes: HttpRoutes[IO] = gate(ApiVersion.`dynamic-endpoint`, code.api.dynamic.endpoint.Http4sDynamicEndpoint.wrappedRoutesDynamicEndpoint) // UK Open Banking (non-/obp prefixes /open-banking/v2.0 and /open-banking/v3.1) — native // http4s, replaces the classpath-scanned Lift ScannedApis. All endpoints (v2.0: 5, v3.1: ~67) diff --git a/obp-api/src/main/scala/code/api/util/http4s/Http4sResourceDocs.scala b/obp-api/src/main/scala/code/api/util/http4s/Http4sResourceDocs.scala index a7a0c86f67..f87f77ca8d 100644 --- a/obp-api/src/main/scala/code/api/util/http4s/Http4sResourceDocs.scala +++ b/obp-api/src/main/scala/code/api/util/http4s/Http4sResourceDocs.scala @@ -77,8 +77,8 @@ import code.api.util.ApiTag.ResourceDocTag * GET /obp/*/banks/{BANK_ID}/resource-docs/{API_VERSION}/openapi.yaml * GET /obp/*/message-docs/{CONNECTOR}/swagger2.0 * - * Wired into `Http4sApp.baseServices` BEFORE the Lift bridge, so requests are - * served natively instead of falling through to Lift. + * Wired into `Http4sApp.baseServices` ahead of the versioned APIs, so these paths are + * answered here rather than by a version's routes. * * Business logic is delegated to `ResourceDocs140.ImplementationsResourceDocs` * (and `ResourceDocsAPIMethodsUtil`) so caching / content-param / locale / diff --git a/obp-api/src/main/scala/code/api/util/http4s/Http4sStandardHeaders.scala b/obp-api/src/main/scala/code/api/util/http4s/Http4sStandardHeaders.scala index 3e2ec3c602..07d16d33ca 100644 --- a/obp-api/src/main/scala/code/api/util/http4s/Http4sStandardHeaders.scala +++ b/obp-api/src/main/scala/code/api/util/http4s/Http4sStandardHeaders.scala @@ -40,8 +40,7 @@ import java.util.UUID /** * Standard HTTP response headers applied to every response. * - * Extracted from Http4sLiftWebBridge so Http4sApp can apply them - * without depending on the bridge at all. + * Http4sApp applies them to the response of every route in the chain. */ object Http4sStandardHeaders { diff --git a/obp-api/src/main/scala/code/api/v2_1_0/Http4s210.scala b/obp-api/src/main/scala/code/api/v2_1_0/Http4s210.scala index 2eac86bc17..befae8b56c 100644 --- a/obp-api/src/main/scala/code/api/v2_1_0/Http4s210.scala +++ b/obp-api/src/main/scala/code/api/v2_1_0/Http4s210.scala @@ -451,11 +451,10 @@ object Http4s210 { } // Register one ResourceDoc per supported type rather than a single - // TRANSACTION_REQUEST_TYPE wildcard. The wildcard would also match v4-only - // types (ACCOUNT, ACCOUNT_OTP, REFUND, SIMPLE, AGENT_CASH_WITHDRAWAL, CARD), - // which the route guard then rejects — leaving the middleware to return 404 - // instead of letting the request fall through to the Lift fallback that - // actually handles those types. + // TRANSACTION_REQUEST_TYPE wildcard. The route serves every type, including the + // v4-only ones (ACCOUNT, ACCOUNT_OTP, REFUND, SIMPLE, AGENT_CASH_WITHDRAWAL, CARD), + // which the handler refuses with InvalidTransactionRequestType. A type with no doc + // of its own is validated under the first of the docs below, which share the route. private val answerChallengeCommonErrors = List( AuthenticatedUserIsRequired, InvalidBankIdFormat, InvalidAccountIdFormat, InvalidJsonFormat, BankNotFound, UserNoPermissionAccessView, TransactionRequestStatusNotInitiated,