diff --git a/obp-api/src/main/scala/code/api/util/Glossary.scala b/obp-api/src/main/scala/code/api/util/Glossary.scala index 2a1bf0faab..b3a0792f76 100644 --- a/obp-api/src/main/scala/code/api/util/Glossary.scala +++ b/obp-api/src/main/scala/code/api/util/Glossary.scala @@ -3735,6 +3735,47 @@ object Glossary extends MdcLoggable { |In short, Static (standard) endpoints are defined in Git as Scala source code, where as Dynamic (custom) endpoints are defined in the OBP database. | |Modifications to Static endpoint core properties such as URLs and response bodies require source code changes and an instance restart. However, JSON Schema Validation and Dynamic Connector changes can be applied in real-time. +""".stripMargin) + + glossaryItems += GlossaryItem( + title = "Resource Doc", + description = + s""" +|A Resource Doc is the machine readable definition / description of an OBP Endpoint. +| +|The aim is that as much endpoint definition as possible is *defined first* within the Resource Doc making the Resource Doc the canonical source of truth about the endpoints structure and behaviour. +| +|In total Resource Docs form the server registry of the API: every Endpoint, static or dynamic, is registered in the running server with its Resource Doc, and that registry is the source of truth about the API surface. +| +|Note that the Resource Docs (like the Glossary) can contain instance variables about the OBP-API instance that is running, so HOSTNAMES and various configuration settings are automatically correct. +| +|An OBP API instance only serves resource docs about endpoints that are actually enabled so any client (e.g. the OBP MCP Server or API Explorer) can use them as a capability discovery channel. +| +| +|Each Resource Doc includes: +| +| 1) The Operation ID / Scala Partial Function name (the source code / function that runs the endpoint) which uniquely identifies the Endpoint (e.g. getCoreAccountById) +| 2) The API version the Endpoint is implemented in +| 3) The request verb (GET, POST, PUT, DELETE etc.) and URL path +| 4) A summary and a longer description (markdown) +| 5) An example request body and a successful response body. These are generated from the actual Scala case classes the Endpoint uses, so field names and types reflect the real implementation rather than separately maintained documentation. +| 6) The possible error responses +| 7) Tags used to group Endpoints in the API Explorer and filter Resource Docs +| 8) The Roles (Entitlements) required to call the Endpoint. Roles declared in the Resource Doc are automatically checked by the framework at runtime. +| 9) Connector methods the Endpoint depends on (linking to the related Message Docs) +| +|Because the Resource Doc registry lives inside the running server, it differs from a published OpenAPI file in two important ways: +| +|* The arrow of generation points from code to documentation: the Swagger / OpenAPI documents that OBP publishes are generated *from* the Resource Docs, not maintained alongside the code. This avoids documentation drift. +| +|* It covers Endpoints created at runtime: Dynamic Endpoints and the auto-generated CRUD Endpoints for Dynamic Entities get Resource Docs when they are created, so per-bank custom APIs are documented by the same mechanism as the static Endpoints - something a static, pre-published API description cannot do. +| +|Resource Docs are available over the Resource Doc endpoints in OBP format (which includes OBP specific metadata such as Roles, Tags and Connector methods) and in Swagger / OpenAPI format. They can be filtered by tags, functions and API collections. +| +|As mentioned above, Resource Docs power the API Explorer interface and are the natural foundation for programmatic consumers of the API surface: SDK generators, API management tooling and AI assistants (such as [Opey](/glossary#Opey) and [OBP-MCP](/glossary#OBP-MCP)) that need to discover, select and validate calls to Endpoints. +| +|See also [Endpoint](/glossary#Endpoint), [Static Endpoint](/glossary#Static-Endpoint), [Dynamic Endpoint Manage](/glossary#Dynamic-Endpoint-Manage), [Message Doc](/glossary#Message-Doc) +| """.stripMargin) glossaryItems += GlossaryItem( @@ -5869,7 +5910,7 @@ object Glossary extends MdcLoggable { | |- **Documentation / discovery** — `list_endpoints_by_tag`, `get_endpoint_schema`, glossary tools. Served from local JSON, no network. |- **Business calls** — `call_obp_api` proxies whatever the endpoint declares: `GET /banks/{BANK_ID}/accounts`, `POST .../transaction-requests/SEPA`, `PUT /accounts/{ACC}/label`, `DELETE /my/consents/{CONSENT_ID}`, etc. Real money / data moves. - |- **Index refresh** — at startup and on a timer, OBP-MCP re-fetches OBP's resource-docs and swagger to rebuild the local indexes, so discovery stays fast and offline. + |- **Index refresh** — at startup and on a timer, OBP-MCP re-fetches OBP's [Resource Docs](/glossary#Resource-Doc) and swagger to rebuild the local indexes, so discovery stays fast and offline. | |## Authentication and authorization | @@ -5894,7 +5935,7 @@ object Glossary extends MdcLoggable { | |OBP-MCP is the canonical way to make Open Bank Project endpoints **agent-callable**. Instead of teaching every LLM about every endpoint up front, the LLM is given five generic tools and lets the indexes and schemas guide it to the right call at runtime. The same server can serve internal agents (Opey) and external clients (Claude Desktop, IDE plugins, third-party agents) by switching auth providers. | - |See also: [Opey](/glossary#Opey), [Consent](/glossary#Consent), [Authentication: OAuth 2.0](/glossary#Authentication:-OAuth-2.0). + |See also: [Opey](/glossary#Opey), [Resource Doc](/glossary#Resource-Doc), [Consent](/glossary#Consent), [Authentication: OAuth 2.0](/glossary#Authentication:-OAuth-2.0). | """) @@ -5958,7 +5999,7 @@ object Glossary extends MdcLoggable { | |**OBP-MCP is the *tool surface* over OBP-API. Opey II is the *agent* that drives it.** Before OBP-MCP, Opey had to be both. Now OBP-MCP provides discovery and authenticated calls as a generic, multi-client surface (Claude Desktop, IDE plugins, third-party agents can all use it), and Opey II becomes a thinner, more focused orchestrator: planning, approvals, conversation state, streaming, and the chat UX that OBP-Portal embeds. | - |See also: [OBP-MCP](/glossary#OBP-MCP), [Consent](/glossary#Consent), [Authentication: OAuth 2.0](/glossary#Authentication:-OAuth-2.0). + |See also: [OBP-MCP](/glossary#OBP-MCP), [Resource Doc](/glossary#Resource-Doc), [Consent](/glossary#Consent), [Authentication: OAuth 2.0](/glossary#Authentication:-OAuth-2.0). | """) diff --git a/obp-api/src/main/scala/code/bankconnectors/opencorridor/OpenCorridorSettlement.scala b/obp-api/src/main/scala/code/bankconnectors/opencorridor/OpenCorridorSettlement.scala index 2368157086..1bce6f6451 100644 --- a/obp-api/src/main/scala/code/bankconnectors/opencorridor/OpenCorridorSettlement.scala +++ b/obp-api/src/main/scala/code/bankconnectors/opencorridor/OpenCorridorSettlement.scala @@ -260,22 +260,25 @@ object OpenCorridorSettlement extends MdcLoggable { // Enqueue the Interface C messages in this same DB transaction (the outbox). // Credit notifications went to each beneficiary at promise-report-back time - // (OpenCorridorProcessor); settlement sends each beneficiary an advice so - // its already-paid-out credits get marked settled. + // (OpenCorridorProcessor); settlement sends BOTH party banks an advice with + // the FULL covered list, so each node stamps its already-paid-out credits + // AND its own outbound promises settled — including the party that did not + // trigger the settle, which otherwise never learns of the coverage (the + // instruction only moves money, and at net zero it is not sent at all). settlementAdviceCount <- Future { - covered.groupBy(_.mTo_BankId.get).map { case (beneficiaryBankId, rows) => - val advice = OutBoundOpenCorridorSettlementAdvice( - settlement_id = settlementTrId, - currency = currency, - net_amount = netAbs.toString(), - debtor_bank_id = debtorBankId, - creditor_bank_id = creditorBankId, - covered_transaction_request_ids = rows.map(_.mTransactionRequestId.get), - idempotency_key = settlementTrId - ) + val advice = OutBoundOpenCorridorSettlementAdvice( + settlement_id = settlementTrId, + currency = currency, + net_amount = netAbs.toString(), + debtor_bank_id = debtorBankId, + creditor_bank_id = creditorBankId, + covered_transaction_request_ids = covered.map(_.mTransactionRequestId.get), + idempotency_key = settlementTrId + ) + Set(bankIdA, bankIdB).map { partyBankId => MessageOutbox.enqueue( MessageOutbox.TYPE_OPEN_CORRIDOR, settlementTrId, MessageOutbox.SUBJECT_TYPE_SETTLEMENT_ID, - "obp_settlement_advice", beneficiaryBankId, Serialization.write(advice)) + "obp_settlement_advice", partyBankId, Serialization.write(advice)) }.size } settlementInstructionCount <- Future { diff --git a/obp-api/src/test/scala/code/api/v7_0_0/Http4s700RoutesTest.scala b/obp-api/src/test/scala/code/api/v7_0_0/Http4s700RoutesTest.scala index 452a4a663a..37411b2d08 100644 --- a/obp-api/src/test/scala/code/api/v7_0_0/Http4s700RoutesTest.scala +++ b/obp-api/src/test/scala/code/api/v7_0_0/Http4s700RoutesTest.scala @@ -2643,13 +2643,15 @@ class Http4s700RoutesTest extends ServerSetupWithTestData { outboxRows.size shouldBe 3 val adviceRows = outboxRows.filter(_.operationName == "obp_settlement_advice") adviceRows.map(_.targetId).sorted shouldBe List(testBankId1.value, testBankId2.value).sorted - val bank2Advice = adviceRows.find(_.targetId == testBankId2.value) - .map(row => parse(row.payloadJson)) - .getOrElse(fail("bank2's settlement advice should be enqueued")) - (bank2Advice \ "settlement_id") shouldBe JString(settlementId) - (bank2Advice \ "covered_transaction_request_ids") match { - case JArray(ids) => ids.collect { case JString(id) => id }.toSet shouldBe Set(promise1, promise2) - case _ => fail("covered_transaction_request_ids should be an array") + // Both party banks get the advice with the FULL covered list (both + // directions): each node stamps its credits AND its own promises from it. + adviceRows.foreach { row => + val advice = parse(row.payloadJson) + (advice \ "settlement_id") shouldBe JString(settlementId) + (advice \ "covered_transaction_request_ids") match { + case JArray(ids) => ids.collect { case JString(id) => id }.toSet shouldBe Set(promise1, promise2, promise3) + case _ => fail("covered_transaction_request_ids should be an array") + } } val instructionRow = outboxRows.filter(_.operationName == "obp_settlement_instruction") match { case row :: Nil => row diff --git a/obp-commons/src/main/scala/com/openbankproject/commons/dto/OpenCorridorInterfaceC.scala b/obp-commons/src/main/scala/com/openbankproject/commons/dto/OpenCorridorInterfaceC.scala index 7550fb1f92..46b89b44a7 100644 --- a/obp-commons/src/main/scala/com/openbankproject/commons/dto/OpenCorridorInterfaceC.scala +++ b/obp-commons/src/main/scala/com/openbankproject/commons/dto/OpenCorridorInterfaceC.scala @@ -107,11 +107,15 @@ case class OutBoundOpenCorridorSettlementInstruction( ) /** - * `obp_settlement_advice` — published to each BENEFICIARY bank's vhost after a - * netted settle: "the promises you already paid out against are now covered". - * Purely reconciliatory — no money moves on this message (the debtor's - * `obp_settlement_instruction` does that). One advice per beneficiary bank, - * listing exactly the covered promise ids where that bank was the creditor. + * `obp_settlement_advice` — published to BOTH party banks' vhosts after a + * netted settle: "these promises are now covered". Purely reconciliatory — no + * money moves on this message (the debtor's `obp_settlement_instruction` does + * that, and at net zero no instruction exists at all — this advice is then the + * only settle-time message either bank receives). + * `covered_transaction_request_ids` is the FULL covered list of the pair, both + * directions: each node stamps whatever matches its own records (credits it + * paid out AND its own outbound promises); ids of the counterparty's records + * match nothing there and are ignored. * Credit notifications themselves travel at promise-report-back time, not here. */ case class OutBoundOpenCorridorSettlementAdvice(