diff --git a/0M7DEJI.code-workspace b/0M7DEJI.code-workspace deleted file mode 100644 index ef6d00f..0000000 --- a/0M7DEJI.code-workspace +++ /dev/null @@ -1,8 +0,0 @@ -{ - "folders": [ - { - "name": "0M7DEJI(ABAP)", - "uri": "adt://0m7deji" - } - ] -} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 3bffc9a..f94fbf9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,705 @@ Append-only decision log for this package. +## 2026-09-18 — Reentrance-ticket login for `abap-adt-mcp`: built, tested, rejected + +- What changed: I built a login helper at `tools/adt-reentrance-login/` + (Node, no dependencies): a one-shot loopback listener that captures an + ADT reentrance ticket from the `redirect-url=http://localhost:/adt/redirect` + flow, writes it to a 0600 file, and probes how the tenant accepts it. It + never prints, logs or passes the ticket through argv or the environment, + and it parses the raw query itself, because `URLSearchParams` turns a + `+` in a ticket into a space and would corrupt it. Then I captured a real + ticket and tested it: `/sap/bc/adt/core/discovery` and an ABAP class + source URL answered 200 with the same 3094-byte SAML redirect page for + cookie transport, header transport and no credential, with and without + `sap-client=080`. +- Why: `abap-adt-mcp`'s browser SSO needs a window on the server's desktop, + which is unreachable over this remote session (OQ-9). ADT for VS Code + logs on with a reentrance ticket instead, so the question was whether + that ticket can feed the server's `sso2` (MYSAPSSO2 cookie) mode. +- Affected: new `tools/adt-reentrance-login/`, `~/.abap-adt-mcp/systems.json` + (switched to `sso2`, then restored to `sso`), KNOWLEDGE.md, PRD OQ-9. +- Decision: the approach is rejected on evidence. A reentrance ticket is + neither a logon-ticket cookie nor an assertion-ticket header on this + tenant, so `sso2` cannot consume it. I restored the browser-login + configuration, shredded the captured ticket, and kept the helper: it is + the only piece that would still be needed if the exchange mechanism is + ever identified. The next step, if this is revisited, is to capture what + ADT for VS Code actually sends, not to guess header names. Tenant work + continues on SAP's ADT MCP with editor paste, which is proven. + +## 2026-09-16 — Worker leg active: approval BO complete on the development tenant + +- What changed: + - I activated W3 (A2X projection BDEF) and W4 (service definition + `ZAPI_ARENA_CHANGE_APPROVAL`). + - I created and activated W5, the OData V4 Web API binding + `ZAPI_ARENA_CHANGE_APPR_A2X`, with binding type `OData V4 - Web API` + (`create_object` returned `.srvb.json`). + - `fetch_services`: service `ZAPI_ARENA_CHANGE_APPROVAL`, version `0001`, + V4, `isPublished: false`, `bindingCategory: "1"`. + - ATC over the five worker objects (worklist + `12ACF6BD93811FE1ACBA776536DD5BD6`): 0 errors and one priority-3 + warning, W607 (+BDL 549) on BDEF `ZC_CHANGEAPPROVAL_A2X`: "CDS annotation + #CHECK … should be replaced with #MANDATORY". + + [source: adt-mcp abap_activate_objects, abap_creation-create_object, + abap_atc_get_result, abap_business_services-fetch_services on 0M7DEJI, + verified 2026-09-16] +- Why: RAP-6. The worker needs create and the three execution-state + actions through a Web API, separate from the read-only UI. +- Affected: dev tenant `ZARENA_PLM` (all 19 approval-BO objects now + active), PRD RAP BO status, KNOWLEDGE.md. +- Decision: W5 stays unpublished. It is exposed to the worker's technical + user through a custom communication scenario (V-2 A2X half), not through + business-user access. I do not switch + `@AccessControl.authorizationCheck` from `#CHECK` to `#MANDATORY` now: + the warning does not block activation, and the change belongs with the + OQ-8 authorization work, which revisits all three projection and root + views together (the same annotation is in `ZR_CHANGEAPPROVAL` and + `ZC_CHANGEAPPROVAL`). + +## 2026-09-16 — Worker leg: A2X projection view and access control active; A2X BDEF created + +- What changed: I activated W1 (`ZC_CHANGEAPPROVAL_A2X` projection view) + and W2 (its access control); both answered "Activation successful." + I then created W3, the projection BDEF `ZC_CHANGEAPPROVAL_A2X`, empty + (`.bdef.abdl`), after W1 was active [source: adt-mcp + abap_activate_objects, abap_creation-create_object on 0M7DEJI, verified + 2026-09-16]. W4 (service definition) is saved but not active. +- Why: worker leg round 2 of the approval BO. +- Affected: dev tenant `ZARENA_PLM`, PRD RAP BO status. +- Decision: W4 activates only after W3 is active. Then W5, the Web API + binding, is created and verified unpublished. + +## 2026-09-16 — Worker leg round 1 created; handover brought in line with live results + +- What changed: after the expired VS Code ADT logon was restored, I + created W1 (`ZC_CHANGEAPPROVAL_A2X` view), W2 (its access control, with + `protectedEntity`) and W4 (service definition + `ZAPI_ARENA_CHANGE_APPROVAL`) empty in `ZARENA_PLM` [source: adt-mcp + create_object on 0M7DEJI, verified 2026-09-16]. I also corrected + `docs/handover/rap/`: both service definition sources are comment-free, + the README records the UI leg as active, ATC-clean and published, the + activation-URI table marks the proven extensions (`.tabl.ddic`, + `.bdef.abdl`, `.srvb.json`), and the worker-leg sequence is rewritten to + two paste rounds. All 31 existing citations were preserved. +- Why: worker (A2X) leg of the approval BO (RAP-6). The handover must not + repeat today's failed activations. +- Affected: dev tenant `ZARENA_PLM`, `docs/handover/rap/`, KNOWLEDGE.md. +- Decision: pastes are opened manually from the ABAP workspace tree. I only + give the local source path and the `abap:/repotree-v1/…` target, because + editor tabs opened by URI cannot save. W5 uses binding type `OData V4 - + Web API`, the exact value the create metadata offers. W5 stays + unpublished until the custom communication scenario brief (V-2). + +## 2026-09-16 — Fiori app bound to the live RAP service identity (V-2 UI half) + +- What changed: I reconciled `app/` with the published service. + - Manifest dataSource and mock-server URL paths now use + `/sap/opu/odata4/sap/zui_arena_change_appr_o4/srvd/sap/zui_arena_change_approval/0001/`. + - The mock `metadata.xml` is the live `$metadata`, byte-identical (sha256 + `c13e86e1…`, checked). + - Mock rows carry `StatusText`, `LocalLastChangedAt`, + `__OperationControl` and `SAP__Messages`. + - All annotation targets and action strings use namespace + `com.sap.gateway.srvd.zui_arena_change_approval.v0001` and entity type + `ChangeApprovalType`. + + A grep for the old identity over `app/` (excluding `node_modules`/`dist`) + returns nothing. Lint, typecheck and `@ui5/linter` are clean. The mock + server answers `$metadata` and `ChangeApproval` with `$filter`, `$count` + and `__OperationControl` selects. +- Why: V-2 and ARN-007. The app must address the service that exists. +- Affected: `app/` (manifest, `ui5-mock.yaml`, `ui5-local.yaml`, + `annotation.xml`, mock metadata and data, README), PRD V-2, new OQ-10, + KNOWLEDGE.md. +- Decision: + - (1) Action enablement belongs to the backend. The mock's local + `OperationAvailable` overrides went away with the mock metadata, and + `annotation.xml` deliberately defines none, because a local one would + replace RAP's `_it/__OperationControl/` [source: + fiori-mcp-server "Action Control for Context-Dependent Actions", + verified 2026-09-16]. + - (2) No real backend is configured in the UI5 tooling yet. Any live + preview targets the UI host, never `-api`. Its authentication type and + client are verified before use, not assumed. + - (3) UI annotation ownership is opened as OQ-10 rather than moved now: + consolidating would drop i18n labels and action criticality and break + the OPA section IDs mid-POC. + - (4) `dist/` is stale and must be rebuilt before any deployment. + +## 2026-09-16 — Live `$metadata` captured: backend contract matches the app; identity names differ + +- What changed: the published UI service's `$metadata` is captured in + `docs/v2/zui_arena_change_appr_o4.metadata.xml`. It was copied from the + browser on the UI host `my431353.s4hana.cloud.sap`; browser-decoded `&` + characters were re-escaped. The `-api` host refused the same request to a + business user ("blocked by UCON"). + - Matches the app: all 17 app properties with identical EDM types + (`Edm.Boolean` flags, `Edm.Byte` criticality, `Edm.DateTimeOffset` + Precision 7), key `ID`, entity set `ChangeApproval`, and the actions + `approve`, `reject(RejectReason)` (non-nullable) and `retrySync`. + - Differs from the mock: namespace + `com.sap.gateway.srvd.zui_arena_change_approval.v0001` (from the service + definition), entity type `ChangeApprovalType`, URL segment + `zui_arena_change_appr_o4`, extra `StatusText` / `LocalLastChangedAt` / + `__OperationControl` / `SAP__Messages`, and backend-driven + `Core.OperationAvailable`. + + I dispatched the app reconciliation. +- Why: V-2. The app was built against a provisional mock identity, and only + the live service can settle it. +- Affected: PRD V-2, `app/` (in progress), KNOWLEDGE.md. +- Decision: the live `$metadata` replaces the mock metadata and becomes the + app's contract. Button enablement comes from the backend's + `__OperationControl` rather than local `IsApprovable`/`IsRetriable` + overrides, unless the Fiori docs show that breaks something. Any + non-mock backend configuration points at the UI host, never `-api`. + Earlier notes claiming the binding name sets the namespace are superseded + (KNOWLEDGE 2026-09-16). + +## 2026-09-16 — Approval UI service published on the development tenant + +- What changed: the OData V4 UI binding `ZUI_ARENA_CHANGE_APPR_O4` is + published. The ADT for VS Code Publish code lens gave no visible + confirmation, so I verified it live: `fetch_services` reports + `isPublished: true`, and `fetch_service_information` returns + `https://my431353-api.s4hana.cloud.sap/sap/opu/odata4/sap/zui_arena_change_appr_o4/srvd/sap/zui_arena_change_approval/0001/?sap-client=080` + with entity set `ChangeApproval` [source: adt-mcp + abap_business_services-fetch_services, fetch_service_information on + 0M7DEJI, verified 2026-09-16]. +- Why: RAP-5. The Fiori app cannot bind to an unpublished service. +- Affected: PRD RAP BO status, V-2, KNOWLEDGE.md. +- Decision: the URL path is exactly the designed one, so no binding change + follows. The app's V-2 reconciliation (dataSource first segment, + namespace, annotation targets, EDM types) is done against the live + `$metadata`, never inferred. The service reports client 080, so the + client value in any tool configuration comes from the live system. + +## 2026-09-16 — UI leg active: approval BO and OData V4 UI service built on `my431353` + +- What changed: + - U13, the service definition `ZUI_ARENA_CHANGE_APPROVAL`, activated after + I re-saved it without its header comment. The same definition failed + with the comment and succeeded without it, so the comment was the + blocker; the missing-draft message is not. + - I created U14, the OData V4 UI binding `ZUI_ARENA_CHANGE_APPR_O4` + (`create_object` returned `.srvb.json`), and activated it. + - ATC over all 14 UI-leg objects: 0 errors, 0 warnings, 43 infos + (worklist `12ACF6BD93811FE1ACB8096AC6763BD6`). + - `fetch_services`: service `ZUI_ARENA_CHANGE_APPROVAL`, version `0001`, + OData V4, unpublished. + + [source: adt-mcp abap_activate_objects, abap_creation-create_object, + abap_atc_get_result, abap_business_services-fetch_services on 0M7DEJI, + verified 2026-09-16] +- Why: RAP-1..8 and the ARN-007 acceptance path. The service must exist + before the app can bind to it. +- Affected: dev tenant `ZARENA_PLM`, PRD RAP BO status, KNOWLEDGE.md, + handover README (both SRVD sources still carry header comments, and U9 + still says `.bdef.json`). +- Decision: the service alias equals the definition name and the version + is `0001`. Publishing therefore yields + `/sap/opu/odata4/sap/zui_arena_change_appr_o4/srvd/sap/zui_arena_change_approval/0001/`, + as designed, so no rename is needed before publishing. The 43 ATC infos + are reviewed in the ADT ATC view (the MCP returns only totals); infos do + not gate publishing. I correct the handover README in one pass before + the worker leg: comment-free SRVD sources and BDEF `.bdef.abdl` URIs. + +## 2026-09-16 — Wave D: projection BDEF, access control and metadata extension active; service definition refused + +- What changed: I activated U9 (projection BDEF `ZC_CHANGEAPPROVAL`), U11 + (its access control) and U12 (its metadata extension); each answered + "Activation successful." Activating U13, the service definition + `ZUI_ARENA_CHANGE_APPROVAL`, answered "Activation failed" with two + messages and no severity: + - "Comments are not supported and will be deleted on save" (Ln 1, the + handover header block comment) + - "oData V4 services with no draft capability will be primarily + READ-ONLY" (Ln 21). The BO has no draft, by design. + + CDS SDL allows comments as a language rule [source: abap-docs + /abap-docs-cloud/ABENCDS_F1_SDL_SYNTAX, ABENCDS_GENERAL_SYNTAX_RULES, + verified 2026-09-16], so the refusal comes from the service definition + tooling, not the language. U14 (the binding) was not created. +- Why: wave D of the UI leg. +- Affected: dev tenant `ZARENA_PLM`, PRD RAP BO status, KNOWLEDGE.md. +- Decision: I re-save U13 with only its definition (annotation, `define + service`, `expose`) and no header comment, then activate again. If it + still fails with only the draft message, that message is blocking, and I + read its severity in the editor's Problems view before changing anything + about draft. + +## 2026-09-16 — `abap-adt-mcp` parked: SSO cannot complete over remote access + +- What changed: with `SAP_BROWSER_PATH` set, the SSO login launched + Chrome with a dedicated profile on the server's desktop display and + waited on the Microsoft Entra sign-in page. The login does not use a + localhost redirect: SAML returns to the tenant, and the server harvests + the session cookies from its own Chrome over the DevTools protocol + (`dist/lib/browserLogin.js`). I tried a localhost-only DevTools relay on + a fixed port for remote interaction. It is not workable through + multi-hop SSH proxies, so I stopped it and closed the login browser. No + login happened. The server remains registered, read-only. +- Why: the sign-in has to happen inside the server's browser, and that + browser is on a desktop I cannot reach from this remote session. +- Affected: PRD OQ-9, KNOWLEDGE.md. +- Decision: I park `abap-adt-mcp` rather than remove it. One sign-in at + the server's desktop, with "stay signed in", should let later logins run + unattended in the invisible browser; that is unverified. The UI leg + finishes on the proven path: SAP `adt-mcp` for create and activate, + source pasted in the VS Code editor. Four wave-D sources, one binding and + a publish remain. + +## 2026-09-16 — `abap-adt-mcp` connected; SSO blocked on browser detection + +- What changed: after the restart, `abap-adt-mcp` connected. `listSystems` + showed exactly one destination, `MY431353` (client 100, SSO), with policy + `readOnly: true` and `allowedPackages: ["ZARENA_PLM"]`. The first SAP call + (`classIncludes` on `ZBP_R_CHANGEAPPROVAL`) failed before any login: "No + Chrome/Edge/Brave found for SSO login." The server's browser detection + covers only macOS paths (KNOWLEDGE 2026-09-16). +- Why: SSO login is the gate to the read-only Local Types check. +- Affected: project MCP registration (env), KNOWLEDGE.md. +- Decision: the registration gains `SAP_BROWSER_PATH=/usr/bin/google-chrome` + and nothing else changes. This is a manual step, like the original + registration. + +## 2026-09-16 — `abap-adt-mcp` registered read-only in project scope + +- What changed: `abap-adt-mcp` is registered in the project `.mcp.json` + (git-ignored, mode 600). It runs over stdio as `npx -y + abap-adt-mcp@2.1.0` with `MCP_READ_ONLY=1`, an audit log at + `~/.abap-adt-mcp/audit.jsonl`, and the desktop display variables for the + SSO browser. Its systems file lists only the development tenant + `my431353`, client 100, SSO, with `policy.readOnly: true` and + `allowedPackages: ["ZARENA_PLM"]`. The six existing project servers are + unchanged, `adt-mcp` included. +- Why: OQ-9. Agent-side source read and write would replace manual pasting + for the remaining RAP objects. +- Affected: project MCP configuration, PRD OQ-9. +- Decision: the first use is a read-only `getObjectSource` of the behavior + pool's Local Types include. That check settles whether wave C's + activation proved the handler constructs. Write access is enabled only + after that read succeeds. The CLAUDE.md MCP routing table changes only + once the server has written and activated an object successfully. + +## 2026-09-16 — `abap-adt-mcp` source review: all checks cleared; written report not produced + +- What changed: the read-only source review of `abap-adt-mcp` 2.1.0 cleared + every check it ran (install scripts, network egress, credential and + session storage, policy enforcement, dangerous primitives), observed as it + ran. It then stalled while collecting line citations, consumed its budget, + and was stopped at ~03:28, so no written report with file:line evidence + exists. As of 03:30 the package is not registered, no `~/.abap-adt-mcp` + configuration exists, and no login has happened. The tenant state is + unchanged: waves A–C are active, and wave D (U9, U11, U12, U13) exists + empty. +- Why: OQ-9 was gated on this review. +- Affected: PRD OQ-9. +- Decision: I accept the review for the development tenant only, under the + read-only-first configuration (pinned 2.1.0, `readOnly: true`, + `allowedPackages: ["ZARENA_PLM"]`, `MCP_READ_ONLY=1`, audit file on). + Registration is run by hand. The missing citations mean this clearance + does not extend to the productive tenant. Any future review brief is + scoped to named files with a size budget, because this one ran unbounded. + +## 2026-09-16 — `abap-adt-mcp` registration is a manual step; read-only first + +- What changed: an attempt to write `~/.abap-adt-mcp/systems.json` and to + register `abap-adt-mcp` in the project MCP configuration was refused by + Claude Code's permission guard as self-modification. Nothing was written. + I confirmed the development tenant logs in on client 100. The package's + SSO login launches Chrome visibly (`headless: false`), so the MCP server + needs the desktop session's display variables. +- Why: I want agent-side source writes (OQ-9), and registration is the + gate. +- Affected: PRD OQ-9, project `.mcp.json` (only once registered by hand), + CLAUDE.md MCP routing (only once adopted). +- Decision: the registration is run by hand, not routed around the guard. + The first configuration is read-only (`policy.readOnly: true`, + `allowedPackages: ["ZARENA_PLM"]`, `MCP_READ_ONLY=1`), limited to the + development tenant, and pinned to 2.1.0. The first login waits for the + source review verdict. Writes are enabled only after a read-only + `getObjectSource` on the behavior pool's Local Types include succeeds. + +## 2026-09-16 — Community `abap-adt-mcp` smoke-tested read-only; not adopted yet (OQ-9) + +- What changed: I ran `abap-adt-mcp` 2.1.0 once through `npx` over stdio. + It ran with `MCP_READ_ONLY=1` and a scratchpad systems file listing only + the development tenant, restricted to `ZARENA_PLM`. `initialize` and + `tools/list` answered: 173 tools, including source read/write and + activation. `listSystems` showed the read-only, package-restricted policy + enforced. There was no login and no SAP call. Nothing was registered in + any Claude Code or project configuration (verified 03:12). The package + remains in the local npx cache. I opened OQ-9. +- Why: SAP's ADT MCP cannot write source (V-4), so every RAP source is + pasted by hand. A server that can read and write source would remove that + step and would also let me read the behavior pool's Local Types include + directly. +- Affected: PRD OQ-9, V-4 (possible alternative), CLAUDE.md MCP routing (a + change only on adoption). +- Decision: I do not adopt it mid-build. Only four pastes and one publish + remain on the UI leg. The package was published the same day, by one + maintainer, and would hold a browser SSO session into the tenant, so it + gets a source review before any login. I run it read-only first. The + productive tenant `my429480` is never listed in its systems file. + +## 2026-09-16 — Wave C active: behavior pool and UI projection view live; wave D created + +- What changed: I activated U6 (base BDEF) and U7 (behavior pool + `ZBP_R_CHANGEAPPROVAL`) together in one call, then U8 (UI projection + view `ZC_CHANGEAPPROVAL`). All answered "Activation successful." The + locally cached class metadata shows the class active with category + `behaviorPool`. I then created wave D empty: U9 projection BDEF + `ZC_CHANGEAPPROVAL` (`.bdef.abdl`), U11 access control + (`.dcls.acds`), U12 metadata extension (`.ddlx.acds`) and U13 service + definition `ZUI_ARENA_CHANGE_APPROVAL` (`.srvd.acds`). `SRVD/SRV` also + offers an optional `referencedObject` create field [source: adt-mcp + abap_activate_objects, abap_creation-create_object, + get_object_type_details on 0M7DEJI, verified 2026-09-16]. +- Why: wave C of the UI leg. Wave D is the last paste group before the UI + binding. +- Affected: dev tenant `ZARENA_PLM`, PRD RAP BO status, handover README + (BDEF extension `.bdef.abdl` for U9 and W3; SRVD tag list), KNOWLEDGE.md. +- Decision: I do not record `OPTIONS PRIVILEGED ACCESS`, + `%op-%action-` on MODIFY response rows or `@status-rejected` as + proven yet. Activation returns no warnings, and no source says whether a + behavior pool with an empty Local Types include fails activation. I + confirm the handler source is in the Local Types include in the editor + first. Failing that, the first action call at U15 proves it at runtime. + I correct the handover README's BDEF extension and SRVD tag list in one + pass after the UI leg, rather than interrupting the paste session. + +## 2026-09-16 — Wave B active: base behavior definition and root access control live + +- What changed: I activated U10, the access control `ZR_CHANGEAPPROVAL`, + and U6, the managed `strict ( 2 )` behavior definition + `ZR_CHANGEAPPROVAL`. Both answered "Activation successful." U6 activated + through its `.bdef.abdl` URI on the first attempt, before its behavior + pool class existed. That also proves the three action-parameter entities + it references are active. I then created wave C empty: U7, the behavior + pool `ZBP_R_CHANGEAPPROVAL` (`create_object` returned only the global + `.clas.abap` path, no local-types include), and U8, the UI projection view + `ZC_CHANGEAPPROVAL` (`.ddls.acds`) [source: adt-mcp abap_activate_objects, + abap_creation-create_object on 0M7DEJI, verified 2026-09-16]. +- Why: wave B of the UI leg. +- Affected: dev tenant `ZARENA_PLM`, PRD RAP BO status, handover README + (the BDEF activation extension is `.bdef.abdl` for U6, U9 and W3, and the + U6 fallback was not needed), KNOWLEDGE.md. +- Decision: now that an empty `ZBP_R_CHANGEAPPROVAL` exists, I activate U6 + and U7 together in one call, so the BDEF is never checked against a class + that is not yet a behavior pool. The activation tool returns no warnings, + so warnings about the four unmapped calculated fields are checked in the + editor's Problems view rather than assumed absent. I locate the + local-types include live, after the global class part is saved. + +## 2026-09-16 — Wave A active: table, parameter entities and root view live; wave B created + +- What changed: + - I activated U1 `ZCHANGEAPPROVAL`, U2–U4 (the three action-parameter + entities) and U5 `ZR_CHANGEAPPROVAL` on `my431353`. Each activation + answered "Activation successful.", and the tool returned no warnings. + - The table activates through its `.tabl.ddic` URI; `.tabl.json` holds + only header metadata. + - U5's `cast( case … as abap_boolean preserving type )` was accepted, so + the fallback is not needed [source: adt-mcp abap_activate_objects on + 0M7DEJI, verified 2026-09-16]. + - I then created wave B empty: U6, the managed BDEF `ZR_CHANGEAPPROVAL` + (`create_object` returned `…/zr_changeapproval.bdef.abdl`), and U10, the + access control `ZR_CHANGEAPPROVAL` (`…/zr_changeapproval.dcls.acds`, + with `protectedEntity`). +- Why: wave A of the UI leg. Creating the U6 BDEF requires the root view to + be active, and it validated, which confirms U5 is active. +- Affected: dev tenant `ZARENA_PLM`, PRD RAP BO status, the handover README + activation table (TABL now proven; BDEF extension open), KNOWLEDGE.md. +- Decision: BDEF source is pasted into `.bdef.abdl`, not `.bdef.json`. At + U6 activation I try the `.bdef.abdl` URI first and fall back to + `.bdef.json`, which activated a generator BDEF on 2026-09-15. The README + activation table is corrected once U6 proves the extension. Activation + gives no direct active/inactive readback, so proof is indirect: U6's + activation is the first test of U2–U4, because U6 names them as action + parameters. + +## 2026-09-16 — Review fixes applied to the handover; duplicate checks read with privileged access + +- What changed: all four remaining review fixes are in `docs/handover/rap/`. + - `use etag` sits in the entity header of both projection BDEFs. + - `retrySync` writes APPROVED, so EXECUTING is written only by + `markExecuting`. + - All six action handlers set `%fail-cause` and `%op-%action-`, + merge the internal UPDATE's failed and reported rows, and return + results only for keys that did not fail. + - The base BDEF declares `validation checkDuplicate on save { create; }`. + Its handler refuses a second non-REJECTED record with the same + `ItemNumber` + `ChangeType` + `ArenaEcoId`, within the request or + against persisted rows. + + The base BDEF and README carry the OQ-8 note that `authorization master + ( none )` is not final. The table's paste target is `.tabl.ddic`; its + activation extension is still pending live proof. Two of my working + sessions wrote these files concurrently between 02:36 and 02:43. I + confirmed the result is consistent and stable (content unchanged over 45 + seconds at 02:45, no duplicate governance entries). This session is now + the only one driving the build. +- Why: review findings B1, M1, m1 and m4 (entry below). Every one must be + in the source before its object is pasted. +- Affected: handover U6, U7, U9, W3, U1 header, README; PRD RAP BO status. +- Decision: the duplicate check reads `ZR_CHANGEAPPROVAL` with privileged + access. The view is access-controlled, and a handler `SELECT` on it + without privileged access breaks the RAP implementation rules [source: + sap-docs sap-help abaprap_impl_rules; abap-docs + /abap-docs-cloud/ABAPSELECT_PRIVILEGED_ACCESS, verified 2026-09-16]. The + validation's failed rows carry only `cause-unspecific`, the only cause + validations may use (same rules page). Three constructs are syntax I + prove only at tenant activation, each with a documented fallback: + `OPTIONS PRIVILEGED ACCESS` (fallback `FROM … WITH PRIVILEGED ACCESS`), + `%op-%action-` on MODIFY response rows, and `preserving type` on + the U5 casts. + +## 2026-09-16 — Root view U5 final: derived booleans cast as a whole + +- What changed: in `zr_changeapproval.ddls.asddls`, `IsApprovable` and + `IsRetriable` are now `cast( case status when … then abap.char'X' else + abap.char' ' end as abap_boolean preserving type )`. The file header + records why: typed `abap.char` literals make the CASE result CHAR(1) + [source: abap-docs /abap-docs-cloud/ABENCDS_TYPED_LITERAL_V2, verified + 2026-09-16], and `ABAP_BOOLEAN` is "of data type `CHAR` of length 1" + [source: abap-docs /abap-docs-standard/ABENDATAOBJECTS_TRUE_VALUE_GUIDL, + verified 2026-09-16], so `preserving type` matches exactly. U5 is cleared + for paste. +- Why: review finding (4). A cast inside a CASE branch drops the data + element semantics. +- Affected: handover U5, wave A paste. +- Decision: if activation still rejects `preserving type`, I drop only + that addition, not the outer cast. I finalized U5 ahead of the remaining + correction work so wave A is not held up. Two concurrent corrections + collided on this file; each later brief names file ownership explicitly. + +## 2026-09-16 — App documentation aligned to retry → APPROVED + +- What changed: I updated the four statements of the retry transition in + `app/` (README, both i18n `#XBUT` comments, the mock `metadata.xml` + comment) from FAILED → EXECUTING to FAILED → APPROVED. No labels, + annotations, manifest or mock data changed. `npm run lint` and + `npm run ts-typecheck` pass. +- Why: ARN-002 was amended today; the app's documentation must not + describe the old state machine. +- Affected: `app/README.md`, `app/webapp/i18n/i18n.properties`, + `app/webapp/i18n/i18n_en.properties`, + `app/webapp/localService/mainService/metadata.xml`, KNOWLEDGE.md. +- Decision: I do not add a mock `executeAction` handler to show the new + transition. The mock server ignores actions (KNOWLEDGE 2026-09-16), and + the live RAP service replaces the mock at V-2 within this build, so a + mock-only transition would be throwaway work. The remote + `ui5-mcp-server` cannot lint local files; I lint UI5 code locally until + that changes. + +## 2026-09-16 — Pre-paste SAP review: one blocker, retry semantics corrected, authorization opened + +- What changed: I reviewed all 18 handover sources read-only against ABAP + Cloud docs, clean-core release states, the PRD and the app contract. The + app contract holds (17 properties, 3 actions, entity set `ChangeApproval`, + no UI create/update/delete), and all 11 referenced SAP objects are clean + core level A [source: sap-released-objects sap_check_clean_core_compliance, + verified 2026-09-16]. The review found: + - One blocker: `use etag` sat in the body of both projection BDEFs, but + it belongs in the entity header [source: abap-docs + /abap-docs-cloud/ABENBDL_CHARACTER_PROJECTION, verified 2026-09-16]. + - Two majors: retry semantics and missing per-operation authorization. + - Four minors. + I dispatched corrections before any affected source reached the tenant. +- Why: every defect caught here saves a paste-and-activate round trip + against the live tenant, and two of them were correctness defects, not + syntax. +- Affected: `docs/handover/rap/` (U5, U6, U7, U9, W3, README), PRD ARN-002, + RAP-2, new OQ-8, `app/` documentation comments, KNOWLEDGE.md. +- Decision: + - (1) `retrySync` goes FAILED → APPROVED, clears `ErrorMessage`, and the + worker re-claims the record through `markExecuting`. The earlier FAILED + → EXECUTING could not be claimed, because `markExecuting` accepts only + APPROVED. Working around that would make a retried record + indistinguishable from one another run is executing, which is the + double-write risk ARN-006 exists for. + - (2) Duplicate staging is refused on create: a `checkDuplicate` + validation on the semantic key while a non-REJECTED record exists. + Webhook and delta poll can both deliver the same ECO, and I will not + leave idempotency to the executors alone. + - (3) Action handlers honor the RAP implementation contract (`%fail`, + `%op`, propagated UPDATE failures, results only for successful keys), + so the worker cannot read a failed `markSynced` as success. + - (4) The derived boolean flags cast the whole CASE to `abap_boolean`, + because a cast inside a CASE branch drops the data element semantics + [source: abap-docs /abap-docs-cloud/ABENCDS_CAST_EXPRESSION_V2, verified + 2026-09-16]. + - (5) Deferred: a metadata-level mandatory `RejectReason`, until after + the POC (server-side enforcement exists). + - (6) Opened: OQ-8. `authorization master ( none )` stays for the + dev-tenant POC only and must be replaced before business roles get the + app. + +## 2026-09-16 — Wave A created: table, parameter entities, root view exist empty + +- What changed: I created U1 `ZCHANGEAPPROVAL` (TABL) and U2–U5 + `ZD_CHANGEAPPROVAL_REJECTP`, `ZD_CHANGEAPPROVAL_SYNCEDP`, + `ZD_CHANGEAPPROVAL_FAILEDP` and `ZR_CHANGEAPPROVAL` (DDLS) in + `ZARENA_PLM`. They are inactive and have no source. `abap_transport-get` + answered `isRecordingRequired: false`. Each object passed validation and + creation with the handover JSON unchanged. The table's returned URI ends + in `.tabl.ddic`, not the `.tabl.json` that activated a generator-created + table on 2026-09-15 [source: adt-mcp abap_creation-create_object on + 0M7DEJI, verified 2026-09-16]. +- Why: wave A of the UI leg. The sources are pasted next. +- Affected: dev tenant `ZARENA_PLM`, PRD RAP BO status, handover activation + URI table (TABL extension open). +- Decision: I activate U1 with the URI `create_object` returned + (`.tabl.ddic`) and fall back to `.tabl.json`. The handover URI table is + corrected once activation proves which one works. + +## 2026-09-16 — Package recreated; UI-leg build started in waves + +- What changed: package `ZARENA_PLM` exists again under `ZLOCAL`. ADT MCP + validation of `ZCHANGEAPPROVAL` in it succeeded (live, 02:27). I started + wave A of the UI leg: U1–U5 (table, the three action-parameter entities, + root view) are created empty so their sources can be pasted. I also read + the create fields for classes, access controls and metadata extensions + live. Those were the three types the handover had marked unconfirmed. + Metadata extension names allow 40 characters. Access controls and + metadata extensions take an optional protected/extended entity (KNOWLEDGE + 2026-09-16). +- Why: the from-scratch build (OQ-7) is unblocked. The build moves in waves + bounded by activation dependencies: BDEF creation needs its root view + active, and the binding needs its service definition active. Each wave + asks for one paste session per group, not one per file. +- Affected: dev tenant `ZARENA_PLM`, PRD RAP BO status, KNOWLEDGE.md. +- Decision: waves are A = U1–U5 (create, paste, activate), B = U6 base + BDEF, C = U7 behavior pool, D = U8 projection view + U10 root access + control, E = U9 projection BDEF + U11 + U12 + U13, F = U14 binding with + the service-alias check. Nothing is activated before its source is + pasted. + +## 2026-09-16 — Handover regenerated under clean names; binding names cut to SAP's 26-character limit + +- What changed: I regenerated `docs/handover/rap/` for a build from an + empty package. Eleven source files were renamed to the clean object + names, every internal reference was updated, and the README was replaced + with a create → paste → activate sequence per object. The UI service + definition exposes only `ChangeApproval`. A scripted comparison against + the app's mock `metadata.xml` confirms that the UI projection carries all + 17 app properties plus `approve`, `reject` and `retrySync`. The build + splits into a 14-source UI leg and a 4-source worker leg. The + `markSynced`/`markFailed` parameter entities move into the UI leg, + because the base BDEF references them and they must be active first. I + amended RAP-5 to two service definitions with one binding each, and + updated both PRD interface-contract rows. A read-only SAP review of the + 18 sources is running before the paste session. +- Why: the tenant package is empty, so the handover's generator inventory + and `ZZ…000` targets no longer existed. SAP Help gives a maximum of 26 + characters for service binding names, the same in ABAP Cloud, SAP Cloud + Platform and ABAP Platform 2021 [source: sap-docs sap-help loio + 4bad0d7280b742a68dd2d42b1441f09a, f3e608ace2a64aada33bdd3e2419113a, + 777e027f61c3490dba0433443d9143a6, verified 2026-09-16]. The ADT MCP's + create metadata allows 30, and generator bindings of 29–30 characters + activated on 2026-09-15, but none was ever published. No source says + whether publishing tolerates more than 26. +- Affected: PRD RAP-5, the two approval-BO interface-contract rows, RAP BO + status, `docs/handover/rap/`, V-2 (the app's dataSource URL, mock + namespace and annotation targets now carry a planned difference), + KNOWLEDGE.md. +- Decision: the bindings are `ZUI_ARENA_CHANGE_APPR_O4` (24) and + `ZAPI_ARENA_CHANGE_APPR_A2X` (26). The service definitions keep + `ZUI_ARENA_CHANGE_APPROVAL` and `ZAPI_ARENA_CHANGE_APPROVAL`. I chose the + documented limit over the tool's undocumented allowance, because a + refusal at publish would come after the whole UI leg and need a delete in + ADT to recover. The app edit this causes is one URL segment plus the + namespace, both already scheduled for V-2 reconciliation against live + `$metadata`, so I defer it to that brief instead of changing the app + twice. The derived-field EDM types (`StatusCriticality` as INT1, + `abap_boolean` flags, `utclong`, `sysuuid_x16`) stay unverified until + live `$metadata` exists; they are not assumed. + +## 2026-09-16 — Deletion verified live: package `ZARENA_PLM` is gone; name limits confirmed + +- What changed: with the ADT MCP back online I probed `my431353` read-only. + `abap_creation-run_validation` answers "Package 'ZARENA_PLM' does not + exist" for tables, data definitions and classes, the same as a + nonexistent control package. The deletion removed the package itself, not + just its contents. `abap_creation-get_object_type_details` gives the + maximum name lengths: database table 16; data definition, service + definition, service binding and behavior definition 30 [source: adt-mcp + abap_creation-get_object_type_details on 0M7DEJI, verified 2026-09-16]. +- Why: the from-scratch build starts from the package, and every clean name + had to fit its limit before any object is created. +- Affected: PRD RAP BO status, `docs/handover/rap/README.md` (step 0 is now + "recreate the package"), KNOWLEDGE.md. +- Decision: I recreate `ZARENA_PLM` under `ZLOCAL` under the same name, so + every activation URI pattern verified on 2026-09-15 stays valid. All clean + names fit (`ZCHANGEAPPROVAL` 15, `ZUI_ARENA_CHANGE_APPROVAL_O4` 28). + `ZLOCAL` means no transport request on every create while I prototype. It + is not a dead end for production: a package can be reassigned from + `ZLOCAL` to `ZCUSTOM_DEVELOPMENT` (transport layer YDEV) later, though not + the reverse [source: sap-docs sap-help loio + a0d80f14fcb74cb1ac8fb6f3d9ad48cc (2608), verified 2026-09-15]. I make + that move once the approval app works end to end on the development + tenant. + +## 2026-09-16 — Tenant package emptied; from-scratch build under names that match the Fiori app + +- What changed: the `ZARENA_PLM` contents on `my431353` were deleted in ADT. + This includes both generator sets and the empty hand-created objects. I + have not yet verified the deletion live (the ADT MCP is offline), and the + package's own survival is unconfirmed. I closed OQ-7 and dispatched a + regeneration of `docs/handover/rap/` under the clean names. +- Why: the Fiori app binds to + `/sap/opu/odata4/sap/zui_arena_change_approval_o4/srvd/sap/zui_arena_change_approval/0001/`, + entity set `ChangeApproval`, and actions `approve`, `reject` and + `retrySync` (`app/webapp/manifest.json`, mock `metadata.xml`). A Fiori app + runs against a replacement service only if the entity set names, + properties and actions match [source: fiori-mcp-server search_docs + "Replacing OData Service in an Adaptation Project", verified 2026-09-16]. + With the package empty, no leftover objects compete for these names. +- Affected: PRD OQ-7 (closed), RAP BO status, `docs/handover/rap/` (all 19 + sources and the README), V-2 (the service identity is now designed to + match the app instead of reconciled after the fact). +- Decision: no generator this time, because it forces a `ZZ` prefix and a + `000` suffix (KNOWLEDGE, 2026-09-15). I create each object through the ADT + MCP under its final name: table `ZCHANGEAPPROVAL`, `ZR_CHANGEAPPROVAL`, + `ZBP_R_CHANGEAPPROVAL`, `ZC_CHANGEAPPROVAL`, SRVD + `ZUI_ARENA_CHANGE_APPROVAL`, SRVB `ZUI_ARENA_CHANGE_APPROVAL_O4`. The A2X + names are unchanged. The UI leg (about 13 sources) is built and verified + against the app before the worker leg starts. + +## 2026-09-16 — Tenant state corrected: two generator sets, no designed logic deployed; naming decision opened + +- What changed: a live ATC run over `ZARENA_PLM` returned 43 objects, not + the 12 my 2026-09-15 handover README lists. The package holds two default + `x-ui-service` generator sets side by side: the `ZZ…000` set + (`ZZCHNGPPROVAL000`, `ZZR_/ZZC_CHANGEAPPROVAL000`, binding + `ZZUI_ARENA_CHANGE_APPRVL000_O4`) and a set without the suffix + (`ZZCHANGEAPPROVAL`, `ZZR_/ZZC_CHANGEAPPROVAL`, binding + `ZZUI_ARENA_CHANGE_APPROVAL_O4`). Each set has its own behavior pools and + SAP object types. It also holds the five empty, inactive hand-created + objects and the probe class. None of the six actions, the status machine, + or the feature control exists in any tenant object. The designed source + lives only in `docs/handover/rap/`, which is now committed and pushed + (`f8b45ea`). My most likely explanation for the second set is an earlier + generator run that I never recorded; I cannot confirm it, because the ADT + MCP has no read tool. +- Why: my 2026-09-15 entry and the PRD status described a clean skeleton + that was ready for a paste session. Following them would put the sources + into objects whose names the Fiori app does not bind to. +- Affected: PRD status (RAP BO → blocked on OQ-7), new OQ-7, + `docs/handover/rap/README.md` (its object inventory is stale), the Fiori + app's V-2 service identity. +- Decision: none yet. I recommend building under clean names that match the + app, next to the unused sets, and deferring deletion. The two sets are + unpublished, their `ZZ` names do not collide with the clean set, and + deleting them needs Eclipse ADT, which is not set up. Of the 19 sources, + 13 serve the UI milestone. The six A2X-leg files wait until the app runs + against the real service. + ## 2026-09-15 — abapGit researched: not required for ADT for VS Code; viable as a bulk source-load path - What changed: I verified that abapGit is neither required nor used by SAP's diff --git a/KNOWLEDGE.md b/KNOWLEDGE.md index 0751837..52d4b02 100644 --- a/KNOWLEDGE.md +++ b/KNOWLEDGE.md @@ -101,6 +101,208 @@ entries the same way — dated and falsifiable. SRVB creation validate the referenced object's ACTIVE version, so create and activate DDLS/SRVD sources first. ATC via MCP rejects RONT/NONT. +- [2026-09-16] **An expired VS Code ADT logon shows up in the ADT MCP only as + "Your user was logged off."** `abap_list_destinations` still lists the + destination and `get_object_type_details` still returns field metadata, + so neither proves a live session. **ADT for VS Code logs on to S/4HANA + Cloud with a reentrance ticket and a localhost redirect:** it opens + `https:///sap/bc/adt/core/http/reentranceticket?redirect-url=http://localhost:/adt/redirect`, + and after SSO the tenant redirects to + `http://localhost:/adt/redirect?…&reentrance-ticket=` + (observed 2026-09-16). The ticket in that URL is a live logon credential: + never paste it into chats, logs or files. The same mechanism is the + plausible browser-free login path for `abap-adt-mcp` (OQ-9). **Tested + 2026-09-18: it does not work.** With a freshly captured reentrance + ticket, `https://my431353.s4hana.cloud.sap/sap/bc/adt/core/discovery` + and an ABAP class source URL answered HTTP 200 with the same 3094-byte + SAML redirect page (`SAMLRequest`, `saml2`) for all of: `Cookie: + MYSAPSSO2=`, header `MYSAPSSO2: `, and no credential at + all, with and without `?sap-client=080`. A reentrance ticket is + therefore neither a logon-ticket cookie nor an assertion-ticket header + here; ADT exchanges it some other way. Do not spend more on + `abap-adt-mcp`'s `sso2` mode without first capturing what the extension + actually sends. `abap_creation-run_validation` is the + cheapest read-only liveness check (it answers "… validation failed: Your + user was logged off."). Fix it by logging on to the destination again in + VS Code; the MCP port and token stay the same unless VS Code reloads + (live 2026-09-16, ~1.5 h after the last successful call). +- [2026-09-16] **Ask the ADT MCP for name limits and package existence; do + not guess them.** `abap_creation-get_object_type_details` returns each + type's create fields and `maxLength`: `TABL/DT` 16, while `DDLS/DF`, + `SRVD/SRV`, `SRVB/SVB` and `BDEF/BDO` allow 30 (the generator's `APPRVL` + truncation came from that 30 limit). `abap_creation-run_validation` is a + read-only way to check that a package exists: a missing package answers + "Package '' does not exist" [source: adt-mcp on 0M7DEJI, verified + 2026-09-16]. The Claude Code session does not expose the adt-mcp tools + unless the server was up at session start. A JSON-RPC probe over + `http://127.0.0.1:2236/mcp` with the `.mcp.json` bearer token works for + read-only checks in the meantime. + +- [2026-09-16] **In the VS Code ADT workspace, the `.json` file in an object + folder is header metadata, never the source.** A table folder holds + `zchangeapproval.tabl.json` (formatVersion, description, originalLanguage, + `abapLanguageVersion`) and `zchangeapproval.tabl.ddic` (the Dictionary DDL + source). Paste into `.ddic`, and activate through the `.tabl.ddic` URI + (live, "Activation successful."). For objects created with + `create_object`, the returned `filePath` names the source file: DDLS + `.ddls.acds`, DCLS `.dcls.acds`, BDEF **`.bdef.abdl`** [source: adt-mcp + abap_creation-create_object / abap_activate_objects on 0M7DEJI, verified + 2026-09-16]. The `.tabl.json` / `.bdef.json` activation URIs recorded on + 2026-09-15 were generator objects; trust the returned `filePath` over + that table. BDEF activation through `.bdef.abdl` is proven (U6, live). +- [2026-09-16] **A managed `strict ( 2 )` BDEF activates before its + behavior pool class exists** (`ZR_CHANGEAPPROVAL`, "Activation + successful.", with no class `ZBP_R_CHANGEAPPROVAL` yet) [source: adt-mcp + abap_activate_objects on 0M7DEJI, verified 2026-09-16]. Once the class + exists, activate BDEF and class together. `abap_activate_objects` returns + only "Activation successful." with no warnings on success. On failure it + lists messages with line and column but no severity (U13 SRVD, + 2026-09-16), so read severity in the editor. Service definitions + report "Comments are not supported and will be deleted on save" for a + leading block comment, and **that message blocks activation**: the same SRVD failed with a + 19-line `/* */` header and activated without it (live 2026-09-16; the + DCLS, DDLX and BDEF headers did not block). The language allows comments + [source: abap-docs /abap-docs-cloud/ABENCDS_GENERAL_SYNTAX_RULES, + verified 2026-09-16], so this is the tooling. Keep SRVD sources free of + comments. `abap_atc_get_result` returned only totals for a run with 43 + infos (UI leg), but per-object `findings[]` (priority, check class, + message id and title, quick-fix flags) for a run with a priority-3 + warning (worker leg). Expect details for warnings and errors at least + (both runs live 2026-09-16). A projection BDEF over a view with + `@AccessControl.authorizationCheck: #CHECK` raises SLIN W607 (+BDL 549) + "should be replaced with #MANDATORY" (priority 3; it does not block + activation). An OData V4 **Web API** binding reports + `bindingCategory: "1"` (UI: `"0"`) in `fetch_services`. +- [2026-09-16] **OData V4 identity on a RAP UI service: the binding name is + only the URL's first segment.** For definition `ZUI_ARENA_CHANGE_APPROVAL` + and binding `ZUI_ARENA_CHANGE_APPR_O4`, the live `$metadata` uses + namespace `com.sap.gateway.srvd.zui_arena_change_approval.v0001`, from the + **service definition**, and entity type `ChangeApprovalType`, with a + `Type` suffix and no underscore (live 2026-09-16, + `docs/v2/zui_arena_change_appr_o4.metadata.xml`). The earlier assumption + that the namespace carries the binding name and a `_Type` suffix was + wrong. RAP also emits `__OperationControl` with + `Core.OperationAvailable Path="_it/__OperationControl/"` for + `features : instance` actions, `SAP__Messages`, `Nullable="false"` on + non-key elements, and `cast( case … as abap_boolean preserving type )` + surfaces as `Edm.Boolean` (INT1 literals as `Edm.Byte`, `utclong` as + `Edm.DateTimeOffset Precision="7"`). Copying `$metadata` from Chrome's + XML tree view decodes `&` to `&`; use `view-source:` or re-escape. +- [2026-09-16] **Live RAP `$metadata` works verbatim as Fiori mock metadata** + (`@sap-ux/fe-mockserver-core` 1.7.15 serves it and accepts `$select` on + `__OperationControl/approve`). Mock rows must then carry + `__OperationControl`, or the enablement paths resolve to nothing. The + app's ESLint config checks only TS/JS, so run `npx @ui5/linter` for UI5. + The fiori-mcp `list_functionality` for `app/` offers only + add/delete page and controller extension, so dataSource and annotation + edits are made directly. The cached `@ui5/manifest` 1.89.0 schema + rejects the generator's manifest `_version: "2.11.0"`; that is not a + defect in the app. +- [2026-09-16] **A published custom UI service answers on the UI host, not the + `-api` host, for a business user.** `fetch_service_information` reports + the service URL on `my431353-api.s4hana.cloud.sap` (with + `sap-client=080`), but a browser business-user call there is refused as + "Forbidden / blocked by UCON". The same path on + `my431353.s4hana.cloud.sap` returns `$metadata` for the developer user, + before any IAM app exists (live 2026-09-16). The `-api` host is the + communication-user entry point (communication arrangement), so the + worker's A2X service goes there through a custom communication scenario + (V-2), never the UI service. After publishing, ADT for VS Code reports + " published locally". +- [2026-09-16] **In ADT for VS Code, publishing a service binding is a code + lens, not a button or a palette command.** Open the binding's `.srvb.json` + source and click the **Publish** code lens; Unpublish and Preview… work the + same way [source: sap-docs sap-help loio dd6f5de63e7245a7abe7e39389841596 + "ABAP RAP Tools", verified 2026-09-16]. The extension (1.1.2) contributes + no publish command, and the SAP ADT MCP has no publish tool. `create_object` for a class returns only the global + `.clas.abap` path, no local-types (CCIMP) path. +- [2026-09-16] **`abap-adt-mcp` 2.1.0 auto-detects a browser for SSO only on + macOS.** `detectBrowser()` checks `SAP_BROWSER_PATH` and then three + `/Applications/…` paths; otherwise it throws "No Chrome/Edge/Brave found + for SSO login" (`dist/lib/browserLogin.js:38-50`, read 2026-09-16). On + this Linux host the server env must carry + `SAP_BROWSER_PATH=/usr/bin/google-chrome` along with the desktop display + variables. Login harvests the HttpOnly `MYSAPSSO2`/`SAP_SESSIONID` cookies + over the Chrome DevTools Protocol (`puppeteer-core`). `listSystems` needs + no SAP call and is the safe first probe of its policy. **Its SSO does not + work from a remote shell:** there is no localhost redirect (SAML returns + to the tenant), and the sign-in must be typed into the server-side Chrome + window (`headless: false`, profile + `~/.abap-adt-mcp/sso/`, `--remote-debugging-port=0`, 300 s timeout). + Over multi-hop SSH neither the desktop nor a DevTools relay is reachable in + practice. Do the first sign-in at the server's desktop; with "stay signed + in" later logins should complete unattended (unverified, 2026-09-16). +- [2026-09-16] **Create fields for the remaining RAP types (`get_object_type_details` + on 0M7DEJI):** `CLAS/OC`: packageName, name (≤30), description, optional + superclass, interfaces. `DCLS/DL`: packageName, name (≤30), description, + optional `protectedEntity`. `DDLX/EX`: packageName, name (≤**40**), + description, optional `extendedEntity` [source: adt-mcp + abap_creation-get_object_type_details on 0M7DEJI, verified 2026-09-16]. + Pass `protectedEntity` / `extendedEntity` so the editor starts from the + right skeleton. DCLS creation does **not** require the protected entity to + be active. `ZC_CHANGEAPPROVAL_A2X` access control validated and was + created while its DDLS was still empty and never activated (unlike + BDEF/SRVB creation). `SRVB/SVB` `bindingType` values are exactly `OData + V2 - UI`, `OData V2 - Web API`, `OData V4 - UI`, `OData V4 - Web API`, + whatever label the release notes use [source: adt-mcp + abap_creation-run_validation / create_object / + get_object_type_details on 0M7DEJI, verified 2026-09-16]. +- [2026-09-16] **Service binding names: SAP Help says 26 characters, the + ADT MCP allows 30.** The 26-character limit appears on the ABAP Cloud, + SAP Cloud Platform and ABAP Platform 2021 "Creating Service Binding" + pages [source: sap-docs sap-help loio 4bad0d7280b742a68dd2d42b1441f09a, + verified 2026-09-16]. `get_object_type_details` reports 30 for + `SRVB/SVB`, and the `x-ui-service` generator itself emits 29–30-character + bindings, which activated unpublished on `my431353` on 2026-09-15. + Whether publishing accepts more than 26 is unknown. Stay at 26 or fewer + rather than find out after a whole BO leg. The generator does not keep to + the documented limit, which is one more reason not to use it for names + that ship. +- [2026-09-16] **In an OData V4 binding URL + `/sap/opu/odata4/sap//srvd/sap//0001/`, the second + segment is the binding's service name (alias), not necessarily the + service definition name. It can be renamed only while the binding is + unpublished** [source: sap-docs community-14433266 "How to rename an + OData V4 service?", verified 2026-09-16]. Check it before publishing. +- [2026-09-16] **CDS CASE results take the longest branch type, and untyped + integer literals are typed INT1..INT8 by value** [source: abap-docs + /abap-docs-cloud/ABENCDS_CASE_EXPRESSION_V2, ABENCDS_UNTYPED_LITERAL_V2, + verified 2026-09-16]. A text CASE over status codes becomes CHAR(n) of + the longest literal, and a 0–5 criticality becomes INT1. Cast + explicitly when the OData type matters. +- [2026-09-16] **The `ui5-mcp-server` linter cannot see this repo.** The + server is reached over SSE on `mcp.alexzaw.dev`, and `run_ui5_linter` on + `app/` fails with `ENOENT: realpath '/home/alexz/…/arena-plm/app'` + (live 2026-09-16). Its doc/API tools work; its file-based tools + (linter, manifest validation, project info) do not. For real UI5 code + changes, lint locally (`npx @ui5/linter` inside `app/`) or run the + server where the files are. +- [2026-09-16] **The Fiori mock server does not simulate bound actions.** + Without a per-entity `webapp/localService/mainService/data/.js` + exporting `executeAction`, `@sap-ux/fe-mockserver-core` 1.7.15 returns + the action data unchanged + (`node_modules/@sap-ux/fe-mockserver-core/dist/mockdata/fileBasedMockData.js:547-549`, + read locally 2026-09-16). Status transitions can be shown only against + the live RAP service, or with a hand-written mock handler. +- [2026-09-16] **Cast the whole CASE to a data element, never each + branch.** "If a data element is specified for `dtype`, the result … + inherits the semantic properties of the data element. An exception to + this is the use of the `CAST` expression within a case distinction." + Write `cast( case … end as abap_boolean preserving type )`. + `PRESERVING TYPE` requires an exact match of built-in type and length + [source: abap-docs /abap-docs-cloud/ABENCDS_CAST_EXPRESSION_V2, verified + 2026-09-16]. To guarantee that length, use typed literals in the branches + (`abap.char'X'`, `abap.char' '` are CHAR(1)): an untyped character + literal's length is only "at least as large as the actual length of the + literal" [source: abap-docs /abap-docs-cloud/ABENCDS_TYPED_LITERAL_V2, + ABENCDS_UNTYPED_LITERAL_V2, verified 2026-09-16]. `ABAP_BOOLEAN` is CHAR 1 + [source: abap-docs /abap-docs-standard/ABENDATAOBJECTS_TRUE_VALUE_GUIDL, + verified 2026-09-16]. The same caveat means a CASE over untyped status + literals is not provably CHAR(n) of the longest literal. +- [2026-09-16] **`sap-released-objects` has no `SRVB` object type for + public_cloud**, the same gap as `DDLS` (use `CDS_STOB` for views). Binding + release questions cannot be answered from that server. + - [2026-09-15] **Developer business catalogs: `SAP_A4C_BC_*` is the Public Cloud / BTP ABAP family; `SAP_A4_BC_*` is on-premise / Private Cloud.** On `my431353` the developer role carries the A4C catalogs (e.g. diff --git a/PRD.md b/PRD.md index 19339f6..09cab02 100644 --- a/PRD.md +++ b/PRD.md @@ -65,8 +65,8 @@ tombstone remains valid. | BOM (read) | V2 | `/sap/opu/odata/sap/API_BILL_OF_MATERIAL_SRV` | v1 only; v2 rejects guid keys | | Change Master | V2 | `/sap/opu/odata/sap/API_CHANGEMASTER;v=0002` | object-type assignments, material linking | | Attachment | V2 | `/sap/opu/odata/sap/API_CV_ATTACHMENT_SRV` | function import for URL creation | -| Approval BO — UI binding (Fiori app) | V4 UI | `/sap/opu/odata4/sap/zzui_arena_change_apprvl000_o4/srvd/sap/zzui_arena_change_apprvl000_o4/0001/` (binding `ZZUI_ARENA_CHANGE_APPRVL000_O4`, generated 2026-09-15, unpublished) | expected namespace `com.sap.gateway.srvd.zzui_arena_change_apprvl000_o4.v0001`, alias `SAP__self`, entity `ChangeApproval_Type`/set `ChangeApproval`; confirm from live `$metadata` after publish (V-2); Fiori app still carries the pre-generation provisional name | -| Approval BO — Web API (A2X) binding (worker) | V4 Web API | binding `ZAPI_ARENA_CHANGE_APPROVAL_A2X` on definition `ZAPI_ARENA_CHANGE_APPROVAL` (pending: created after the A2X projection is active) | custom communication scenario, technical user; namespace `com.sap.gateway.srvd_a2x.zapi_arena_change_approval_a2x.v0001` expected; exposes create + read + `markExecuting` / `markSynced(SapChangeNumber)` / `markFailed(ErrorMessage)` (RAP-6) | +| Approval BO — UI binding (Fiori app) | V4 UI | `/sap/opu/odata4/sap/zui_arena_change_appr_o4/srvd/sap/zui_arena_change_approval/0001/` (binding `ZUI_ARENA_CHANGE_APPR_O4` on definition `ZUI_ARENA_CHANGE_APPROVAL`; designed 2026-09-16, not yet created) | entity set `ChangeApproval` / type `ChangeApproval_Type`, actions `approve`, `reject(RejectReason)`, `retrySync`; namespace, EDM types and the URL's service alias segment are confirmed from live `$metadata` after publish (V-2). The Fiori app still points at `zui_arena_change_approval_o4`, and V-2 reconciliation moves it | +| Approval BO — Web API (A2X) binding (worker) | V4 Web API | binding `ZAPI_ARENA_CHANGE_APPR_A2X` on definition `ZAPI_ARENA_CHANGE_APPROVAL` (designed 2026-09-16; created after the A2X projection is active) | custom communication scenario, technical user; namespace from live `$metadata`; exposes create + read + `markExecuting` / `markSynced(SapChangeNumber)` / `markFailed(ErrorMessage)` (RAP-6) | | Worker inbound surface | REST (OpenAPI 3.1) | `docs/api/worker.openapi.json` | webhook intake (503 when buffer full), health (`ok`/`degraded`, session `none/active/expired/refreshing`), sync-run admin (strict request schema); contract v0.2.0 (2026-09-14 wave); auth scheme pending OQ-6 (ARN-009) | ## Requirements @@ -76,7 +76,7 @@ Capability level — field mappings deferred to the POC per my POC-before-spec r | ID | Requirement | Status | |---|---|---| | ARN-001 | Sync chain PRODUCT → CHANGE_MASTER → BOM → DOCUMENTS in config-driven order, executed by the worker on approval | Planned | -| ARN-002 | Approval domain as an on-stack RAP BO: staging fields, state machine PENDING → APPROVED → EXECUTING → SYNCED \| FAILED, REJECTED terminal, FAILED → retrySync → EXECUTING (amended 2026-09-14: a terminal success state is required so an approved record is never executed twice); approve/reject/retrySync as RAP actions with instance feature control; audit fields (`RequestedBy`/`RequestedOn`/`DecidedBy`/`DecidedOn`); execution failure is a distinct state, never overloaded onto rejection (predecessor defect, `docs/predecessor-flow.md` §2) | Planned | +| ARN-002 | Approval domain as an on-stack RAP BO: staging fields, state machine PENDING → APPROVED → EXECUTING → SYNCED \| FAILED, REJECTED terminal, FAILED → retrySync → APPROVED (amended 2026-09-14: a terminal success state is required so an approved record is never executed twice; amended 2026-09-16: retry returns the record to APPROVED so the worker re-claims it through `markExecuting`, the only way into EXECUTING. A retry that jumped straight to EXECUTING was never picked up, or it risked a second concurrent execution); duplicate staging of the same `ItemNumber`+`ChangeType`+`ArenaEcoId` is refused on create while a non-REJECTED record exists (2026-09-16); approve/reject/retrySync as RAP actions with instance feature control; audit fields (`RequestedBy`/`RequestedOn`/`DecidedBy`/`DecidedOn`); execution failure is a distinct state, never overloaded onto rejection (predecessor defect, `docs/predecessor-flow.md` §2) | Planned | | ARN-003 | Durable persistence & audit — satisfied structurally by in-tenant RAP persistence (the predecessor lost its queue on every restart) | Planned (by design) | | ARN-004 | Business mappings maintained in-tenant — evaluate Business Configuration maintenance objects (Custom Business Configurations app) before any file-based config | Planned | | ARN-005 | Arena ingestion via webhook + delta poll in the connectivity worker | Planned | @@ -94,10 +94,10 @@ once V-1 opens the development tenant. | ID | Requirement | Source | |---|---|---| | RAP-1 | Behavior definition declares `approve`, `reject`, `retrySync` with `( features : instance )` and implements `get_instance_features` so the UI service emits `Core.OperationAvailable`; approve/reject only for PENDING, retrySync only for FAILED. `approve` writes no `SapChangeNumber` (execution does). The CDS emits numeric `StatusCriticality` 2/2/5/3/1/1 for PENDING/APPROVED/EXECUTING/SYNCED/REJECTED/FAILED (5 = Information is V4-only) | [source: abap-docs /abap-docs-standard/ABENBDL_ACTIONS_FC; sap-docs /abap-fiori-showcase/01_general_features#operations-and-actions, verified 2026-09-14] | -| RAP-2 | `reject` takes a mandatory `RejectReason` (char 255) parameter; `approve`/`retrySync` return `$self`; the entity is read-only from the UI (no create/update/delete) | UI contract (annotation.xml, mock metadata) | +| RAP-2 | `reject` takes a mandatory `RejectReason` (char 255) parameter; `approve`/`retrySync` return `$self`; the entity is read-only from the UI (no create/update/delete). **Partly met for the POC (2026-09-16):** the handler refuses an empty reason server-side. A metadata-level mandatory marker needs a root abstract entity, an abstract BDEF with `field ( mandatory : execute )` and a `deep parameter`, and is deferred until after the UI POC [source: abap-docs /abap-fiori-showcase/01_general_features #MandatoryParameter; /abap-docs-cloud/ABENBDL_ACTION_INPUT_PARAM, verified 2026-09-16 by my review] | UI contract (annotation.xml, mock metadata) | | RAP-3 | `Status` carries a text element (`@ObjectModel.text.element`, `@UI.textArrangement: #TEXT_ONLY`) so the UI shows text, not codes | [source: fiori-mcp-server "Text and Text Arrangement", verified 2026-09-14] | | RAP-4 | Semantic key `ItemNumber` (`@ObjectModel.semanticKey`); `Status` and `RequestedOn` sortable/filterable | UI default variant | -| RAP-5 | Two service bindings on definition `ZUI_ARENA_CHANGE_APPROVAL`: OData V4 **UI** binding `ZUI_ARENA_CHANGE_APPROVAL_O4` for the app and an OData V4 **Web API** binding for the worker; the binding name sets the URL first segment and the namespace; hand back the live `$metadata` of the UI binding to reconcile (V-2) | [source: sap-docs sap-help loio 5717c0a1dafc4335add0207b81c39cef, 5f45f125dfe84c20beb18f5de1512f5a, verified 2026-09-14] | +| RAP-5 | (Amended 2026-09-16.) Two service definitions, one binding each. `ZUI_ARENA_CHANGE_APPROVAL` exposes the UI projection as `ChangeApproval` through the OData V4 **UI** binding `ZUI_ARENA_CHANGE_APPR_O4`. `ZAPI_ARENA_CHANGE_APPROVAL` exposes the A2X projection through the OData V4 **Web API** binding `ZAPI_ARENA_CHANGE_APPR_A2X`. Binding names are 26 characters or fewer. The binding name sets the URL's first segment; hand back the UI binding's live `$metadata` to reconcile (V-2). The single-definition wording of 2026-09-14 could not hold, because a projection BDEF exposes only what it lists | [source: sap-docs sap-help loio 5717c0a1dafc4335add0207b81c39cef, 5f45f125dfe84c20beb18f5de1512f5a, verified 2026-09-14; binding-name limit: sap-docs sap-help loio 4bad0d7280b742a68dd2d42b1441f09a, verified 2026-09-16; abap-docs /abap-docs-cloud/ABENBDL_USE_PROJECTION, verified 2026-09-15] | | RAP-6 | Worker-callable execution-state actions on the A2X binding — `markExecuting`, `markSynced` (sets `SapChangeNumber`), `markFailed` (sets `ErrorMessage`) — with instance feature control so the UI never offers Approve on an executing record | [source: abap-docs /abap-docs-cloud/ABENBDL_ACTIONS_FC, verified 2026-09-14] | | RAP-7 | Staged payload (`SapPreview`) is `abap.string` — CDS char elements are capped at 1333 characters and truncation would break ARN-010 | [source: abap-docs /abap-docs-cloud/ABENCDS_SELECT_LIST_ENTRY_V2, verified 2026-09-14] | | RAP-8 | Field `DecidedBy` (not `ApprovedBy`) holds approver or rejecter; semantic key `ItemNumber, ChangeType, ArenaEcoId` | UI contract | @@ -112,11 +112,15 @@ once V-1 opens the development tenant. | ID | Item | Status | |---|---|---| | V-1 | Developer-extensibility access on the development tenant | **Closed 2026-09-15** — `my431353.s4hana.cloud.sap`: business user AZAW holds `ZBR_DEVELOPER` (SAP_BR_DEVELOPER template), `ZBR_ADMINISTRATOR_SW_DEV`, `ZBR_DEVELOPMENT_MANAGER_DIN` and five more (live: `sap-api users getRoles`, verified 2026-09-15). V-1b done 2026-09-15 (destination `0M7DEJI`, MCP lists it). Role correct (A4C catalogs). V-1b **closed 2026-09-15**: package `ZARENA_PLM` under `ZLOCAL`; probe class created and activated via ADT MCP | -| V-2 | Custom communication scenario can expose the approval BO's A2X service to the worker's technical user; and the Fiori app's annotation namespace, `_Type` entity-type suffix and UI-binding URL segment are reconciled from the live `$metadata` of the UI binding (decided 2026-09-14; the mock cannot verify this) | Open | +| V-2 | Custom communication scenario can expose the approval BO's A2X service to the worker's technical user; and the Fiori app's annotation namespace, `_Type` entity-type suffix and UI-binding URL segment are reconciled from the live `$metadata` of the UI binding (decided 2026-09-14; the mock cannot verify this) | Open. **UI half in progress 2026-09-16:** live `$metadata` is captured in `docs/v2/zui_arena_change_appr_o4.metadata.xml`, and the UI host serves it to the developer user. The contract holds: all 17 property types, key, entity set, the three actions, and `RejectReason` non-nullable. Identity differs from the mock: namespace `com.sap.gateway.srvd.zui_arena_change_approval.v0001`, entity type `ChangeApprovalType`, URL segment `zui_arena_change_appr_o4`, and backend operation control via `__OperationControl`. **The app was reconciled 2026-09-16:** the dataSource targets `zui_arena_change_appr_o4`; the mock serves the live `$metadata` byte-for-byte; annotation targets use the live namespace and entity type; enablement is backend-owned; no old identity strings remain; lint, typecheck and `@ui5/linter` are clean; the mock server serves the service. Unverified until a live preview: the `DataFieldForAction` action-string form versus the backend's overload-specific `OperationAvailable` target. The A2X half (custom communication scenario, `-api` host) is still open. The service URL reported on `-api` carries `sap-client=080` | | V-3 | On-stack Application Jobs vs worker-side polling for the Arena delta | Open — flow findings 2026-09-10: Arena's outbound-integration queue holds the cursor (`reconciled` flag) and is the clean delta mechanism; `/changes`-feed polling needs client-side date filtering (`docs/predecessor-flow.md` §1) | | V-4 | Can a Claude Code agent outside the VS Code editor author ABAP source through the ADT MCP Server? | **Answered 2026-09-15: no.** `create_object` and the generators take structure, not source (live probe); SAP's editing MCP tool is planned for Public Edition 2702 [source: sap-docs loio 7f716223a88c40e0992777c8d9febccf, verified 2026-09-15]. Build plan: `x-ui-service` generator for the BO skeleton, source edits handed over as files for the VS Code editor until 2702 | | OQ-5 | Long-term: move the sync executor on-stack (released ABAP APIs) vs keep it in the worker — worker initially | Open | | OQ-6 | Webhook receiver hosting and auth for the worker on CF | Open | +| OQ-9 | Agent-side ABAP source read/write through the community `abap-adt-mcp` server (npm 2.1.0, a single maintainer, published 2026-09-16 01:04Z, SLSA provenance attested). It lists `getObjectSource`, `setObjectSource` and `activateObjects` next to 170 other tools, enforces `readOnly` / `allowedPackages` policy server-side, and signs in to Public Cloud through a one-time browser SSO. It would remove the manual paste step that SAP's ADT MCP forces (V-4). Checked 2026-09-16: a read-only smoke run with no login and no SAP call listed 173 tools and enforced a `ZARENA_PLM`-only read-only policy; nothing is registered. Adoption needs, in order: a source review of the package, because it would hold an SSO session into the tenant; a version pin; the development tenant only, never `my429480`; `allowedPackages: ["ZARENA_PLM"]`; read-only first; then a CLAUDE.md routing change | Open (2026-09-16), not blocking. I want it as the replacement for manual pasting; the read-only source review of the cached package (03:15) cleared every check it ran, observed as it ran. It stalled while collecting line citations for its report and was stopped at ~03:28, so no written report with file:line evidence exists. I accept it for the development tenant under the read-only-first configuration. Registered by hand in project scope at 03:37: stdio `npx -y abap-adt-mcp@2.1.0`, `MCP_READ_ONLY=1`, audit file on, `~/.abap-adt-mcp/systems.json` (development tenant `my431353`, client 100, SSO, `readOnly: true`, `allowedPackages: ["ZARENA_PLM"]`, mode 600). The other six project servers are intact. It connected after the restart, and its policy checked out live. **Parked 2026-09-16 03:49:** SSO needs an interactive sign-in inside the server's own Chrome, on the server's desktop (`:0`). That window is unreachable over my multi-hop SSH remote access; a DevTools relay was tried and dropped. The server stays registered but unused. It is revisited when a session at the server's desktop can complete one sign-in with "stay signed in", after which logins run unattended. The reentrance-ticket path was built and tested on 2026-09-18 (`tools/adt-reentrance-login/`) and **rejected on evidence**: the tenant ignores the ticket as both a `MYSAPSSO2` cookie and header, answering with its SAML redirect page (KNOWLEDGE 2026-09-18). Reviving OQ-9 requires capturing how ADT for VS Code exchanges the ticket. Until then, tenant work stays on SAP's ADT MCP plus editor paste. Until then, the UI leg continues with SAP's `adt-mcp` plus editor paste. The registration is run by hand, because Claude Code's permission guard refuses agent-side changes to its own MCP configuration (03:17, nothing written). The target is the development tenant, client 100, SSO login, first with `readOnly: true` + `allowedPackages: ["ZARENA_PLM"]` + `MCP_READ_ONLY=1`, then write enabled for `ZARENA_PLM` only | +| OQ-10 | Ownership of UI annotations: the backend metadata extension `ZC_CHANGEAPPROVAL` or the app's local `annotation.xml`. Today 35 of the 58 local annotations override a backend annotation with the same target, term and qualifier (layering rule [source: fiori-mcp-server "Visualizing and Managing Annotations with SAP Fiori Tools - Service Modeler", verified 2026-09-16]), so a DDLX change to those terms is invisible in the app. The local file adds i18n labels, action criticality, the `#Default` variants and the facet IDs the OPA journey relies on | Open (2026-09-16). Interim rule: a term defined locally is changed locally. I consolidate before launchpad deployment | +| OQ-8 | Per-operation authorization for the approval BO. Today it is `authorization master ( none )`: any holder of the UI app can approve, reject and retry, and any tenant ABAP code can create and mark* through EML. SAP Help calls `( none )` "not recommended in real-world scenarios". The target is `( global )` with a `FOR GLOBAL AUTHORIZATION` handler and `AUTHORITY-CHECK OBJECT` on a custom authorization object: decisions for business roles, create/mark* for the communication role. First verify that custom authorization objects can be created on `my431353` [source: abap-docs /abap-docs-cloud/ABENBDL_AUTHORIZATION, ABAPHANDLER_METH_GLOBAL_AUTH; sap-docs sap-help loio 673c05562e144702a080c574c3da2da3, verified 2026-09-16 by my review] | Open (2026-09-16): does not block the dev-tenant UI POC; must close before any business role other than the developer's gets the app (IAM brief) | +| OQ-7 | Tenant object naming and cleanup for the approval BO | **Closed 2026-09-16.** The package was deleted in ADT, contents and package itself (verified live through ADT MCP validation, 2026-09-16). I build from scratch, without the generator: table `ZCHANGEAPPROVAL`, `ZR_CHANGEAPPROVAL`, `ZBP_R_CHANGEAPPROVAL`, `ZC_CHANGEAPPROVAL`, SRVD `ZUI_ARENA_CHANGE_APPROVAL` with SRVB `ZUI_ARENA_CHANGE_APPR_O4`, and SRVD `ZAPI_ARENA_CHANGE_APPROVAL` with SRVB `ZAPI_ARENA_CHANGE_APPR_A2X`. Binding names follow SAP's 26-character limit (RAP-5). Entity set is `ChangeApproval`; the A2X projection and parameter entity names are unchanged | ## Status @@ -128,6 +132,6 @@ once V-1 opens the development tenant. | Specialist agent roster (5 agents, docs/agent-roster.md) | Done (2026-09-04; all 5 files YAML-verified in .claude/agents/) | | V-1…V-3 verification POC | Planned — V-1 (ADT access on my429480) is the user-side gate for everything on-stack | | Fiori Elements app (`app/`, mock-bound) | Done for the mock stage (2026-09-14) — gateway service shape (binding-derived identity), six-state domain, polished List Report/Object Page, launchpad inbound `ArenaChangeApproval-manage`; SAP review findings closed. Deploy gated on V-1/V-2 (live `$metadata` reconciliation) | -| RAP BO (approval domain) | In progress — skeleton generated and active in `ZARENA_PLM` (2026-09-15, ATC clean); **waiting on owner: paste `docs/handover/rap/` sources in VS Code per its README**, then I activate/ATC/create the A2X BDEF + binding; IAM app/catalog and publishing are the next brief | +| RAP BO (approval domain) | In progress (2026-09-16): from-scratch build in package `ZARENA_PLM` (under `ZLOCAL`, no transport recording) under the clean names (OQ-7 closed). The handover in `docs/handover/rap/` has all pre-paste review fixes applied and was stable as of 02:45. Wave A (U1–U5: table, three parameter entities, root view) was created empty at 02:30. Its sources were pasted at 02:47, and all five activated at 02:49 with no errors, `preserving type` included. Wave B (U6 base BDEF, U10 root access control) was created empty at 02:50, its sources were pasted at 02:53, and both activated at 02:55 (BDEF through `.bdef.abdl`). Wave C (U7 behavior pool, U8 UI projection view) was pasted at ~03:00 and activated at 03:03 (U6 and U7 together, then U8), with no errors. It is not yet confirmed that the handler source sits in the class's Local Types include; until it is, activation does not prove the three flagged constructs. Wave D (U9 projection BDEF, U11 access control, U12 metadata extension, U13 UI service definition) was created empty at 03:05 and pasted at ~04:12. U9, U11 and U12 activated at 04:15. U13 activation failed, listing "Comments are not supported and will be deleted on save" (Ln 1) and "oData V4 services with no draft capability will be primarily READ-ONLY" (Ln 21, expected by design). After a comment-free re-save, U13 activated at 04:18. U14 binding `ZUI_ARENA_CHANGE_APPR_O4` (OData V4 UI) was created and activated. **The UI leg is active: all 14 objects; ATC 0 errors, 0 warnings, 43 infos.** **Published 2026-09-16 04:26** through the ADT Publish code lens: `https://my431353-api.s4hana.cloud.sap/sap/opu/odata4/sap/zui_arena_change_appr_o4/srvd/sap/zui_arena_change_approval/0001/?sap-client=080`, entity set `ChangeApproval` (`fetch_service_information`). The table is empty until the worker leg creates records, because the UI exposes no create. V-2 app reconciliation done. **Worker (A2X) leg ACTIVE 2026-09-16 06:37:** W1 view `ZC_CHANGEAPPROVAL_A2X`, W2 access control, W3 projection BDEF (create, markExecuting, markSynced, markFailed), W4 service definition `ZAPI_ARENA_CHANGE_APPROVAL` and W5 OData V4 Web API binding `ZAPI_ARENA_CHANGE_APPR_A2X` (unpublished, service `0001`). ATC on the 5 worker objects: 0 errors, 1 priority-3 warning (W607 on `ZC_CHANGEAPPROVAL_A2X`: `authorizationCheck: #CHECK` should be `#MANDATORY`), deferred to OQ-8. **The approval BO is complete on the development tenant (19 objects).** Next: expose W5 to the worker's communication user through a custom communication scenario (V-2 A2X half); IAM app, business catalog and role for the UI app (ARN-007); OQ-8 authorization before other business users. The table source goes into `.tabl.ddic`; `.tabl.json` holds only header metadata. (3) UI leg, 14 sources ending with binding `ZUI_ARENA_CHANGE_APPR_O4`: I create each object through the MCP, its source is pasted in the VS Code editor, and I activate it. The `markSynced`/`markFailed` parameter entities belong to this leg, because the base BDEF references them. (4) Publish the UI binding, then run V-2 reconciliation of the app against its live `$metadata`. (5) Worker (A2X) leg, 4 sources. Syntax proven only by tenant activation, still open: `OPTIONS PRIVILEGED ACCESS` in `checkDuplicate` (fallback `FROM … WITH PRIVILEGED ACCESS`), `%op-%action` on MODIFY response rows, `preserving type` on the U5 boolean casts | | Connectivity worker | Review wave Done (2026-09-14) — TTL wiring, cursor-after-success, buffer-full = tick failure, health conformance, strict JSON-only validation, bounded buffer, graceful drain, HTTP timeout, fixed-phrase errors; 137 tests green incl. the schema-driven contract-conformance suite. Next: Arena client POC (live), executor leg gated on V-2. **Do not deploy the scaffold**: the ingest buffer has no consumer yet, so a long-running instance would answer 503 after `WEBHOOK_QUEUE_MAX_SIZE` events. Next: Arena client POC (live), executor leg gated on V-2 | | Worker API contract (`docs/api/worker.openapi.json`) | v0.2.1 Done (2026-09-14) — health enums/`degraded`/`poller`, strict `SyncRunRequest`, webhook 503, 413/415 + `default` on every operation, reserved 502, `DOCUMENTS` token; `x-sap-stateInfo: beta`, provisional webhook schema pending Arena POC | diff --git a/app/README.md b/app/README.md index 95a4356..82e39cc 100644 --- a/app/README.md +++ b/app/README.md @@ -17,42 +17,55 @@ |**Add Eslint configuration**
True, see https://www.npmjs.com/package/@sap-ux/eslint-plugin-fiori-tools#rules for the eslint rules.| |**Main Entity**
ChangeApprovals| -## Service binding (provisional) +## Service binding (live, reconciled 2026-09-16 - PRD V-2) The app binds to the RAP **UI** service binding, not the A2X binding the worker uses. -The single source of truth for the service identity is the header comment of -`webapp/localService/mainService/metadata.xml`: +The single source of truth for the service identity is the live `$metadata` of the published +binding, captured 2026-09-16 as `docs/v2/zui_arena_change_appr_o4.metadata.xml` (repo root). +`webapp/localService/mainService/metadata.xml` is a **byte-identical copy** of it, so the mock +server serves the real contract. Refresh both together; never hand-edit the copy. | Item | Value | |---|---| -| Service binding (OData V4 - UI) | `ZUI_ARENA_CHANGE_APPROVAL_O4` | +| Service binding (OData V4 - UI) | `ZUI_ARENA_CHANGE_APPR_O4` | | Service definition | `ZUI_ARENA_CHANGE_APPROVAL` | -| Service URL | `/sap/opu/odata4/sap/zui_arena_change_approval_o4/srvd/sap/zui_arena_change_approval/0001/` | -| Schema namespace / alias | `com.sap.gateway.srvd.zui_arena_change_approval_o4.v0001` / `SAP__self` | -| Entity type / entity set | `ChangeApproval_Type` / `ChangeApproval` | +| Service URL | `/sap/opu/odata4/sap/zui_arena_change_appr_o4/srvd/sap/zui_arena_change_approval/0001/` | +| Schema namespace / alias | `com.sap.gateway.srvd.zui_arena_change_approval.v0001` / `SAP__self` | +| Entity type / entity set | `ChangeApprovalType` / `ChangeApproval` | +| Bound actions | `approve`, `reject(RejectReason)`, `retrySync` | -The first URL segment and the schema namespace are derived from the service **binding** name, -the segment after `srvd/sap/` from the service **definition** name - strongly indicated by the -tenant's `/sap/opu/odata4/sap/api_currencyexchangerate/srvd_a2x/sap/currencyexchangerate/0001` -(namespace `com.sap.gateway.srvd_a2x.api_currencyexchangerate.v0001`) and SAP Help's -`/sap/opu/odata4/sap/api_product/srvd_a2x/sap/product/0002` -[source: sap-docs sap-help loio 5f45f125dfe84c20beb18f5de1512f5a, verified 2026-09-14]. +Only the first URL segment comes from the service **binding** name. The schema namespace comes +from the service **definition** name, and the entity type carries a `Type` suffix without an +underscore (live `$metadata`, 2026-09-16; see `KNOWLEDGE.md`). The pre-V-2 assumption of a +binding-derived namespace and a `_Type` suffix was wrong. -`webapp/manifest.json` (`sap.app.dataSources.mainService.uri`, routing) and -`webapp/annotations/annotation.xml` (`edmx:Reference`, every `Annotations Target`, every -`DataFieldForAction.Action`) must stay in sync with it. JSON cannot carry comments, so the -manifest references this note instead. +`webapp/manifest.json` (`sap.app.dataSources.mainService.uri`), `ui5-mock.yaml` / +`ui5-local.yaml` (`sap-fe-mockserver` `urlPath`) and `webapp/annotations/annotation.xml` +(`edmx:Reference`, every `Annotations Target`, every `DataFieldForAction.Action`) must stay in +sync with it. JSON cannot carry comments, so the manifest references this note instead. -**Provisional - namespace = binding name is strongly indicated, not proven; reconcile namespace, -entity type suffix and URL segments against the live `$metadata` of the RAP UI service binding -at PRD V-2 before deploy.** +**Annotation layering.** The backend metadata extension already emits most UI annotations +(LineItem, HeaderInfo, Facets, FieldGroups, Identification, SelectionFields, SemanticKey, +`Status` text with `TextOnly`). An annotation in `annotation.xml` replaces the backend annotation +with the same term + qualifier on the same target, and deleting it there makes the backend one +effective again [source: fiori-mcp-server "Visualizing and Managing Annotations with SAP Fiori +Tools - Service Modeler", verified 2026-09-16]. + +**Backend for a non-mock run: not configured.** `ui5.yaml` and `ui5-local.yaml` carry +`fiori-tools-proxy` with `backend: []`, and `npm start` is a generator placeholder. When a live +preview is added, the backend is the **UI host** `https://my431353.s4hana.cloud.sap`, never the +`-api` host: a business-user call there is blocked by UCON (live 2026-09-16, `KNOWLEDGE.md`). ## Status model and actions `Status`: `PENDING -> APPROVED -> EXECUTING -> SYNCED | FAILED`; `REJECTED` is terminal; -`FAILED -> retrySync -> EXECUTING`. `approve`/`reject` are enabled only for `PENDING`, -`retrySync` only for `FAILED` (mocked via `IsApprovable` / `IsRetriable`, which stand in for the -`Core.OperationAvailable` RAP emits for `( features : instance )` actions - PRD RAP-1). +`FAILED -> retrySync -> APPROVED`. `approve`/`reject` are enabled only for `PENDING`, +`retrySync` only for `FAILED`. The backend owns enablement: RAP emits +`Core.OperationAvailable Path="_it/__OperationControl/"` for the `( features : instance )` +actions (PRD RAP-1), and the app keeps no local `OperationAvailable`, which would replace it +[source: fiori-mcp-server "Action Control for Context-Dependent Actions (SAP Fiori Elements)", +verified 2026-09-16]. `IsApprovable` / `IsRetriable` are still in the service but hidden, and +nothing in the app binds to them. Actions are offered on the object page header only; the list report table is display-only (`selectionMode: "None"`). @@ -68,7 +81,11 @@ required for a custom app (only `archeType` is mandatory) Dev server: `npm run start-mock` / `npm run int-test` bind to port **8082** (8080 is taken on the dev host). Mock data lives in `webapp/localService/mainService/data/ChangeApproval.json` -(`generateMockData: false`), 8 rows covering every status. +(`generateMockData: false`), 8 rows covering every status. Each row carries every live property: +`StatusText` / `StatusCriticality` as `ZR_CHANGEAPPROVAL` computes them, `__OperationControl` +consistent with the status, `LocalLastChangedAt`, and `SAP__Messages: []`. Initial CHAR values are +`""`, not `null`, on `Nullable="false"` properties. The mock server does not execute bound +actions (see `KNOWLEDGE.md`), so status transitions show only against the live service. ## app diff --git a/app/ui5-local.yaml b/app/ui5-local.yaml index 0b6e1d5..470c6cf 100644 --- a/app/ui5-local.yaml +++ b/app/ui5-local.yaml @@ -44,7 +44,7 @@ server: configuration: mountPath: / services: - - urlPath: /sap/opu/odata4/sap/zui_arena_change_approval_o4/srvd/sap/zui_arena_change_approval/0001/ + - urlPath: /sap/opu/odata4/sap/zui_arena_change_appr_o4/srvd/sap/zui_arena_change_approval/0001/ metadataPath: ./webapp/localService/mainService/metadata.xml mockdataPath: ./webapp/localService/mainService/data generateMockData: false diff --git a/app/ui5-mock.yaml b/app/ui5-mock.yaml index 4f7fd6c..76355db 100644 --- a/app/ui5-mock.yaml +++ b/app/ui5-mock.yaml @@ -44,7 +44,7 @@ server: configuration: mountPath: / services: - - urlPath: /sap/opu/odata4/sap/zui_arena_change_approval_o4/srvd/sap/zui_arena_change_approval/0001/ + - urlPath: /sap/opu/odata4/sap/zui_arena_change_appr_o4/srvd/sap/zui_arena_change_approval/0001/ metadataPath: ./webapp/localService/mainService/metadata.xml mockdataPath: ./webapp/localService/mainService/data generateMockData: false diff --git a/app/webapp/annotations/annotation.xml b/app/webapp/annotations/annotation.xml index 830ff28..f4c2292 100644 --- a/app/webapp/annotations/annotation.xml +++ b/app/webapp/annotations/annotation.xml @@ -1,12 +1,18 @@ @@ -15,8 +21,8 @@ - - + + @@ -29,92 +35,92 @@ "Enabling Table Personalization in SAP Fiori Elements (OData V2 & V4)"; "Filter Fields - Add, Configure, Move, Delete...", verified 2026-09-14] ============================================================================ --> - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + @@ -255,28 +261,30 @@ + Enablement is owned by the backend: RAP emits Core.OperationAvailable + Path="_it/__OperationControl/" for the ( features : instance ) actions (live $metadata + 2026-09-16). Deliberately NO OperationAvailable in this file - a local one would replace the + backend annotation on the same target. + [source: fiori-mcp-server "Action Control for Context-Dependent Actions (SAP Fiori Elements)"; + "Enable Header Actions on the Object Page (UI.Identification / DataFieldForAction)"; + "Visualizing and Managing Annotations with SAP Fiori Tools - Service Modeler", verified 2026-09-16] --> - + - + - + diff --git a/app/webapp/i18n/i18n.properties b/app/webapp/i18n/i18n.properties index 61f3c01..0a1b599 100644 --- a/app/webapp/i18n/i18n.properties +++ b/app/webapp/i18n/i18n.properties @@ -26,7 +26,7 @@ CHANGE_APPROVALS_TITLE=Change Approvals ACTION_APPROVE=Approve #XBUT: Rejects the staged change; asks for a reason ACTION_REJECT=Reject -#XBUT: Re-runs a FAILED SAP sync (FAILED -> EXECUTING) +#XBUT: Re-runs a FAILED SAP sync (FAILED -> APPROVED) ACTION_RETRY=Retry Sync #XFLD: Arena item number (e.g. MC-100-0042) diff --git a/app/webapp/i18n/i18n_en.properties b/app/webapp/i18n/i18n_en.properties index 25216b7..61d7ed5 100644 --- a/app/webapp/i18n/i18n_en.properties +++ b/app/webapp/i18n/i18n_en.properties @@ -26,7 +26,7 @@ CHANGE_APPROVALS_TITLE=Change Approvals ACTION_APPROVE=Approve #XBUT: Rejects the staged change; asks for a reason ACTION_REJECT=Reject -#XBUT: Re-runs a FAILED SAP sync (FAILED -> EXECUTING) +#XBUT: Re-runs a FAILED SAP sync (FAILED -> APPROVED) ACTION_RETRY=Retry Sync #XFLD: Arena item number (e.g. MC-100-0042) diff --git a/app/webapp/localService/mainService/data/ChangeApproval.json b/app/webapp/localService/mainService/data/ChangeApproval.json index ecc638c..de5776f 100644 --- a/app/webapp/localService/mainService/data/ChangeApproval.json +++ b/app/webapp/localService/mainService/data/ChangeApproval.json @@ -4,151 +4,215 @@ "ItemNumber": "MC-100-0042", "ItemName": "RF Amplifier Module, 2-18 GHz", "ChangeType": "PRODUCT", - "Status": "PENDING", - "StatusCriticality": 2, "ArenaEcoId": "ECO-2026-0113", + "Status": "PENDING", + "StatusText": "Pending approval", + "StatusCriticality": 2, "RequestedBy": "AZAW", "RequestedOn": "2026-09-12T14:05:00Z", - "DecidedBy": null, + "DecidedBy": "", "DecidedOn": null, - "SapChangeNumber": null, - "ErrorMessage": null, - "RejectReason": null, + "SapChangeNumber": "", + "ErrorMessage": "", + "RejectReason": "", "SapPreview": "{\"Product\":\"MC-100-0042\",\"ProductType\":\"FERT\",\"BaseUnit\":\"PC\",\"IndustrySector\":\"M\",\"GrossWeight\":\"0.412\",\"WeightUnit\":\"KG\",\"ProductGroup\":\"RFAMP\",\"CrossPlantStatus\":\"\",\"_ProductDescription\":[{\"Language\":\"EN\",\"ProductDescription\":\"RF Amplifier Module, 2-18 GHz\"}],\"_ProductPlant\":[{\"Plant\":\"1710\",\"AvailabilityCheckType\":\"02\",\"ProfitCenter\":\"YB700\",\"MRPType\":\"PD\",\"ProcurementType\":\"E\"},{\"Plant\":\"1720\",\"AvailabilityCheckType\":\"02\",\"ProfitCenter\":\"YB700\",\"MRPType\":\"PD\",\"ProcurementType\":\"F\"}],\"_ProductSalesDelivery\":[{\"ProductSalesOrg\":\"1710\",\"ProductDistributionChnl\":\"10\",\"DeliveryQuantityUnit\":\"PC\",\"ItemCategoryGroup\":\"NORM\"}],\"_ProductStorage\":[{\"Plant\":\"1710\",\"StorageLocation\":\"171A\"}],\"_ProductValuation\":[{\"ValuationArea\":\"1710\",\"ValuationClass\":\"7920\",\"PriceControl\":\"S\",\"StandardPrice\":\"245.00\",\"Currency\":\"USD\"}]}", "IsApprovable": true, - "IsRetriable": false + "IsRetriable": false, + "LocalLastChangedAt": "2026-09-12T14:05:00Z", + "__OperationControl": { + "approve": true, + "reject": true, + "retrySync": false + }, + "SAP__Messages": [] }, { "ID": "6f1c2a3e-0002-4a6b-9c1d-000000000002", "ItemNumber": "MC-100-0042", "ItemName": "RF Amplifier Module, 2-18 GHz", "ChangeType": "CHANGE_MASTER", - "Status": "PENDING", - "StatusCriticality": 2, "ArenaEcoId": "ECO-2026-0113", + "Status": "PENDING", + "StatusText": "Pending approval", + "StatusCriticality": 2, "RequestedBy": "AZAW", "RequestedOn": "2026-09-12T14:05:04Z", - "DecidedBy": null, + "DecidedBy": "", "DecidedOn": null, - "SapChangeNumber": null, - "ErrorMessage": null, - "RejectReason": null, + "SapChangeNumber": "", + "ErrorMessage": "", + "RejectReason": "", "SapPreview": "{\"ChangeNumberDescription\":\"ECO: Rev C amplifier bias network update\",\"ChangeNumberValidFromDate\":\"2026-09-12T00:00:00Z\",\"ReasonForChangeText\":\"ARENA ECO-2026-0113\",\"ChangeNumberStatus\":\"01\"}", "IsApprovable": true, - "IsRetriable": false + "IsRetriable": false, + "LocalLastChangedAt": "2026-09-12T14:05:04Z", + "__OperationControl": { + "approve": true, + "reject": true, + "retrySync": false + }, + "SAP__Messages": [] }, { "ID": "6f1c2a3e-0003-4a6b-9c1d-000000000003", "ItemNumber": "MC-200-0117", "ItemName": "Bias Tee Assembly, SMA", "ChangeType": "BOM", - "Status": "PENDING", - "StatusCriticality": 2, "ArenaEcoId": "ECO-2026-0118", + "Status": "PENDING", + "StatusText": "Pending approval", + "StatusCriticality": 2, "RequestedBy": "JDOE", "RequestedOn": "2026-09-13T09:41:00Z", - "DecidedBy": null, + "DecidedBy": "", "DecidedOn": null, - "SapChangeNumber": null, - "ErrorMessage": null, - "RejectReason": null, + "SapChangeNumber": "", + "ErrorMessage": "", + "RejectReason": "", "SapPreview": "{\"Material\":\"MC-200-0117\",\"Plant\":\"1710\",\"BillOfMaterialCategory\":\"M\",\"BillOfMaterialVariantUsage\":\"1\",\"HeaderValidityStartDate\":\"2026-09-13T00:00:00Z\",\"ToBOMItem\":[{\"BillOfMaterialItemNumber\":\"0010\",\"BillOfMaterialComponent\":\"MC-500-0031\",\"BillOfMaterialItemQuantity\":\"2\",\"BillOfMaterialItemCategory\":\"L\"},{\"BillOfMaterialItemNumber\":\"0020\",\"BillOfMaterialComponent\":\"MC-500-0088\",\"BillOfMaterialItemQuantity\":\"1\",\"BillOfMaterialItemCategory\":\"L\"}]}", "IsApprovable": true, - "IsRetriable": false + "IsRetriable": false, + "LocalLastChangedAt": "2026-09-13T09:41:00Z", + "__OperationControl": { + "approve": true, + "reject": true, + "retrySync": false + }, + "SAP__Messages": [] }, { "ID": "6f1c2a3e-0004-4a6b-9c1d-000000000004", "ItemNumber": "MC-300-0009", "ItemName": "Low Noise Amplifier, 0.5-4 GHz", "ChangeType": "PRODUCT", - "Status": "APPROVED", - "StatusCriticality": 2, "ArenaEcoId": "ECO-2026-0097", + "Status": "APPROVED", + "StatusText": "Approved - awaiting execution", + "StatusCriticality": 2, "RequestedBy": "AZAW", "RequestedOn": "2026-09-08T16:22:00Z", "DecidedBy": "MSMITH", "DecidedOn": "2026-09-09T08:10:00Z", - "SapChangeNumber": null, - "ErrorMessage": null, - "RejectReason": null, + "SapChangeNumber": "", + "ErrorMessage": "", + "RejectReason": "", "SapPreview": "{\"Product\":\"MC-300-0009\",\"ProductType\":\"FERT\",\"BaseUnit\":\"PC\",\"ProductGroup\":\"LNA\",\"_ProductDescription\":[{\"Language\":\"EN\",\"ProductDescription\":\"Low Noise Amplifier, 0.5-4 GHz\"}]}", "IsApprovable": false, - "IsRetriable": false + "IsRetriable": false, + "LocalLastChangedAt": "2026-09-09T08:10:00Z", + "__OperationControl": { + "approve": false, + "reject": false, + "retrySync": false + }, + "SAP__Messages": [] }, { "ID": "6f1c2a3e-0007-4a6b-9c1d-000000000007", "ItemNumber": "MC-300-0009", "ItemName": "Low Noise Amplifier, 0.5-4 GHz", "ChangeType": "CHANGE_MASTER", - "Status": "EXECUTING", "ArenaEcoId": "ECO-2026-0097", + "Status": "EXECUTING", + "StatusText": "Executing in SAP", + "StatusCriticality": 5, "RequestedBy": "AZAW", "RequestedOn": "2026-09-08T16:22:03Z", "DecidedBy": "MSMITH", "DecidedOn": "2026-09-09T08:10:00Z", - "SapChangeNumber": null, - "ErrorMessage": null, - "RejectReason": null, + "SapChangeNumber": "", + "ErrorMessage": "", + "RejectReason": "", "SapPreview": "{\"ChangeNumberDescription\":\"ECO: LNA input match rework\",\"ChangeNumberValidFromDate\":\"2026-09-09T00:00:00Z\",\"ReasonForChangeText\":\"ARENA ECO-2026-0097\",\"ChangeNumberStatus\":\"01\"}", - "StatusCriticality": 5, "IsApprovable": false, - "IsRetriable": false + "IsRetriable": false, + "LocalLastChangedAt": "2026-09-09T08:10:00Z", + "__OperationControl": { + "approve": false, + "reject": false, + "retrySync": false + }, + "SAP__Messages": [] }, { "ID": "6f1c2a3e-0008-4a6b-9c1d-000000000008", "ItemNumber": "MC-150-0301", "ItemName": "Directional Coupler, 10 dB", "ChangeType": "CHANGE_MASTER", - "Status": "SYNCED", "ArenaEcoId": "ECO-2026-0088", + "Status": "SYNCED", + "StatusText": "Synced to SAP", + "StatusCriticality": 3, "RequestedBy": "JDOE", "RequestedOn": "2026-09-05T10:00:00Z", "DecidedBy": "MSMITH", "DecidedOn": "2026-09-05T11:30:00Z", "SapChangeNumber": "500000012301", - "ErrorMessage": null, - "RejectReason": null, + "ErrorMessage": "", + "RejectReason": "", "SapPreview": "{\"ChangeNumberDescription\":\"ECO: Coupler housing plating change\",\"ChangeNumberValidFromDate\":\"2026-09-05T00:00:00Z\",\"ReasonForChangeText\":\"ARENA ECO-2026-0088\",\"ChangeNumberStatus\":\"01\"}", - "StatusCriticality": 3, "IsApprovable": false, - "IsRetriable": false + "IsRetriable": false, + "LocalLastChangedAt": "2026-09-05T11:30:00Z", + "__OperationControl": { + "approve": false, + "reject": false, + "retrySync": false + }, + "SAP__Messages": [] }, { "ID": "6f1c2a3e-0005-4a6b-9c1d-000000000005", "ItemNumber": "MC-410-0250", "ItemName": "Datasheet MC-410-0250 Rev B", "ChangeType": "DOCUMENTS", - "Status": "REJECTED", - "StatusCriticality": 1, "ArenaEcoId": "ECO-2026-0101", + "Status": "REJECTED", + "StatusText": "Rejected", + "StatusCriticality": 1, "RequestedBy": "JDOE", "RequestedOn": "2026-09-10T11:15:00Z", "DecidedBy": "MSMITH", "DecidedOn": "2026-09-10T13:02:00Z", - "SapChangeNumber": null, - "ErrorMessage": null, + "SapChangeNumber": "", + "ErrorMessage": "", "RejectReason": "Datasheet revision B superseded by Rev C in Arena; re-stage after ECO-2026-0121 is released.", "SapPreview": "{\"BusinessObjectTypeName\":\"BUS1001006\",\"LinkedSAPObjectKey\":\"MC-410-0250\",\"Url\":\"https://app.bom.com/files/MC-410-0250-B.pdf\",\"MIMEType\":\"text/url\",\"AttachmentDescription\":\"MC-410-0250 Datasheet Rev B\"}", "IsApprovable": false, - "IsRetriable": false + "IsRetriable": false, + "LocalLastChangedAt": "2026-09-10T13:02:00Z", + "__OperationControl": { + "approve": false, + "reject": false, + "retrySync": false + }, + "SAP__Messages": [] }, { "ID": "6f1c2a3e-0006-4a6b-9c1d-000000000006", "ItemNumber": "MC-200-0117", "ItemName": "Bias Tee Assembly, SMA", "ChangeType": "CHANGE_MASTER", - "Status": "FAILED", - "StatusCriticality": 1, "ArenaEcoId": "ECO-2026-0118", + "Status": "FAILED", + "StatusText": "Execution failed", + "StatusCriticality": 1, "RequestedBy": "JDOE", "RequestedOn": "2026-09-13T09:40:00Z", "DecidedBy": "AZAW", "DecidedOn": "2026-09-13T10:05:00Z", - "SapChangeNumber": null, + "SapChangeNumber": "", "ErrorMessage": "API_CHANGEMASTER;v=0002 returned 400: Input parameters are initial (A_ChangeMasterObjTypeAssgmt requires IsActiveForChangeNumber).", - "RejectReason": null, + "RejectReason": "", "SapPreview": "{\"ChangeNumberDescription\":\"ECO: Bias tee connector change to SMA-F\",\"ChangeNumberValidFromDate\":\"2026-09-13T00:00:00Z\",\"ReasonForChangeText\":\"ARENA ECO-2026-0118\",\"ChangeNumberStatus\":\"01\"}", "IsApprovable": false, - "IsRetriable": true + "IsRetriable": true, + "LocalLastChangedAt": "2026-09-13T10:05:00Z", + "__OperationControl": { + "approve": false, + "reject": false, + "retrySync": true + }, + "SAP__Messages": [] } ] diff --git a/app/webapp/localService/mainService/metadata.xml b/app/webapp/localService/mainService/metadata.xml index 1db5ea2..5096540 100644 --- a/app/webapp/localService/mainService/metadata.xml +++ b/app/webapp/localService/mainService/metadata.xml @@ -1,133 +1,593 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +ArenaEcoId +ChangeType +ItemNumber + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +@SAP__UI.LineItem + + + + + + +ItemNumber +Status +ChangeType +ArenaEcoId + + + + + + + + + + + + + + + + + + + + + + + +__OperationControl + + + + + + + + +SapPreview +__OperationControl + + + + + + + + + + + + + + + + + + + + + + + + + + +LocalLastChangedAt + + + + + + + +eq +ne +gt +ge +lt +le +and +or +contains +startswith +endswith +any +all + + + + + + + + + + +application/json +application/pdf + + + + + + + + + + + + + + + + + + + + + + + + + + + + +application/json +multipart/mixed + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/webapp/manifest.json b/app/webapp/manifest.json index eecb075..ebbbe26 100644 --- a/app/webapp/manifest.json +++ b/app/webapp/manifest.json @@ -24,7 +24,7 @@ } }, "mainService": { - "uri": "/sap/opu/odata4/sap/zui_arena_change_approval_o4/srvd/sap/zui_arena_change_approval/0001/", + "uri": "/sap/opu/odata4/sap/zui_arena_change_appr_o4/srvd/sap/zui_arena_change_approval/0001/", "type": "OData", "settings": { "annotations": [ diff --git a/docs/handover/rap/README.md b/docs/handover/rap/README.md index 811169f..48e7fb2 100644 --- a/docs/handover/rap/README.md +++ b/docs/handover/rap/README.md @@ -1,176 +1,632 @@ -# RAP handover — Arena change-approval BO (stage 1, 2026-09-15) +# RAP handover: Arena change-approval BO, built from an empty package (rev. 2026-09-16, corrected after the UI leg went live) -Target: development tenant `my431353` (ADT destination `0M7DEJI`), package `ZARENA_PLM` -(sub-package of `ZLOCAL`, no transport). The ADT MCP Server can create objects and activate -them but cannot write source (PRD V-4), so every file in this folder is a **ready-to-paste -source** for the VS Code ADT editor. Each file header names the target object, its -`abap:` URI and says "replace entire content". +Target: development tenant `my431353` (ADT destination `0M7DEJI`), package `ZARENA_PLM`, a +sub-package of `ZLOCAL` with no transport. No generator is used. Every object is created empty +through the ADT MCP `abap_creation-create_object`, filled by pasting a file from this folder into +the VS Code ADT editor, saved, and activated through `abap_activate_objects`. The ADT MCP cannot +write source (PRD V-4). -Every SAP behavioral claim in these files carries a `[source: … , verified 2026-09-15]` -tag; the sources are `abap-docs` (ABAP Cloud keyword docs + SAP samples), `sap-docs` -(SAP Help) and `sap-released-objects` (release states). +Most source files start with a header comment that names the target object, its `abap:` URI and its +README step. **The two service definition sources (U13 `zui_arena_change_approval.srvd.srvdsrv`, W4 +`zapi_arena_change_approval.srvd.srvdsrv`) carry no comment at all**, because a comment blocks SRVD +activation on this tenant (see Landmines). Their former header content is in the U13 and W4 notes +below. Where a header's `abap:` URI disagrees with this README, this README and the `filePath` +returned by `create_object` win. The U6 and U9 BDEF headers still name the generator-era `.bdef.json`; +they are left as they were, because those objects are already active. The W3 header, not yet pasted, +names `.bdef.abdl`. -## What exists in the tenant right now +The entity set, properties and actions match the Fiori app contract: entity set `ChangeApproval`, +key `ID`, bound actions `approve`, `reject(RejectReason)`, `retrySync`. The UI service is published +at `/sap/opu/odata4/sap/zui_arena_change_appr_o4/srvd/sap/zui_arena_change_approval/0001/` (the UI +binding was shortened to fit the 26-character limit, D-1). The app was reconciled to that live +service on 2026-09-16 (CHANGELOG): dataSource URL, namespace +`com.sap.gateway.srvd.zui_arena_change_approval.v0001` (taken from the **service definition**, not +the binding) and entity type `ChangeApprovalType` (no underscore), all from the live `$metadata` in +`docs/v2/zui_arena_change_appr_o4.metadata.xml` (live 2026-09-16; KNOWLEDGE 2026-09-16). Text in +this folder that says `ChangeApproval_Type`, or that the binding name sets the namespace, predates +that capture and is marked where it remains. -Generated by `x-ui-service` (all **active**, ATC 0 findings on 12 objects, 2026-09-15): +Citations use `[source: , verified ]`. The servers are `abap-docs`, +`sap-docs`, `sap-released-objects`, and `adt-mcp` for live tool answers on `0M7DEJI`. Observations +made in the editor or browser, with no tool answer to cite, are marked `(live )`. -| Object | Type | Notes | -|---|---|---| -| `ZZCHNGPPROVAL000` | TABL | persistent table (generator naming) | -| `ZZR_CHANGEAPPROVAL000` | DDLS / BDEF / DCLS | root view entity, managed BDEF, access control | -| `ZZBP_R_CHANGEAPPROVAL000` | CLAS | behavior pool of the base BDEF | -| `ZZC_CHANGEAPPROVAL000` | DDLS / BDEF / DDLX / DCLS | UI projection view, projection BDEF, metadata extension, access control | -| `ZZBP_C_CHANGEAPPROVAL000` | CLAS | projection behavior pool — **delete it** (step 9) | -| `ZZUI_ARENA_CHANGE_APPRVL000_O4` | SRVD + SRVB | service definition and OData V4 **UI** binding (same name), **unpublished** | -| `ZZARENA_CHANGE_APPROVAL000` | RONT + NONT | SAP object type / node type | +## Status (2026-09-16) -Created empty via `create_object` (exist, **no source, inactive**): +- **UI leg U1–U14: ACTIVE and ATC-clean.** All 14 objects answered "Activation successful." ATC over + the 14 objects: 0 errors, 0 warnings, 43 infos (worklist `12ACF6BD93811FE1ACB8096AC6763BD6`) + [source: adt-mcp abap_activate_objects, abap_atc_get_result on 0M7DEJI, verified 2026-09-16]. The + MCP returns only totals by priority, so the 43 infos are read in the ADT ATC view. Infos do not + gate publishing. +- **UI binding `ZUI_ARENA_CHANGE_APPR_O4`: PUBLISHED 2026-09-16 04:26**, with the **Publish code + lens** in the binding's `.srvb.json` source editor (U15). The ADT MCP has no publish tool, and VS + Code shows no confirmation beyond " published locally". The publish was therefore verified + by tool: `fetch_services` reports `isPublished: true`, and `fetch_service_information` returns + `https://my431353-api.s4hana.cloud.sap/sap/opu/odata4/sap/zui_arena_change_appr_o4/srvd/sap/zui_arena_change_approval/0001/?sap-client=080` + with entity set `ChangeApproval` [source: adt-mcp abap_business_services-fetch_services, + fetch_service_information on 0M7DEJI, verified 2026-09-16]. +- **Where the live service answers:** on the UI host `my431353.s4hana.cloud.sap`. The same path on + the `-api` host that `fetch_service_information` reports is refused to a business user as + "Forbidden / blocked by UCON" (live 2026-09-16; KNOWLEDGE 2026-09-16). The `-api` host is the + communication-user entry point. +- **Worker leg W1–W5:** started on the tenant 2026-09-16 06:14 (PRD status). Run it exactly as in + "Worker leg" below. This README records no worker-leg result. +- **Package:** `ZARENA_PLM` exists again under `ZLOCAL` (recreated 2026-09-16; validation in it + succeeded live at 02:27, CHANGELOG). Historical, before step 0: the package had been deleted + together with its contents, and `abap_creation-run_validation` answered "Package 'ZARENA_PLM' does + not exist." for TABL, DDLS and CLAS [source: adt-mcp on 0M7DEJI, verified 2026-09-16]. -| Object | Type | Purpose | -|---|---|---| -| `ZD_CHANGEAPPROVAL_REJECTP` | DDLS | abstract entity — `reject` parameter (`RejectReason`) | -| `ZD_CHANGEAPPROVAL_SYNCEDP` | DDLS | abstract entity — `markSynced` parameter (`SapChangeNumber`) | -| `ZD_CHANGEAPPROVAL_FAILEDP` | DDLS | abstract entity — `markFailed` parameter (`ErrorMessage`) | -| `ZC_CHANGEAPPROVAL_A2X` | DDLS + DCLS | A2X projection view (worker) and its access control | -| `ZAPI_ARENA_CHANGE_APPROVAL` | SRVD | Web API service definition (worker) | +## Object inventory and name limits -Not yet creatable (the MCP validation refuses until the referenced objects are active): +| Step | Object | Type (create code) | Source file(s) | Length / limit | +|---|---|---|---|---| +| U1 | `ZCHANGEAPPROVAL` | TABL (`TABL/DT`) | `zchangeapproval.tabl.asddls` | 15 / 16 | +| U2 | `ZD_CHANGEAPPROVAL_REJECTP` | DDLS (`DDLS/DF`) | `zd_changeapproval_rejectp.ddls.asddls` | 25 / 30 | +| U3 | `ZD_CHANGEAPPROVAL_SYNCEDP` | DDLS (`DDLS/DF`) | `zd_changeapproval_syncedp.ddls.asddls` | 25 / 30 | +| U4 | `ZD_CHANGEAPPROVAL_FAILEDP` | DDLS (`DDLS/DF`) | `zd_changeapproval_failedp.ddls.asddls` | 25 / 30 | +| U5 | `ZR_CHANGEAPPROVAL` | DDLS (`DDLS/DF`) | `zr_changeapproval.ddls.asddls` | 17 / 30 | +| U6 | `ZR_CHANGEAPPROVAL` | BDEF (`BDEF/BDO`) | `zr_changeapproval.bdef.asbdef` | 17 / 30 | +| U7 | `ZBP_R_CHANGEAPPROVAL` | CLAS (`CLAS/OC`) | `zbp_r_changeapproval.clas.abap` + `.clas.locals_imp.abap` | 20 / 30 | +| U8 | `ZC_CHANGEAPPROVAL` | DDLS (`DDLS/DF`) | `zc_changeapproval.ddls.asddls` | 17 / 30 | +| U9 | `ZC_CHANGEAPPROVAL` | BDEF (`BDEF/BDO`) | `zc_changeapproval.bdef.asbdef` | 17 / 30 | +| U10 | `ZR_CHANGEAPPROVAL` | DCLS (`DCLS/DL`) | `zr_changeapproval.dcls.asdcls` | 17 / 30 | +| U11 | `ZC_CHANGEAPPROVAL` | DCLS (`DCLS/DL`) | `zc_changeapproval.dcls.asdcls` | 17 / 30 | +| U12 | `ZC_CHANGEAPPROVAL` | DDLX (`DDLX/EX`) | `zc_changeapproval.ddlx.asddlx` | 17 / 40 | +| U13 | `ZUI_ARENA_CHANGE_APPROVAL` | SRVD (`SRVD/SRV`) | `zui_arena_change_approval.srvd.srvdsrv` (no comments) | 25 / 30 | +| U14 | `ZUI_ARENA_CHANGE_APPR_O4` | SRVB (`SRVB/SVB`), OData V4 - UI | none in this folder; `create_object` returns `.srvb.json` | 24 / 26 (D-1) | +| W1 | `ZC_CHANGEAPPROVAL_A2X` | DDLS (`DDLS/DF`) | `zc_changeapproval_a2x.ddls.asddls` | 21 / 30 | +| W2 | `ZC_CHANGEAPPROVAL_A2X` | DCLS (`DCLS/DL`) | `zc_changeapproval_a2x.dcls.asdcls` | 21 / 30 | +| W3 | `ZC_CHANGEAPPROVAL_A2X` | BDEF (`BDEF/BDO`) | `zc_changeapproval_a2x.bdef.asbdef` | 21 / 30 | +| W4 | `ZAPI_ARENA_CHANGE_APPROVAL` | SRVD (`SRVD/SRV`) | `zapi_arena_change_approval.srvd.srvdsrv` (no comments) | 26 / 30 | +| W5 | `ZAPI_ARENA_CHANGE_APPR_A2X` | SRVB (`SRVB/SVB`), Web API (see W5 for the exact `bindingType` value) | none in this folder; `.srvb.json` expected, as U14 | 26 / 26 (D-1) | -| Object | Type | Blocker seen 2026-09-15 | -|---|---|---| -| `ZC_CHANGEAPPROVAL_A2X` | BDEF (projection) | "The referenced STOB object ZC_CHANGEAPPROVAL_A2X does not exist" | -| `ZAPI_ARENA_CHANGE_APPROVAL_A2X` | SRVB (OData V4 - Web API) | "An active version of the Service Definition ZAPI_ARENA_CHANGE_APPROVAL does not exist" | +Limits: `TABL/DT` 16; `DDLS/DF`, `BDEF/BDO` and `SRVD/SRV` 30 [source: adt-mcp +`abap_creation-get_object_type_details` on 0M7DEJI, verified 2026-09-16]. Service bindings: 26 per +SAP Help. The ADT MCP accepts 30 for `SRVB/SVB`, but that is undocumented and not used here (D-1). +The documentation agrees +for tables ("maximum of 16 characters") [source: abap-docs /abap-docs-cloud/ABENDDIC_DBTAB_NAMES, +verified 2026-09-16] and for CDS view entities ("maximum of 30 characters"; the DDL source and the +CDS entity must have the same name) [source: abap-docs /abap-docs-cloud/ABENCDS_DEFINE_VIEW_ENTITY, +verified 2026-09-16]. `CLAS/OC` and `DCLS/DL` allow 30 and `DDLX/EX` allows 40 [source: adt-mcp +abap_creation-get_object_type_details on 0M7DEJI, verified 2026-09-16] (KNOWLEDGE 2026-09-16; these +were "not recorded" in the pre-build revision). -## Paste-and-activate sequence +Not created: a projection behavior pool (see design decisions) and an SAP object type or node type. +No documentation found makes an SAP object type mandatory for a RAP BO. SAP Help lists it only as +one of the objects the "OData UI Service from Scratch" wizard generates [source: sap-docs sap-help +loio d3f5e90a306a44b6b37b8b05f04d2c10, verified 2026-09-16]. Business events are the case that +requires `@ObjectModel.sapObjectNodeType.name` [source: sap-docs +/btp-cloud-platform/30-development/log-custom-business-events-6ecc451, verified 2026-09-16]. Add +one if the BO later raises business events. -Open each object in the VS Code ADT editor (or ADT Eclipse), replace the whole source with -the file content, save, activate. Activation order matters — dependencies are top-down. +Split: the **UI leg** has 14 source files (13 objects with source + the U14 binding). The **worker +leg** has 4 source files (+ the W5 binding). The `markSynced`/`markFailed` parameter entities +(U3, U4) belong to the UI leg because the base BDEF (U6) names them. They must be active before +U6 activates. -1. `zzchngpproval000.tabl.asddls` → table `ZZCHNGPPROVAL000`. (File name uses the - abap-file-formats extension by analogy with `.ddls.asddls`; the tenant object is the - same regardless.) -2. `zd_changeapproval_rejectp.ddls.asddls`, `zd_changeapproval_syncedp.ddls.asddls`, - `zd_changeapproval_failedp.ddls.asddls` → the three abstract entities. -3. `zzr_changeapproval000.ddls.asddls` → root view entity. -4. `zzr_changeapproval000.bdef.asbdef` → base BDEF. - Watch for a message about the unmapped elements `StatusText`, `StatusCriticality`, - `IsApprovable`, `IsRetriable` (CDS-calculated, deliberately not in the mapping). A - warning is acceptable; an **error** means the fallback in the file header applies. -5. `zzbp_r_changeapproval000.clas.abap` (global class) and - `zzbp_r_changeapproval000.clas.locals_imp.abap` (Local Types tab) → behavior pool. -6. `zzc_changeapproval000.ddls.asddls` and `zc_changeapproval_a2x.ddls.asddls` → both - projection views. -7. `zzc_changeapproval000.bdef.asbdef` → UI projection BDEF. -8. Create the A2X projection BDEF (now possible): ADT *New Behavior Definition* on - `ZC_CHANGEAPPROVAL_A2X`, implementation type *Projection* — or via MCP: - `abap_creation-run_validation` + `create_object` with objectType `BDEF/BDO` and - `{"behaviorDefinitionType":"definition","packageName":"ZARENA_PLM","rootEntity":"ZC_CHANGEAPPROVAL_A2X","name":"ZC_CHANGEAPPROVAL_A2X","description":"Arena change approval: A2X projection behavior","implementationType":"Projection"}`, - transport `""`. Then paste `zc_changeapproval_a2x.bdef.asbdef`, activate. -9. Delete the generated class `ZZBP_C_CHANGEAPPROVAL000` (no longer referenced by the - projection BDEF; a projection needs a behavior pool only for new actions/functions - [source: abap-docs /abap-docs-cloud/ABENBDL_BDEF_PROJECTION_HEADER, verified 2026-09-15]). -10. `zzr_changeapproval000.dcls.asdcls`, then `zzc_changeapproval000.dcls.asdcls` and - `zc_changeapproval_a2x.dcls.asdcls` → access controls. -11. `zzc_changeapproval000.ddlx.asddlx` → metadata extension. -12. `zzui_arena_change_apprvl000_o4.srvd.srvdsrv` and - `zapi_arena_change_approval.srvd.srvdsrv` → service definitions. -13. Create the Web API binding (now possible): ADT *New Service Binding* on - `ZAPI_ARENA_CHANGE_APPROVAL`, binding type *OData V4 - Web API*, name - `ZAPI_ARENA_CHANGE_APPROVAL_A2X` — or via MCP `create_object` with objectType - `SRVB/SVB` and - `{"packageName":"ZARENA_PLM","name":"ZAPI_ARENA_CHANGE_APPROVAL_A2X","description":"Arena change approval: OData V4 Web API binding (worker)","bindingType":"OData V4 - Web API","serviceDefinition":"ZAPI_ARENA_CHANGE_APPROVAL"}`. - Activate. **Do not publish** either binding (publishing is a later brief, together - with the IAM app / communication scenario). +## Resolved decisions -## Activation URIs for `abap_activate_objects` (after pasting) +- **D-1: service binding names stay within 26 characters (resolved 2026-09-16).** SAP Help gives + 26 characters as the maximum length for a service binding name in every version checked: ABAP + Cloud [source: sap-docs sap-help loio 4bad0d7280b742a68dd2d42b1441f09a, verified 2026-09-16], SAP + Cloud Platform [source: sap-docs sap-help loio f3e608ace2a64aada33bdd3e2419113a and + 6709cab6cb5b4c01b28463d760429a9a, verified 2026-09-16], and ABAP Platform 2021 [source: sap-docs + sap-help loio 777e027f61c3490dba0433443d9143a6, verified 2026-09-16]. The ADT MCP's 30 is + undocumented. The bindings are therefore: + - UI binding `ZUI_ARENA_CHANGE_APPR_O4` (24), replacing the 28-character long form. + - Web API binding `ZAPI_ARENA_CHANGE_APPR_A2X` (26), replacing the 30-character long form. -Pattern learned live 2026-09-15: `abap:/repotree-v1/0M7DEJI/System%20Library/ZLOCAL/ZARENA_PLM///.` -with these folders/extensions (all verified by a successful activation): + The service definitions `ZUI_ARENA_CHANGE_APPROVAL` (25) and `ZAPI_ARENA_CHANGE_APPROVAL` (26) + and every other object name are unchanged. Consequence: the binding name is the first URL + segment, so the app's dataSource URL had to change. **Live correction (2026-09-16):** the + pre-build text here also said the binding name sets the schema namespace. It does not; the live + namespace `com.sap.gateway.srvd.zui_arena_change_approval.v0001` comes from the service definition + (live 2026-09-16, `docs/v2/zui_arena_change_appr_o4.metadata.xml`; KNOWLEDGE 2026-09-16). The app + was reconciled to the live service on 2026-09-16 (CHANGELOG). The PRD worker contract row now + names `ZAPI_ARENA_CHANGE_APPR_A2X` (PRD read 2026-09-16), so that governance item is closed. U14 + published under its 24-character name, so a name within 26 is proven to publish. Whether + publishing accepts more than 26 is still unknown (KNOWLEDGE 2026-09-16). -| Type | Folder | Extension | -|---|---|---| -| TABL | `Dictionary/Database%20Tables` | `.tabl.json` | -| DDLS | `Core%20Data%20Services/Data%20Definitions` | `.ddls.acds` | -| BDEF | `Core%20Data%20Services/Behavior%20Definitions` | `.bdef.json` (`.bdef.acds`, `.bdef.asbdef`, `.bdef.abap` all fail with "array element 0 is null") | -| DDLX | `Core%20Data%20Services/Metadata%20Extensions` | `.ddlx.acds` | -| DCLS | `Core%20Data%20Services/Access%20Controls` | `.dcls.acds` | -| CLAS | `Source%20Code%20Library/Classes` | `.clas.abap` | -| SRVD | `Business%20Services/Service%20Definitions` | `.srvd.acds` | -| SRVB | `Business%20Services/Service%20Bindings` | `.srvb.json` | -| RONT | `Business%20Services/SAP%20Object%20Types` | `.ront.json` | -| NONT | `Business%20Services/SAP%20Object%20Node%20Types` | `.nont.json` | +## Preconditions -In sequence order (`B` = `abap:/repotree-v1/0M7DEJI/System%20Library/ZLOCAL/ZARENA_PLM`): +1. VS Code with ADT (`sapse.adt-vscode`) is running, destination `0M7DEJI` is logged on, and the + ADT MCP answers on `http://127.0.0.1:2236/mcp`. After a VS Code reload, refresh the bearer + token in `.mcp.json` (CLAUDE.md). A Claude Code session exposes the `adt-mcp` tools only if the + server was up when the session started (KNOWLEDGE 2026-09-16). +2. `abap_list_destinations` lists `0M7DEJI`. + +## Step 0: recreate the package (done 2026-09-16) + +Done: `ZARENA_PLM` exists again under `ZLOCAL`, validation in it succeeded live at 02:27, and +`abap_transport-get` answered `isRecordingRequired: false` (CHANGELOG 2026-09-16). The steps below +are kept as the record. + +In ADT (VS Code or Eclipse), create development package **`ZARENA_PLM` with super-package +`ZLOCAL`**, no transport. The ADT MCP has no package tool. Use a `ZLOCAL` sub-package because +`ZLOCAL` itself is a structure package, `$TMP` fails the language-version authorization, and +sub-packages of `ZLOCAL` created from release 2508 need no workbench request [source: sap-docs +sap-help loio a0d80f14fcb74cb1ac8fb6f3d9ad48cc (2608), verified 2026-09-15; refusals live +2026-09-15, KNOWLEDGE]. + +Check it read-only: `abap_creation-run_validation` for `TABL/DT` with the U1 JSON must no longer +answer "Package 'ZARENA_PLM' does not exist". Then call `abap_transport-get` once for U1 +(`objectType` `TABL/DT`, `developmentPackage` `ZARENA_PLM`, `isCreation` true) and expect "recording +not required". Every `create_object` below uses `"transportRequestNumber": ""`. + +## Per-object routine + +For every step: + +1. `abap_creation-get_object_type_details` (objectType, name, description). It lists the required + fields and limits. If it names a required field that the JSON below lacks, add it and note it. +2. `abap_creation-run_validation` with the JSON. +3. `abap_creation-create_object` with the same JSON, `destination` `0M7DEJI`, transport `""`. + **Record the `filePath` it returns.** That path is both the paste target and the activation URI. +4. Open that **source** file in the VS Code ADT editor, replace the whole content with the handover + file (its header comment included where it has one; the SRVD files have none and must stay + comment-free), and save. A DDIC database table is edited as Dictionary DDL source generated from its + metadata [source: abap-docs /abap-docs-cloud/ABENDDIC_DEFINE_TABLE, verified 2026-09-16]. +5. `abap_activate_objects` with the returned `filePath` (URL-encoded, spaces as `%20`). The + activation table below is a cross-check, not a substitute. Fix errors before the next step. + +**General rule (live 2026-09-16): in the VS Code ADT workspace, the `.json` file in an object folder +is header metadata and never receives pasted source. Always use the `filePath` returned by +`create_object`.** For example, `zchangeapproval.tabl.json` holds only formatVersion, description, +originalLanguage and `abapLanguageVersion`, and the table source lives in `zchangeapproval.tabl.ddic` +[source: adt-mcp abap_creation-create_object / abap_activate_objects on 0M7DEJI, verified +2026-09-16]. The returned source files are TABL `.tabl.ddic`, DDLS `.ddls.acds`, DCLS `.dcls.acds`, +DDLX `.ddlx.acds`, BDEF `.bdef.abdl`, CLAS `.clas.abap` and SRVD `.srvd.acds`. Among the types in +this build, the one returned `filePath` that is a `.json` is the service binding's `.srvb.json`. +Nothing is pasted into it, the binding is activated through it (U14), and its editor carries the +Publish code lens (U15). + +Create-metadata tags per type [source: adt-mcp `abap_creation-get_object_type_details` on 0M7DEJI, +verified 2026-09-16]: + +- `TABL/DT`: `packageName`, `name`, `description`. +- `DDLS/DF`: `packageName`, `name`, `description`, optional `referencedObject` (omitted here). +- `SRVD/SRV`: `packageName`, `name`, `description`, `sourceType` (`S` | `X` | none). Use `"S"`: it + worked live, and omitting it fails with "serviceDefinitionInfo.sourceType is null" (live 2026-09-15). + `SRVD/SRV` also offers an optional `referencedObject` (omitted here) [source: adt-mcp + get_object_type_details on 0M7DEJI, verified 2026-09-16]. +- `SRVB/SVB`: `packageName`, `name`, `description`, `bindingType`, `serviceDefinition`. Proven + `bindingType` value: `OData V4 - UI` (U14, 2026-09-16). The Web API value is not proven; see W5. +- `BDEF/BDO`: `behaviorDefinitionType`, `packageName`, `rootEntity`, `name`, `description`, + `implementationType` (`Managed` | `Unmanaged` | `Projection` | `Abstract` | `Interface`). +- `CLAS/OC`: `packageName`, `name` (≤30), `description`, optional superclass and interfaces (none + needed here). `DCLS/DL`: `packageName`, `name` (≤30), `description`, optional `protectedEntity`. + `DDLX/EX`: `packageName`, `name` (≤40), `description`, optional `extendedEntity` [source: adt-mcp + abap_creation-get_object_type_details on 0M7DEJI, verified 2026-09-16]. The pre-build revision + marked these three "tags not recorded"; the JSON in the UI-leg table below still carries that + marker as the record. + +Descriptions are kept at 40 characters or fewer. The per-type description limit was not recorded. + +BDEF and SRVB creation validates the referenced DDLS/SRVD's **active** version (live 2026-09-15: +"The referenced STOB object … does not exist"; "An active version of the Service Definition … does +not exist"). The sequence below always activates the referenced object first. + +## UI leg (first; done 2026-09-16) + +**Done: U1–U14 are active, ATC-clean, and the U14 binding is published (see Status).** Do not re-run +this table. It is kept as the build record. The JSON column is the pre-build plan: the `(tags +unconfirmed)` markers were settled on 2026-09-16 (per-object routine), and U10 was created with +`protectedEntity` added (CHANGELOG 2026-09-16). + +| Step | create_object JSON (objectType) | Paste | Depends on (active) | +|---|---|---|---| +| U1 | `TABL/DT` `{"packageName":"ZARENA_PLM","name":"ZCHANGEAPPROVAL","description":"Arena change approval"}` | `zchangeapproval.tabl.asddls` | none | +| U2 | `DDLS/DF` `{"packageName":"ZARENA_PLM","name":"ZD_CHANGEAPPROVAL_REJECTP","description":"Arena approval: reject parameter"}` | `zd_changeapproval_rejectp.ddls.asddls` | none | +| U3 | `DDLS/DF` `{"packageName":"ZARENA_PLM","name":"ZD_CHANGEAPPROVAL_SYNCEDP","description":"Arena approval: markSynced parameter"}` | `zd_changeapproval_syncedp.ddls.asddls` | none | +| U4 | `DDLS/DF` `{"packageName":"ZARENA_PLM","name":"ZD_CHANGEAPPROVAL_FAILEDP","description":"Arena approval: markFailed parameter"}` | `zd_changeapproval_failedp.ddls.asddls` | none | +| U5 | `DDLS/DF` `{"packageName":"ZARENA_PLM","name":"ZR_CHANGEAPPROVAL","description":"Arena change approval"}` | `zr_changeapproval.ddls.asddls` | U1 | +| U6 | `BDEF/BDO` `{"behaviorDefinitionType":"definition","packageName":"ZARENA_PLM","rootEntity":"ZR_CHANGEAPPROVAL","name":"ZR_CHANGEAPPROVAL","description":"Arena change approval behavior","implementationType":"Managed"}` | `zr_changeapproval.bdef.asbdef` | U2, U3, U4, U5 | +| U7 | `CLAS/OC` `{"packageName":"ZARENA_PLM","name":"ZBP_R_CHANGEAPPROVAL","description":"Arena change approval behavior pool"}` (tags unconfirmed) | `zbp_r_changeapproval.clas.abap` (global class) + `zbp_r_changeapproval.clas.locals_imp.abap` (Local Types) | U6 | +| U8 | `DDLS/DF` `{"packageName":"ZARENA_PLM","name":"ZC_CHANGEAPPROVAL","description":"Arena change approval UI projection"}` | `zc_changeapproval.ddls.asddls` | U5 (U6/U7 done) | +| U9 | `BDEF/BDO` `{"behaviorDefinitionType":"definition","packageName":"ZARENA_PLM","rootEntity":"ZC_CHANGEAPPROVAL","name":"ZC_CHANGEAPPROVAL","description":"Arena approval UI projection behavior","implementationType":"Projection"}` | `zc_changeapproval.bdef.asbdef` | U6, U7, U8 | +| U10 | `DCLS/DL` `{"packageName":"ZARENA_PLM","name":"ZR_CHANGEAPPROVAL","description":"Arena change approval access"}` (tags unconfirmed) | `zr_changeapproval.dcls.asdcls` | U5 | +| U11 | `DCLS/DL` `{"packageName":"ZARENA_PLM","name":"ZC_CHANGEAPPROVAL","description":"Arena approval UI projection access"}` (tags unconfirmed) | `zc_changeapproval.dcls.asdcls` | U8, U10 | +| U12 | `DDLX/EX` `{"packageName":"ZARENA_PLM","name":"ZC_CHANGEAPPROVAL","description":"Arena approval UI annotations"}` (tags unconfirmed) | `zc_changeapproval.ddlx.asddlx` | U8 | +| U13 | `SRVD/SRV` `{"packageName":"ZARENA_PLM","name":"ZUI_ARENA_CHANGE_APPROVAL","description":"Arena change approval UI service","sourceType":"S"}` | `zui_arena_change_approval.srvd.srvdsrv` | U8, U9 | +| U14 | `SRVB/SVB` `{"packageName":"ZARENA_PLM","name":"ZUI_ARENA_CHANGE_APPR_O4","description":"Arena change approval UI binding","bindingType":"OData V4 - UI","serviceDefinition":"ZUI_ARENA_CHANGE_APPROVAL"}` | none | U13 | + +Notes per step: + +- **U1:** paste `zchangeapproval.tabl.asddls` into **`zchangeapproval.tabl.ddic`**, never into + `zchangeapproval.tabl.json` (metadata only, see the per-object routine). Proven: U1 activated + through `.tabl.ddic` [source: adt-mcp abap_activate_objects on 0M7DEJI, verified 2026-09-16]. +- **U6 then U7 (order matters).** **Resolved live 2026-09-16:** U6 activated through `.bdef.abdl` on + the first attempt, before class `ZBP_R_CHANGEAPPROVAL` existed, so the fallback below was not + needed. U6 and U7 were then activated together in one call [source: adt-mcp abap_activate_objects + on 0M7DEJI, verified 2026-09-16]. The pre-build instruction follows as the record. + Activate the base BDEF **before** the behavior pool class exists, + then create the class. SAP's RAP exercise does it that way [source: abap-docs + /abap-platform-rap-opensap/week4/unit3#step-3-create-and-implement-the-behavior-implementation-bil-class-for-the-travel-entity, + verified 2026-09-16]. That exercise is an unmanaged, non-strict BDEF. If U6 activation fails with + an **error** saying class `ZBP_R_CHANGEAPPROVAL` does not exist, run U7's create and paste, then + activate U6 and U7 together in one `abap_activate_objects` call. Also watch U6 for messages about + the unmapped CDS-calculated elements `StatusText`, `StatusCriticality`, `IsApprovable` and + `IsRetriable`. A warning is acceptable. An error triggers the fallback in the BDEF file header. +- **U7:** paste the global class, then switch to the Local Types include and paste the handlers. + Save both. Activate through the `.clas.abap` URI. The Local Types include also implements the + `checkDuplicate` validation. Its duplicate query uses `OPTIONS PRIVILEGED ACCESS`. If the syntax + check refuses that form, the documented fallback is in the include header. `create_object` for + the class returned only the global `.clas.abap` path, no Local Types path (KNOWLEDGE 2026-09-16). +- **U9:** no behavior pool is created for the projection (see design decisions). `use etag` is in + the entity header, not the body (design decisions). `create_object` returned `.bdef.abdl`, and U9 + activated through it [source: adt-mcp abap_creation-create_object, abap_activate_objects on + 0M7DEJI, verified 2026-09-16]. +- **U13: service definition `ZUI_ARENA_CHANGE_APPROVAL` (UI service).** This note holds what the + source file's header comment used to say. The header is gone, because it blocked activation. + - `abap:` URI: + `abap:/repotree-v1/0M7DEJI/System%20Library/ZLOCAL/ZARENA_PLM/Business%20Services/Service%20Definitions/ZUI_ARENA_CHANGE_APPROVAL/zui_arena_change_approval.srvd.acds` + - Action: create empty through the ADT MCP (`SRVD/SRV`, `sourceType` `"S"`), replace the entire + content with `zui_arena_change_approval.srvd.srvdsrv`, save, and activate after the + `ZC_CHANGEAPPROVAL` view (U8) and BDEF (U9). Then create the OData V4 UI binding + `ZUI_ARENA_CHANGE_APPR_O4` on it (U14). + - **What happened live (2026-09-16):** the first activation failed with "Comments are not supported + and will be deleted on save" (Ln 1, the header block comment) and "oData V4 services with no + draft capability will be primarily READ-ONLY" (Ln 21). The same definition, re-saved without the + comment, activated. The comment was the blocker; the draft message was not (the BO has no draft, + by design) (live 2026-09-16; CHANGELOG 2026-09-16). + - Contract: exposes exactly one entity set, `ChangeApproval`. The service URL (live, published) is + `/sap/opu/odata4/sap/zui_arena_change_appr_o4/srvd/sap/zui_arena_change_approval/0001/`. The + first segment is the binding (service group `ZUI_ARENA_CHANGE_APPR_O4`, 24 characters, within + SAP Help's 26-character binding limit, D-1). The segment after `srvd/sap/` is the binding's + service alias (the service name in the binding editor), which can differ from this definition's + name and can be renamed only while the binding is unpublished [source: sap-docs community-14433266 + "How to rename an OData V4 service?" (SAP, 2026-07-04), verified 2026-09-16]. The former header + called the app reconciliation pending. It was done on 2026-09-16 (CHANGELOG). + - `expose … as `: the alias becomes the entity set (`ChangeApproval`) and the entity type + (`ChangeApproval_Type`) [source: abap-docs /abap-fiori-showcase/06_object_page_content (Service + Definition), verified 2026-09-15]. **Live correction:** the entity set is `ChangeApproval`, but + the entity type is `ChangeApprovalType`, with no underscore (live 2026-09-16, + `docs/v2/zui_arena_change_appr_o4.metadata.xml`; KNOWLEDGE 2026-09-16). + - `provider contracts odata_v4_ui` switches on the UI-binding syntax checks and must match the + binding type [source: abap-docs /abap-docs-cloud/ABENSRVD_PROVIDER_CONTRACT, verified 2026-09-15]. +- **U14:** **Obsolete, removed:** the pre-build instruction to open the binding in the editor + *before activating* to check the alias. What was done and proven on 2026-09-16: `create_object` + returned `.srvb.json`, U14 was activated through that `filePath`, and the alias was verified + **after** activation with `fetch_services`: service `ZUI_ARENA_CHANGE_APPROVAL`, version `0001`, + OData V4, unpublished [source: adt-mcp abap_creation-create_object, abap_activate_objects, + abap_business_services-fetch_services on 0M7DEJI, verified 2026-09-16]. The alias check must + come before publishing, not before activating. The URL segment after `srvd/sap/` is that alias, + not necessarily the definition name, and it can be renamed only while the binding is unpublished + [source: sap-docs community-14433266 "How to rename an OData V4 service?", verified 2026-09-16]. + The binding name `ZUI_ARENA_CHANGE_APPR_O4` (24) is within SAP Help's 26-character limit (D-1). + +### U15: checks and publish of the UI binding (done 2026-09-16) + +1. `abap_atc_run` on all 14 UI-leg objects: `ZCHANGEAPPROVAL` TABL; `ZD_CHANGEAPPROVAL_REJECTP`, + `ZD_CHANGEAPPROVAL_SYNCEDP`, `ZD_CHANGEAPPROVAL_FAILEDP`, `ZR_CHANGEAPPROVAL`, `ZC_CHANGEAPPROVAL` + DDLS; `ZR_CHANGEAPPROVAL`, `ZC_CHANGEAPPROVAL` BDEF; `ZBP_R_CHANGEAPPROVAL` CLAS; + `ZR_CHANGEAPPROVAL`, `ZC_CHANGEAPPROVAL` DCLS; `ZC_CHANGEAPPROVAL` DDLX; + `ZUI_ARENA_CHANGE_APPROVAL` SRVD; `ZUI_ARENA_CHANGE_APPR_O4` SRVB. Then + `abap_atc_get_result`. **Result:** 0 errors, 0 warnings, 43 infos (worklist + `12ACF6BD93811FE1ACB8096AC6763BD6`) [source: adt-mcp abap_atc_get_result on 0M7DEJI, verified + 2026-09-16]. +2. `abap_business_services-fetch_services` on `ZUI_ARENA_CHANGE_APPR_O4`. **Result:** + `isPublished: false`, service `ZUI_ARENA_CHANGE_APPROVAL`, version `0001`, OData V4. +3. **Publish with the Publish code lens, not a button.** Open the binding's `.srvb.json` source in + the VS Code ADT editor and choose the **Publish** code lens in the source. In ADT for VS Code a + service binding is a source-based editor, and publish, unpublish and preview are code lenses + [source: sap-docs sap-help loio dd6f5de63e7245a7abe7e39389841596 "ABAP RAP Tools", verified + 2026-09-16]. (Superseded wording: "the Publish button in the ADT service binding editor" describes + the Eclipse form-based editor.) The binding must be active first [source: sap-docs sap-help loio + b58a3c27df4e406f9335d4b346f6be04, verified 2026-09-16]. The ADT MCP has no publish tool. VS Code + shows no confirmation beyond " published locally", so step 4 is the proof. **Done + 2026-09-16 04:26.** +4. `fetch_services` again (expect `isPublished: true`), then + `abap_business_services-fetch_service_information`. The service URL path must equal + `/sap/opu/odata4/sap/zui_arena_change_appr_o4/srvd/sap/zui_arena_change_approval/0001/` + exactly, and the entity set must be `ChangeApproval`. **Result:** both hold, on host + `my431353-api.s4hana.cloud.sap` with `sap-client=080` [source: adt-mcp + abap_business_services-fetch_services, fetch_service_information on 0M7DEJI, verified + 2026-09-16]. Business users and browser checks use the UI host `my431353.s4hana.cloud.sap` + instead; `-api` refuses them (Landmines). +5. Hand back the live `$metadata` for PRD V-2. **Done:** it is captured in + `docs/v2/zui_arena_change_appr_o4.metadata.xml`, and the app was reconciled to it on 2026-09-16 + (CHANGELOG). The pre-build expectation `ChangeApproval_Type` was wrong; the live entity type is + `ChangeApprovalType`. See "App contract check" for the settled types. + +## Worker leg (second; run exactly this sequence) + +Prerequisites, all met on 2026-09-16: the U5 root view, U6 base BDEF, U7 behavior pool and U10 root +access control are active. Every step uses the per-object routine (details → validation → create → +paste into the returned `filePath` → activate through the same `filePath`). + +| Step | create_object JSON (objectType) | Paste | Needs (active) | +|---|---|---|---| +| W1 | `DDLS/DF` `{"packageName":"ZARENA_PLM","name":"ZC_CHANGEAPPROVAL_A2X","description":"Arena approval A2X projection"}` | `zc_changeapproval_a2x.ddls.asddls` into `.ddls.acds` | activation: U5 (U6, U7 done) | +| W2 | `DCLS/DL` `{"packageName":"ZARENA_PLM","name":"ZC_CHANGEAPPROVAL_A2X","description":"Arena approval A2X projection access"}` (no `protectedEntity`, see round 1) | `zc_changeapproval_a2x.dcls.asdcls` into `.dcls.acds` | activation: W1, U10 | +| W3 | `BDEF/BDO` `{"behaviorDefinitionType":"definition","packageName":"ZARENA_PLM","rootEntity":"ZC_CHANGEAPPROVAL_A2X","name":"ZC_CHANGEAPPROVAL_A2X","description":"Arena approval A2X behavior","implementationType":"Projection"}` | `zc_changeapproval_a2x.bdef.asbdef` into `.bdef.abdl` | **creation: W1**; activation: W1, U6, U7 | +| W4 | `SRVD/SRV` `{"packageName":"ZARENA_PLM","name":"ZAPI_ARENA_CHANGE_APPROVAL","description":"Arena change approval Web API","sourceType":"S"}` | `zapi_arena_change_approval.srvd.srvdsrv` (no comments) into `.srvd.acds` | activation: W1, W3 | +| W5 | `SRVB/SVB` `{"packageName":"ZARENA_PLM","name":"ZAPI_ARENA_CHANGE_APPR_A2X","description":"Arena change approval Web API binding","bindingType":"OData V4 - Web API","serviceDefinition":"ZAPI_ARENA_CHANGE_APPROVAL"}` (check `bindingType`, step 7) | none | **creation: W4**; activation: W4 | + +**Round 1: W1, W2 and W4 created empty and pasted; W1 and W2 activated.** + +1. Create W1 (DDLS), W2 (DCLS) and W4 (SRVD) empty. Record each returned `filePath`. + - W2: leave out the optional `protectedEntity`. W1 has no active version yet. BDEF creation is + known to validate its referenced entity's active version; whether DCLS creation does the same + for `protectedEntity` is untested. The pasted source replaces the skeleton anyway. + - W4: `"sourceType":"S"` is required. Leave out the optional `referencedObject`. +2. Paste W1, W2 and W4 into their returned source files and save all three. Never paste into a + `.json`. W4 is pasted now but activated in round 2. +3. Activate W1, then W2, in that order. + +**Round 2: W3 created and pasted; W3 then W4 activated.** + +4. Create W3 (BDEF, `Projection`). This needs W1 **active**, because BDEF creation validates the + referenced entity's active version (live 2026-09-15). Expect a `filePath` ending in `.bdef.abdl`. +5. Paste W3 into that `.bdef.abdl` file and save. +6. Activate W3, then W4. + +**Binding: W5 created and activated, not published.** + +7. **Check the `bindingType` string before creating.** `OData V4 - UI` is proven (U14). The Web API + value has not been seen live. SAP Help names the type `OData V4 - Web API` [source: sap-docs + /btp-cloud-platform/30-development/service-bindings-c6dee8e, verified 2026-09-16], but the 2608 + release note for ADT for VS Code calls it "OData V4 – API" [source: sap-docs sap-help loio + 7f674e6190564228a5bb217ebf382611 (APPL-VSC-0011), verified 2026-09-16]. Take the exact value from + `get_object_type_details` for `SRVB/SVB` if it lists one; otherwise `run_validation` with + `OData V4 - Web API` settles it. If validation refuses the value and no allowed value is listed, + stop and report the message rather than trying label variants. The type must be the Web API one: the provider contract + `odata_v4_webapi` and the binding type must match, or an error occurs [source: abap-docs + /abap-docs-cloud/ABENSRVD_PROVIDER_CONTRACT, verified 2026-09-16]. +8. Create W5 `ZAPI_ARENA_CHANGE_APPR_A2X` (needs W4 active). Activate it through its returned + `filePath` (`.srvb.json` expected, as U14). +9. Verify with `abap_business_services-fetch_services` on `ZAPI_ARENA_CHANGE_APPR_A2X`. Expect + service `ZAPI_ARENA_CHANGE_APPROVAL`, version `0001`, OData V4, and `isPublished: false`. That + unpublished state is the intended end state of this handover. Do not follow the tool's prompt to + have it published, and do not call `fetch_service_information` for W5. + +**W5 is NOT published in this handover.** Web API exposure to the worker's communication user goes +through a custom communication scenario (PRD V-2), together with the IAM app. That is a separate +brief. The communication user will reach it on the `-api` host, the communication-user entry point +(KNOWLEDGE 2026-09-16). The W5 name `ZAPI_ARENA_CHANGE_APPR_A2X` is 26 characters, exactly SAP Help's +limit (D-1), and the PRD worker contract row names it. + +Follow-up, not part of the sequence above: `abap_atc_run` on W1–W5 (DDLS, DCLS, BDEF, SRVD, SRVB), +as U15 step 1 did for the UI leg. + +Notes per step: + +- **W3:** the source header's `abap:` URI line was corrected from `.bdef.json` to `.bdef.abdl` on + 2026-09-16 (comment only; no BDL change). Still use the returned `filePath`. +- **W4: service definition `ZAPI_ARENA_CHANGE_APPROVAL` (Web API service for the worker).** This note + holds what the source file's header comment used to say. The header is gone, for the U13 reason. + - `abap:` URI: + `abap:/repotree-v1/0M7DEJI/System%20Library/ZLOCAL/ZARENA_PLM/Business%20Services/Service%20Definitions/ZAPI_ARENA_CHANGE_APPROVAL/zapi_arena_change_approval.srvd.acds` + - Action: create empty through the ADT MCP (`SRVD/SRV`, `sourceType` `"S"`), replace the entire + content with `zapi_arena_change_approval.srvd.srvdsrv`, save, and activate after the + `ZC_CHANGEAPPROVAL_A2X` view (W1) and BDEF (W3). Then create the Web API binding + `ZAPI_ARENA_CHANGE_APPR_A2X` on it (W5), and do not publish it. + - **Keep the source comment-free.** A leading comment blocked U13's activation (U13 note). + On U13 the "no draft capability … READ-ONLY" message did not block. If W4 activation fails and + lists only that message, read its severity in the editor's Problems view before changing + anything; the BO has no draft by design (CHANGELOG 2026-09-16). + - Sources: as `ZUI_ARENA_CHANGE_APPROVAL` (U13 note); `odata_v4_webapi` is the Web API contract + [ABENSRVD_PROVIDER_CONTRACT], re-read as [source: abap-docs + /abap-docs-cloud/ABENSRVD_PROVIDER_CONTRACT, verified 2026-09-16]. + +## Activation URIs for `abap_activate_objects` + +**Rule: activate through the `filePath` that `create_object` returned for the object** (per-object +routine). The table below is a cross-check. Pattern, learned live 2026-09-15 and unchanged for every +object built on 2026-09-16: +`abap:/repotree-v1/0M7DEJI/System%20Library/ZLOCAL/ZARENA_PLM///.`. +In the VS Code workspace the `.json` file in an object folder is header metadata and never receives +pasted source. Among the types in this build, SRVB is the one whose returned `filePath` is the +`.json` (nothing is pasted there). + +Extensions as returned by `create_object` for this build and proven by activation on 2026-09-16 +[source: adt-mcp abap_creation-create_object, abap_activate_objects on 0M7DEJI, verified +2026-09-16]: + +| Type | Folder | Extension | Proven by | +|---|---|---|---| +| TABL | `Dictionary/Database%20Tables` | `.tabl.ddic` | U1. `.tabl.json` is header metadata only; it activated a **generator-era** table on 2026-09-15 and is not used | +| DDLS | `Core%20Data%20Services/Data%20Definitions` | `.ddls.acds` | U2–U5, U8 | +| BDEF | `Core%20Data%20Services/Behavior%20Definitions` | `.bdef.abdl` | U6, U9. **Generator-era record (2026-09-15, not for this build):** `.bdef.json` activated a generator BDEF, and `.bdef.acds`, `.bdef.asbdef` and `.bdef.abap` failed with "array element 0 is null" | +| DDLX | `Core%20Data%20Services/Metadata%20Extensions` | `.ddlx.acds` | U12 | +| DCLS | `Core%20Data%20Services/Access%20Controls` | `.dcls.acds` | U10, U11 | +| CLAS | `Source%20Code%20Library/Classes` | `.clas.abap` | U7 (no Local Types path is returned) | +| SRVD | `Business%20Services/Service%20Definitions` | `.srvd.acds` | U13 (after the comment-free re-save) | +| SRVB | `Business%20Services/Service%20Bindings` | `.srvb.json` | U14 (returned by `create_object`, activated through it) | + +In step order. The U lines are the URIs of the active objects. The W lines follow the proven +pattern; confirm each against the returned `filePath`. ``` -B/Dictionary/Database%20Tables/ZZCHNGPPROVAL000/zzchngpproval000.tabl.json -B/Core%20Data%20Services/Data%20Definitions/ZD_CHANGEAPPROVAL_REJECTP/zd_changeapproval_rejectp.ddls.acds -B/Core%20Data%20Services/Data%20Definitions/ZD_CHANGEAPPROVAL_SYNCEDP/zd_changeapproval_syncedp.ddls.acds -B/Core%20Data%20Services/Data%20Definitions/ZD_CHANGEAPPROVAL_FAILEDP/zd_changeapproval_failedp.ddls.acds -B/Core%20Data%20Services/Data%20Definitions/ZZR_CHANGEAPPROVAL000/zzr_changeapproval000.ddls.acds -B/Core%20Data%20Services/Behavior%20Definitions/ZZR_CHANGEAPPROVAL000/zzr_changeapproval000.bdef.json -B/Source%20Code%20Library/Classes/ZZBP_R_CHANGEAPPROVAL000/zzbp_r_changeapproval000.clas.abap -B/Core%20Data%20Services/Data%20Definitions/ZZC_CHANGEAPPROVAL000/zzc_changeapproval000.ddls.acds -B/Core%20Data%20Services/Data%20Definitions/ZC_CHANGEAPPROVAL_A2X/zc_changeapproval_a2x.ddls.acds -B/Core%20Data%20Services/Behavior%20Definitions/ZZC_CHANGEAPPROVAL000/zzc_changeapproval000.bdef.json -B/Core%20Data%20Services/Behavior%20Definitions/ZC_CHANGEAPPROVAL_A2X/zc_changeapproval_a2x.bdef.json -B/Core%20Data%20Services/Access%20Controls/ZZR_CHANGEAPPROVAL000/zzr_changeapproval000.dcls.acds -B/Core%20Data%20Services/Access%20Controls/ZZC_CHANGEAPPROVAL000/zzc_changeapproval000.dcls.acds -B/Core%20Data%20Services/Access%20Controls/ZC_CHANGEAPPROVAL_A2X/zc_changeapproval_a2x.dcls.acds -B/Core%20Data%20Services/Metadata%20Extensions/ZZC_CHANGEAPPROVAL000/zzc_changeapproval000.ddlx.acds -B/Business%20Services/Service%20Definitions/ZZUI_ARENA_CHANGE_APPRVL000_O4/zzui_arena_change_apprvl000_o4.srvd.acds -B/Business%20Services/Service%20Definitions/ZAPI_ARENA_CHANGE_APPROVAL/zapi_arena_change_approval.srvd.acds -B/Business%20Services/Service%20Bindings/ZZUI_ARENA_CHANGE_APPRVL000_O4/zzui_arena_change_apprvl000_o4.srvb.json -B/Business%20Services/Service%20Bindings/ZAPI_ARENA_CHANGE_APPROVAL_A2X/zapi_arena_change_approval_a2x.srvb.json +U1 abap:/repotree-v1/0M7DEJI/System%20Library/ZLOCAL/ZARENA_PLM/Dictionary/Database%20Tables/ZCHANGEAPPROVAL/zchangeapproval.tabl.ddic +U2 abap:/repotree-v1/0M7DEJI/System%20Library/ZLOCAL/ZARENA_PLM/Core%20Data%20Services/Data%20Definitions/ZD_CHANGEAPPROVAL_REJECTP/zd_changeapproval_rejectp.ddls.acds +U3 abap:/repotree-v1/0M7DEJI/System%20Library/ZLOCAL/ZARENA_PLM/Core%20Data%20Services/Data%20Definitions/ZD_CHANGEAPPROVAL_SYNCEDP/zd_changeapproval_syncedp.ddls.acds +U4 abap:/repotree-v1/0M7DEJI/System%20Library/ZLOCAL/ZARENA_PLM/Core%20Data%20Services/Data%20Definitions/ZD_CHANGEAPPROVAL_FAILEDP/zd_changeapproval_failedp.ddls.acds +U5 abap:/repotree-v1/0M7DEJI/System%20Library/ZLOCAL/ZARENA_PLM/Core%20Data%20Services/Data%20Definitions/ZR_CHANGEAPPROVAL/zr_changeapproval.ddls.acds +U6 abap:/repotree-v1/0M7DEJI/System%20Library/ZLOCAL/ZARENA_PLM/Core%20Data%20Services/Behavior%20Definitions/ZR_CHANGEAPPROVAL/zr_changeapproval.bdef.abdl +U7 abap:/repotree-v1/0M7DEJI/System%20Library/ZLOCAL/ZARENA_PLM/Source%20Code%20Library/Classes/ZBP_R_CHANGEAPPROVAL/zbp_r_changeapproval.clas.abap +U8 abap:/repotree-v1/0M7DEJI/System%20Library/ZLOCAL/ZARENA_PLM/Core%20Data%20Services/Data%20Definitions/ZC_CHANGEAPPROVAL/zc_changeapproval.ddls.acds +U9 abap:/repotree-v1/0M7DEJI/System%20Library/ZLOCAL/ZARENA_PLM/Core%20Data%20Services/Behavior%20Definitions/ZC_CHANGEAPPROVAL/zc_changeapproval.bdef.abdl +U10 abap:/repotree-v1/0M7DEJI/System%20Library/ZLOCAL/ZARENA_PLM/Core%20Data%20Services/Access%20Controls/ZR_CHANGEAPPROVAL/zr_changeapproval.dcls.acds +U11 abap:/repotree-v1/0M7DEJI/System%20Library/ZLOCAL/ZARENA_PLM/Core%20Data%20Services/Access%20Controls/ZC_CHANGEAPPROVAL/zc_changeapproval.dcls.acds +U12 abap:/repotree-v1/0M7DEJI/System%20Library/ZLOCAL/ZARENA_PLM/Core%20Data%20Services/Metadata%20Extensions/ZC_CHANGEAPPROVAL/zc_changeapproval.ddlx.acds +U13 abap:/repotree-v1/0M7DEJI/System%20Library/ZLOCAL/ZARENA_PLM/Business%20Services/Service%20Definitions/ZUI_ARENA_CHANGE_APPROVAL/zui_arena_change_approval.srvd.acds +U14 abap:/repotree-v1/0M7DEJI/System%20Library/ZLOCAL/ZARENA_PLM/Business%20Services/Service%20Bindings/ZUI_ARENA_CHANGE_APPR_O4/zui_arena_change_appr_o4.srvb.json +W1 abap:/repotree-v1/0M7DEJI/System%20Library/ZLOCAL/ZARENA_PLM/Core%20Data%20Services/Data%20Definitions/ZC_CHANGEAPPROVAL_A2X/zc_changeapproval_a2x.ddls.acds +W2 abap:/repotree-v1/0M7DEJI/System%20Library/ZLOCAL/ZARENA_PLM/Core%20Data%20Services/Access%20Controls/ZC_CHANGEAPPROVAL_A2X/zc_changeapproval_a2x.dcls.acds +W3 abap:/repotree-v1/0M7DEJI/System%20Library/ZLOCAL/ZARENA_PLM/Core%20Data%20Services/Behavior%20Definitions/ZC_CHANGEAPPROVAL_A2X/zc_changeapproval_a2x.bdef.abdl +W4 abap:/repotree-v1/0M7DEJI/System%20Library/ZLOCAL/ZARENA_PLM/Business%20Services/Service%20Definitions/ZAPI_ARENA_CHANGE_APPROVAL/zapi_arena_change_approval.srvd.acds +W5 abap:/repotree-v1/0M7DEJI/System%20Library/ZLOCAL/ZARENA_PLM/Business%20Services/Service%20Bindings/ZAPI_ARENA_CHANGE_APPR_A2X/zapi_arena_change_appr_a2x.srvb.json ``` -Then re-run ATC on the package objects (`abap_atc_run`; omit RONT/NONT — the ATC API -answers "No valid data model exists for object type RONT/ROT") and -`abap_business_services-fetch_services` on both bindings. +## App contract check (source vs `app/webapp/localService/mainService/metadata.xml`, 2026-09-16) -## Design decisions baked into the files (falsifiable, dated 2026-09-15) +**Settled by the live `$metadata` (2026-09-16).** The table below is the pre-activation comparison, +kept as the record. The live capture `docs/v2/zui_arena_change_appr_o4.metadata.xml` (live +2026-09-16; read for this revision) settles every open row: -- **Two projections, two service definitions.** The brief asked for both bindings on the - generated service definition *and* for the UI to expose only approve/reject/retrySync - while the worker gets create + mark*. A service definition exposes one projection, and a - projection BDEF exposes exactly what it lists (no automatic inheritance - [source: abap-docs /abap-docs-cloud/ABENBDL_USE_PROJECTION]). Both requirements cannot - hold on one definition, so the A2X path got its own projection view/BDEF/definition and - the Web API binding sits on `ZAPI_ARENA_CHANGE_APPROVAL`. The RAP-2 "read-only from the - UI" requirement wins over the structural wording. -- **`authorization master ( none )`** instead of the generator's `( global )`: ADR-002 - makes the IAM app / business catalog the only access control and DCL governs reads; - `( none )` is the documented way to declare "no RAP authorization checks" - [source: abap-docs /abap-docs-cloud/ABENBDL_AUTHORIZATION]. Flip to `( global )` plus a - `FOR GLOBAL AUTHORIZATION` handler when a custom authorization object exists. -- **Derived fields are computed, not persisted.** `StatusText`, `StatusCriticality`, - `IsApprovable`, `IsRetriable` are CASE expressions in the root view; a transactional - query projection cannot hold expressions - [source: abap-docs /abap-docs-cloud/ABENCDS_DEFINE_VIEW_AS_PROJECTION]. They are - `field ( readonly )` and absent from the mapping — see step 4. -- **Action parameters are abstract entities** (`ZD_*`), one element each, because action - input must be an abstract entity or DDIC type - [source: abap-docs /abap-docs-cloud/ABENBDL_ACTION_INPUT_PARAM]. `markSynced` and - `markFailed` therefore take `SapChangeNumber` / `ErrorMessage` as parameters — the only - way an action can "set" them from the worker. -- **`internal update`** keeps `update` out of every OData surface while the actions and - the determination still update fields `IN LOCAL MODE` - [source: abap-docs /abap-docs-cloud/ABENBDL_INTERNAL]. -- **RequestedBy** is the technical user of the creator (the worker's communication user in - the A2X flow). Change to `mandatory : create` if the Arena requester should be shown. +- Namespace `com.sap.gateway.srvd.zui_arena_change_approval.v0001`. Entity type + `ChangeApprovalType`, not `ChangeApproval_Type`. Entity set `ChangeApproval`. +- `ID` `Edm.Guid`. `StatusText` `Edm.String` MaxLength 29. `StatusCriticality` `Edm.Byte`. + `IsApprovable`/`IsRetriable` `Edm.Boolean`. `RequestedOn`/`DecidedOn` `Edm.DateTimeOffset` + Precision 7. `SapPreview` `Edm.String` without MaxLength. +- `reject` parameter `RejectReason` `Edm.String` `Nullable="false"` MaxLength 255, the same as the app. +- Action enablement `Core.OperationAvailable Path="_it/__OperationControl/"` for `approve`, + `reject` and `retrySync`. +- Insertable, Updatable and Deletable are all `false`. -## Landmines learned live (KNOWLEDGE.md candidates) +The app was reconciled to this metadata on 2026-09-16, including backend-owned action enablement +(CHANGELOG). Rows below marked UNVERIFIED, "unknown until live" or "intended difference" are resolved +by this list. -- [2026-09-15] `x-ui-service` generator: every field needs `semanticType` (e.g. `custom`) - or validation fails with "Builtin type of field X is invalid"; semantic-key fields pass - without it. The generator prepends its own `Z` to the `prefix` (`Z` → `ZZ…`) and appends - `000` to every artifact name even with `suffix: ""`; service definition and UI binding - get the same name (`ZZUI_ARENA_CHANGE_APPRVL000_O4`). There is no delete tool in the ADT - MCP, so a naming mistake is permanent from the agent side. -- [2026-09-15] `create_object` for SRVD needs `sourceType` (`"S"` worked; omitted → +All 17 app properties exist in `ZC_CHANGEAPPROVAL` with the same names and case. The three actions +`approve`/`reject`/`retrySync` are in `ZC_CHANGEAPPROVAL` (projection BDEF), and +`ZUI_ARENA_CHANGE_APPROVAL` exposes exactly `ZC_CHANGEAPPROVAL as ChangeApproval`. `expose … as` +sets the entity set name and `_Type` [source: abap-docs /abap-fiori-showcase/06_object_page_content, +verified 2026-09-15] (live: `Type`, see above). The source is not changed for the open items below. Decisions sit with the +coordinator and are confirmed at U15 / V-2. + +| Item | App mock | Source | Status | +|---|---|---|---| +| Service URL / schema namespace | `manifest.json` dataSource `/sap/opu/odata4/sap//srvd/sap/zui_arena_change_approval/0001/`; namespace `com.sap.gateway.srvd..v0001` | binding `ZUI_ARENA_CHANGE_APPR_O4` (D-1): intended `/sap/opu/odata4/sap/zui_arena_change_appr_o4/srvd/sap/zui_arena_change_approval/0001/` | **intended difference**; the app's dataSource URL, mock namespace and annotation targets are reconciled at V-2 from the live `$metadata` (separate Fiori brief) | +| Entity set / entity type / key | `ChangeApproval` / `ChangeApproval_Type` / `ID` | `expose ZC_CHANGEAPPROVAL as ChangeApproval` | match | +| String lengths (ItemNumber 18, ItemName 40, ChangeType 15, Status 10, ArenaEcoId 20, RequestedBy/DecidedBy 12, SapChangeNumber 12, ErrorMessage/RejectReason 255) | as listed | same CHAR lengths in `ZCHANGEAPPROVAL` | match | +| `SapPreview` | Edm.String, no MaxLength | `abap.string(0)` (RAP-7) | match expected | +| `StatusText` | absent | CASE result, CHAR(29) by the max-type rule [source: abap-docs /abap-docs-cloud/ABENCDS_CASE_EXPRESSION_V2, verified 2026-09-16]; RAP-3 text element | extra; keep | +| `LocalLastChangedAt` | absent | etag field for `use etag` | extra; keep | +| `StatusCriticality` | Edm.Byte | integer literals typed INT1..INT8 by value [source: abap-docs /abap-docs-cloud/ABENCDS_UNTYPED_LITERAL_V2, verified 2026-09-16] | INT1 → Edm.Byte UNVERIFIED | +| `IsApprovable`, `IsRetriable` | Edm.Boolean | `cast(… as abap_boolean)` | ABAP_BOOLEAN → Edm.Boolean UNVERIFIED | +| `RequestedOn`, `DecidedOn` | Edm.DateTimeOffset Precision 7 | `abap.utclong` | UTCLONG mapping UNVERIFIED | +| `ID` | Edm.Guid, `Core.Computed` | `sysuuid_x16`, `readonly, numbering : managed` | expected, UNVERIFIED | +| `reject` parameter `RejectReason` | Nullable="false", MaxLength 255 | `ZD_CHANGEAPPROVAL_REJECTP` CHAR(255), not declared mandatory; the handler rejects an empty reason | differs in metadata, behavior meets RAP-2 | +| Action enablement | `Core.OperationAvailable Path="IsApprovable"/"IsRetriable"` (mock only) | RAP generates it from `( features : instance )` | path form unknown until live | +| Insert/Update/Delete restrictions | all false | UI projection BDEF exposes no create/update/delete | expected | + +## Design decisions baked into the files (falsifiable, dated 2026-09-15 unless noted) + +- **State machine** (PRD ARN-002; `retrySync` target decided 2026-09-16): + + | Action | From | To | Caller | + |---|---|---|---| + | create (determination `setInitialStatus`) | none | PENDING | worker (A2X) | + | `approve` | PENDING | APPROVED | business user (UI) | + | `reject(RejectReason)` | PENDING | REJECTED (terminal) | business user (UI) | + | `markExecuting` | APPROVED | EXECUTING | worker (A2X) | + | `markSynced(SapChangeNumber)` | EXECUTING | SYNCED (terminal) | worker (A2X) | + | `markFailed(ErrorMessage)` | EXECUTING | FAILED | worker (A2X) | + | `retrySync` | FAILED | **APPROVED** (clears `ErrorMessage`, keeps `DecidedBy`/`DecidedOn`) | business user (UI) | + + Only `markExecuting` writes EXECUTING. A retried record goes back through `markExecuting`, so the + worker re-claims it the same way as a first approval. Feature control is unchanged: `retrySync` is + enabled only for FAILED. Each action re-checks the status in the handler as well. +- **RAP action implementation contract** (2026-09-16). Every action handler returns failed rows + with `%fail-cause` (not_found or unspecific; `locked` is mapped to unspecific) and + `%op-%action-`. Reported messages carry `%op-%action-`. Failures and messages of the + internal UPDATE are merged into `failed`/`reported`, and `result` holds only keys that did not + fail [source: sap-docs sap-help loio de6569d4b92e40a0911c926170140beb (Action contract), + 1040b81372d44ed38b07a409fa0e1769 (General contract), abaprap_impl_rules (allowed fail causes), + verified 2026-09-16]. Under `strict(2)`, contract violations are runtime errors [source: abap-docs + /abap-docs-cloud/ABENBDL_STRICT_2, verified 2026-09-16]. +- **Semantic key enforced on create** (2026-09-16). `validation checkDuplicate on save { create; }` + fails a create when another record with the same `ItemNumber` + `ChangeType` + `ArenaEcoId` + exists and is not REJECTED, or when the same request contains that key twice. Validation syntax, + handler signature and effect (the whole transactional buffer is rejected) [source: abap-docs + /abap-docs-cloud/ABENBDL_VALIDATIONS, ABAPHANDLER_METH_VALIDATE, verified 2026-09-16]. + - Fail cause: validations may only use `cause-unspecific`. + - Messages are state messages with `%state_area` and `%element`. + - Persisted rows are read from `ZR_CHANGEAPPROVAL` with `OPTIONS PRIVILEGED ACCESS`, because a + handler SELECT on an access-controlled view without it violates the RAP BO contract [source: + sap-docs sap-help abaprap_impl_rules; abap-docs /abap-docs-cloud/ABAPSELECT_PRIVILEGED_ACCESS, + ABAPDERIVED_TYPES_STATE_AREA, verified 2026-09-16]. + - A validation is not something a projection lists [source: abap-docs + /abap-docs-cloud/ABENBDL_USE_PROJECTION, verified 2026-09-16], so both projections are unchanged. +- **`use etag` in the entity header** (2026-09-16). In both projection BDEFs `use etag` sits between + `define behavior for … alias ChangeApproval` and `{`, without a semicolon. It is an entity behavior + characteristic, not a body statement [source: abap-docs /abap-docs-cloud/ABENBDL_CHARACTER_PROJECTION, + ABENBDL_DEFINE_BEH_PROJECTION, verified 2026-09-16]. +- **Two projections, two service definitions.** The UI projection `ZC_CHANGEAPPROVAL` exposes only + approve/reject/retrySync on `ZUI_ARENA_CHANGE_APPROVAL`. The worker's `ZC_CHANGEAPPROVAL_A2X` + exposes create + markExecuting/markSynced/markFailed on `ZAPI_ARENA_CHANGE_APPROVAL`. A projection + BDEF exposes exactly what it lists, with no automatic inheritance [source: abap-docs + /abap-docs-cloud/ABENBDL_USE_PROJECTION]. PRD RAP-5's wording (two bindings on one definition) is + superseded by this design and is a governance item. (Closed: PRD RAP-5 was amended to this + design on 2026-09-16; PRD read 2026-09-16.) +- **No projection behavior pool** (2026-09-16: never created). A projection BDEF needs one only for + new actions/functions [source: abap-docs /abap-docs-cloud/ABENBDL_BDEF_PROJECTION_HEADER]. +- **`authorization master ( none )`**: ADR-002 makes the IAM app / business catalog the only access + control, and DCL governs reads [source: abap-docs /abap-docs-cloud/ABENBDL_AUTHORIZATION]. Switch + to `( global )` plus a `FOR GLOBAL AUTHORIZATION` handler when a custom authorization object + exists. **Not final:** `( none )` versus global authorization is open question PRD OQ-8, to be + decided before business roles are assigned. +- **Derived fields are computed, not persisted.** `StatusText`, `StatusCriticality`, `IsApprovable` + and `IsRetriable` are CASE expressions in `ZR_CHANGEAPPROVAL`, because a transactional query + projection cannot hold expressions [source: abap-docs + /abap-docs-cloud/ABENCDS_DEFINE_VIEW_AS_PROJECTION]. They are `field ( readonly )` and absent from + the mapping (see U6). +- **Action parameters are abstract entities** (`ZD_*`, one element each), because action input must + be an abstract entity or DDIC type [source: abap-docs /abap-docs-cloud/ABENBDL_ACTION_INPUT_PARAM]. +- **`internal update`** keeps `update` off every OData surface. Actions and the determination still + update fields `IN LOCAL MODE` [source: abap-docs /abap-docs-cloud/ABENBDL_INTERNAL]. +- **RequestedBy** holds the technical user of the creator (the worker's communication user in the + A2X flow). Change it to `mandatory : create` if the Arena requester should be shown. +- **No SAP object type** (2026-09-16), see the inventory section. +- **Released objects used** (all level A, public_cloud): `SYSUUID_X16`, `ABP_CREATION_USER`, + `ABP_CREATION_TSTMPL`, `ABP_LASTCHANGE_USER`, `ABP_LASTCHANGE_TSTMPL`, + `ABP_LOCINST_LASTCHANGE_TSTMPL`, `ABAP_BOOLEAN`, `CL_ABAP_BEHAVIOR_HANDLER`, `CL_ABAP_CONTEXT_INFO`, + `IF_ABAP_BEHV`, `IF_ABAP_BEHV_MESSAGE` [source: sap-released-objects + sap_check_clean_core_compliance, verified 2026-09-16]. + +## Landmines (live-learned, dated) + +- [2026-09-15] `create_object` for SRVD needs `sourceType` (`"S"` worked; omitting it fails with "serviceDefinitionInfo.sourceType is null"). BDEF/SRVB creation validates the referenced - DDLS/SRVD **active** version, so source-less objects block dependent creations. -- [2026-09-15] Activation URIs: see table above; BDEF is `.bdef.json`, table `.tabl.json`, - SAP object types live under `Business Services`. -- [2026-09-15] The MCP session id from `get_schema` `referenceContent` is single-use in - practice — fetch a fresh one before each `generate_objects` attempt. + DDLS/SRVD's **active** version, so source-less objects block dependent creations. +- [2026-09-15, **superseded 2026-09-16**] Activation URIs recorded on 2026-09-15 came from + generator objects: BDEF `.bdef.json` and TABL `.tabl.json` (both generator-era). They are wrong for + objects created with `create_object`; see the per-type entry below. +- [2026-09-16] **Per-type file extensions: use the `filePath` returned by `create_object`.** Returned + and proven by activation: TABL `.tabl.ddic` (U1), DDLS `.ddls.acds`, DCLS `.dcls.acds`, DDLX + `.ddlx.acds`, BDEF **`.bdef.abdl`** (U6, U9), CLAS `.clas.abap` (global class only; no Local Types + path is returned), SRVD `.srvd.acds` (U13), SRVB **`.srvb.json`** (U14) [source: adt-mcp + abap_creation-create_object / abap_activate_objects on 0M7DEJI, verified 2026-09-16]. +- [2026-09-16] A `.json` file in an object folder of the VS Code ADT workspace is metadata only + (`zchangeapproval.tabl.json`: formatVersion, description, originalLanguage, + `abapLanguageVersion: cloudDevelopment`). Never paste source into it. The table source goes into + `.tabl.ddic` (live observation 2026-09-16). The binding's `.srvb.json` is the exception by type: + it is the returned `filePath`, nothing is pasted into it, and activation and publish go through + it (U14, U15). +- [2026-09-16] **Service definition sources must carry no comments.** A leading `/* … */` header made + U13's activation fail with "Comments are not supported and will be deleted on save" (Ln 1), and the + same definition without it activated (live 2026-09-16). The DCLS, DDLX and BDEF headers did not + block. CDS SDL follows the general CDS syntax rules, which allow comments [source: abap-docs + /abap-docs-cloud/ABENCDS_F1_SDL_SYNTAX, ABENCDS_GENERAL_SYNTAX_RULES, verified 2026-09-16], so this + is the tooling, not the language. Both SRVD files in this folder are comment-free; keep them so. +- [2026-09-16] **`abap_activate_objects` output is thin.** Success returns only "Activation + successful.", with no warnings. Failure lists messages with line and column but no severity, so read + severity in the editor [source: adt-mcp abap_activate_objects on 0M7DEJI, verified 2026-09-16]. +- [2026-09-16] **Publishing a binding in ADT for VS Code is a code lens.** Open the binding's + `.srvb.json` source and choose the **Publish** code lens; Unpublish and Preview… work the same way + [source: sap-docs sap-help loio dd6f5de63e7245a7abe7e39389841596 "ABAP RAP Tools", verified + 2026-09-16]. The extension contributes no publish command, the SAP ADT MCP has no publish tool, and + the only visible feedback is " published locally". Verify with `fetch_services` + (`isPublished: true`) (KNOWLEDGE 2026-09-16; U14 published 2026-09-16 04:26). +- [2026-09-16] **UI host vs `-api` host.** `fetch_service_information` reports the service URL on + `my431353-api.s4hana.cloud.sap` with `sap-client=080` [source: adt-mcp + abap_business_services-fetch_service_information on 0M7DEJI, verified 2026-09-16]. A business user + calling that host from the browser is refused as "Forbidden / blocked by UCON". The same path on + `my431353.s4hana.cloud.sap` returns `$metadata` (live 2026-09-16; KNOWLEDGE 2026-09-16). The UI + service and the Fiori app use the UI host. The `-api` host is the communication-user entry point, + so the worker's Web API (W5) is reached there only through a custom communication scenario (PRD + V-2), never through the UI service. +- [2026-09-15] The ADT MCP has no delete tool and no package tool. A wrong name is permanent from + the agent side, and a missing package must be created in ADT. +- [2026-09-15] ATC via MCP rejects RONT/NONT ("No valid data model exists for object type + RONT/ROT"). Not relevant here because no SAP object type is created. +- [2026-09-16] A missing package shows up read-only as "Package '' does not exist" from + `abap_creation-run_validation` [source: adt-mcp on 0M7DEJI, verified 2026-09-16]. +- [2026-09-16] Service binding name limit: SAP Help says 26 in every version checked, while the + ADT MCP validation accepts 30 (undocumented). The 26 limit is used here (D-1, resolved). U14 (24 + characters) published on 2026-09-16. +- [2026-09-16] The V4 URL's second segment is the binding's service alias, renamable only while + unpublished (U14). For U14 it was verified after activation with `fetch_services`, before + publishing; opening the binding before activation is not needed. U14 is now published, so its + alias is fixed. diff --git a/docs/handover/rap/zapi_arena_change_approval.srvd.srvdsrv b/docs/handover/rap/zapi_arena_change_approval.srvd.srvdsrv index bf86e82..957a976 100644 --- a/docs/handover/rap/zapi_arena_change_approval.srvd.srvdsrv +++ b/docs/handover/rap/zapi_arena_change_approval.srvd.srvdsrv @@ -1,7 +1,3 @@ -/* TARGET OBJECT : service definition ZAPI_ARENA_CHANGE_APPROVAL (created empty via ADT MCP 2026-09-15) - abap: URI : abap:/repotree-v1/0M7DEJI/System%20Library/ZLOCAL/ZARENA_PLM/Business%20Services/Service%20Definitions/ZAPI_ARENA_CHANGE_APPROVAL/zapi_arena_change_approval.srvd.acds - ACTION : replace entire content, then activate (after ZC_CHANGEAPPROVAL_A2X view + BDEF). - Sources: as ZZUI_ARENA_CHANGE_APPRVL000_O4; odata_v4_webapi is the Web API contract [ABENSRVD_PROVIDER_CONTRACT]. */ @EndUserText.label: 'Arena change approval - Web API (worker)' define service ZAPI_ARENA_CHANGE_APPROVAL provider contracts odata_v4_webapi diff --git a/docs/handover/rap/zbp_r_changeapproval.clas.abap b/docs/handover/rap/zbp_r_changeapproval.clas.abap new file mode 100644 index 0000000..b7b2cc3 --- /dev/null +++ b/docs/handover/rap/zbp_r_changeapproval.clas.abap @@ -0,0 +1,16 @@ +" TARGET OBJECT : class ZBP_R_CHANGEAPPROVAL - GLOBAL CLASS include (behavior pool) - README step U7, UI leg +" abap: URI : abap:/repotree-v1/0M7DEJI/System%20Library/ZLOCAL/ZARENA_PLM/Source%20Code%20Library/Classes/ZBP_R_CHANGEAPPROVAL/zbp_r_changeapproval.clas.abap +" ACTION : after BDEF ZR_CHANGEAPPROVAL is active (README U6), create empty via ADT MCP (objectType CLAS/OC, +" README U7), replace entire content of the global class; the handlers go into "Local Types" +" (locals_imp file). Save both, then activate the class. +" Source: FOR BEHAVIOR OF defines the ABAP behavior pool; the global class implements nothing itself +" [source: abap-docs /abap-docs-cloud/ABAPCLASS_FOR_BEHAVIOR_OF, ABENABAP_BEHAVIOR_POOLS, verified 2026-09-15]. +CLASS zbp_r_changeapproval DEFINITION + PUBLIC + ABSTRACT + FINAL + FOR BEHAVIOR OF zr_changeapproval. +ENDCLASS. + +CLASS zbp_r_changeapproval IMPLEMENTATION. +ENDCLASS. diff --git a/docs/handover/rap/zbp_r_changeapproval.clas.locals_imp.abap b/docs/handover/rap/zbp_r_changeapproval.clas.locals_imp.abap new file mode 100644 index 0000000..c57f065 --- /dev/null +++ b/docs/handover/rap/zbp_r_changeapproval.clas.locals_imp.abap @@ -0,0 +1,765 @@ +" TARGET OBJECT : class ZBP_R_CHANGEAPPROVAL - LOCAL TYPES include (Local Types tab in ADT) - README step U7, UI leg +" abap: URI : abap:/repotree-v1/0M7DEJI/System%20Library/ZLOCAL/ZARENA_PLM/Source%20Code%20Library/Classes/ZBP_R_CHANGEAPPROVAL/zbp_r_changeapproval.clas.locals_imp.abap +" ACTION : replace entire content, save, then activate the class (README U7; BDEF ZR_CHANGEAPPROVAL must be +" active first). Activation goes through the class URI (.clas.abap), not this include. +" +" Sources (verified 2026-09-15 unless dated otherwise): +" handler class inherits from CL_ABAP_BEHAVIOR_HANDLER (released) [sap-released-objects public_cloud A, verified 2026-09-16] +" FOR INSTANCE FEATURES signature + result via %tky / %features-%action- / if_abap_behv=>fc-o-enabled|disabled +" [abap-docs /abap-docs-cloud/ABAPHANDLER_METH_FEATURES, ABAPDERIVED_TYPES_FEATURES, ABENBDL_ACTIONS_FC] +" FOR MODIFY ... FOR ACTION with RESULT; %param carries the action parameter (keys) and the result ($self) +" [abap-docs /abap-docs-cloud/ABENBDL_ACTION2_ABEXA, ABAPDERIVED_TYPES_PARAM] +" READ ENTITIES ... IN LOCAL MODE ... FIELDS ( ) WITH CORRESPONDING #( keys ) RESULT +" [abap-docs /abap-docs-cloud/ABENBDL_ACTIONS_FC example] +" MODIFY ENTITIES ... IN LOCAL MODE (skips feature control and authorization) with UPDATE FIELDS ( ) WITH +" [abap-docs /abap-docs-cloud/ABAPMODIFY_ENTITY_ENTITIES, ABAPMODIFY_ENTITY_ENTITIES_OP, ABENBDL_AUTHORIZATION; +" sap-docs sap-help loio e75275489fe648f6b00977be76f0bc06 (deductDiscount: collect valid keys, update, read, result), verified 2026-09-16] +" FOR DETERMINE ON MODIFY signature [abap-docs /abap-docs-cloud/ABAPHANDLER_METH_DET] +" utclong_current( ) [abap-docs /abap-docs-cloud/ABENUTCLONG_CURRENT] +" cl_abap_context_info=>get_user_technical_name( ) (released class) [sap-docs sap-help loio 7be24435b3e24540a9edad9a2535175a; +" sap-released-objects CL_ABAP_CONTEXT_INFO public_cloud A, verified 2026-09-16] +" if_abap_behv / if_abap_behv_message are released [sap-released-objects INTF public_cloud A, verified 2026-09-16] +" RAP action implementation contract (verified 2026-09-16): +" - an unsuccessful action returns one row per failed input identifier in failed "with the correct fail cause and +" flags the %action component on the correct action"; for every failed identifier with a cause other than +" not_found there is one error message in reported [sap-docs sap-help loio de6569d4b92e40a0911c926170140beb] +" - for fail causes other than not_found/locked the failed operation in %op must be provided; reported always fills +" %op; result contains only input identifiers not in failed, without duplicates +" [sap-docs sap-help loio 1040b81372d44ed38b07a409fa0e1769] +" - %op groups the operation flags incl. %action [abap-docs /abap-docs-cloud/ABAPDERIVED_TYPES_OP]; %fail-cause values +" are defined in IF_ABAP_BEHV [abap-docs /abap-docs-cloud/ABAPDERIVED_TYPES_FAIL]; %op-%action- = mk-on in +" reported [sap-docs loio e75275489fe648f6b00977be76f0bc06] +" - allowed fail causes: actions any cause except if_abap_behv=>cause-locked; validations on save only +" if_abap_behv=>cause-unspecific; with a non-initial %state_area only %element may be flagged in %op +" [sap-docs sap-help abaprap_impl_rules "RAP Implementation Rules"] +" - strict(2) turns RAP BO contract violations into runtime errors [abap-docs /abap-docs-cloud/ABENBDL_STRICT_2] +" - merging a MODIFY response: CORRESPONDING #( DEEP APPENDING BASE ( reported ) ... ). DEEP alone replaces a nested +" table such as reported-changeapproval and would drop the messages appended earlier in the handler; DEEP APPENDING +" BASE "ensures that the nested target tables are not deleted" and "appends the lines of the nested source tables" +" [abap-docs /abap-docs-cloud/ABENCORRESPONDING_CONSTR_ARG_TYPE, ABENNEWS-783-ASSIGNMENTS, verified 2026-09-16] +" Validation (verified 2026-09-16): +" - METHODS meth FOR VALIDATE ON SAVE IMPORTING keys FOR bdef~val; failed/reported implicit +" [abap-docs /abap-docs-cloud/ABAPHANDLER_METH_VALIDATE]; no EML MODIFY in validations; a failing validation +" rejects the whole transactional buffer [abap-docs /abap-docs-cloud/ABENBDL_VALIDATIONS] +" - state message pattern: invalidate %tky + %state_area first, then fail + message with %state_area and %element +" [abap-docs /abap-docs-cloud/ABAPDERIVED_TYPES_STATE_AREA, ABENBDL_VALIDATIONS example] +" - a SELECT on an access-controlled CDS view without OPTIONS PRIVILEGED ACCESS inside a RAP handler is a RAP BO +" contract violation [sap-docs sap-help abaprap_impl_rules "Non-privileged access"]; PRIVILEGED ACCESS disables CDS +" access control for the SELECT, OPTIONS goes at the end of the statement +" [abap-docs /abap-docs-cloud/ABAPSELECT_PRIVILEGED_ACCESS, ABAPSELECT_OPTIONS] +" Design notes: +" - State machine (PRD ARN-002; retrySync decision 2026-09-16): create -> PENDING; approve PENDING -> APPROVED; +" reject PENDING -> REJECTED (terminal); markExecuting APPROVED -> EXECUTING; markSynced EXECUTING -> SYNCED +" (terminal); markFailed EXECUTING -> FAILED; retrySync FAILED -> APPROVED. Only markExecuting writes EXECUTING, so a +" retried record is re-claimed by the worker through markExecuting and cannot be executed twice in parallel. +" - Status transitions are enforced twice: get_instance_features disables the action (UI greys it out / EML fails +" with an entry in failed [ABENBDL_ACTIONS_FC]) AND each action re-checks the current status so an EML caller +" IN LOCAL MODE cannot skip the state machine. +" - The six actions repeat the same read/check/update/read pattern on purpose: each action flags its own +" %op-%action- component, so a generic helper would need additional derived-type plumbing. +" - A failed UPDATE with cause locked is reported as unspecific, because locked is not an allowed cause for actions. +" - RequestedBy is the technical user of the creator (for the A2X flow: the worker's communication user). +" If the Arena requester should be shown instead, make RequestedBy mandatory:create in the BDEF and drop it +" from the determination. +" - checkDuplicate: the SELECT only sees persisted rows (a validation runs in the save sequence before the new +" instances are written), so duplicates inside the same request are checked separately on the buffer data. +" UNVERIFIED 2026-09-16: the list of SELECT additions that cannot be combined with PRIVILEGED ACCESS was not +" retrievable; this uses SELECT SINGLE without FOR ALL ENTRIES. The syntax check at U7 activation is the proof. +" abaplint (abap-docs abap_lint, 2026-09-16) does not parse `OPTIONS PRIVILEGED ACCESS` but parses the FROM-clause +" form. If the tenant syntax check refuses the OPTIONS form, use the equally documented +" `SELECT SINGLE FROM zr_changeapproval WITH PRIVILEGED ACCESS FIELDS ID WHERE ... INTO @DATA(existing_id).` +" (affects only that entity; not combinable with OPTIONS PRIVILEGED ACCESS) +" [source: abap-docs /abap-docs-cloud/ABAPSELECT_DATA_SOURCE, verified 2026-09-16]. + +CLASS lhc_changeapproval DEFINITION INHERITING FROM cl_abap_behavior_handler. + PRIVATE SECTION. + + CONSTANTS: + BEGIN OF status, + pending TYPE zchangeapproval-status VALUE 'PENDING', + approved TYPE zchangeapproval-status VALUE 'APPROVED', + executing TYPE zchangeapproval-status VALUE 'EXECUTING', + synced TYPE zchangeapproval-status VALUE 'SYNCED', + rejected TYPE zchangeapproval-status VALUE 'REJECTED', + failed TYPE zchangeapproval-status VALUE 'FAILED', + END OF status. + + CONSTANTS state_area_duplicate TYPE string VALUE `CHECK_DUPLICATE`. + + METHODS get_instance_features FOR INSTANCE FEATURES + IMPORTING keys REQUEST requested_features FOR ChangeApproval RESULT result. + + METHODS approve FOR MODIFY + IMPORTING keys FOR ACTION ChangeApproval~approve RESULT result. + + METHODS reject FOR MODIFY + IMPORTING keys FOR ACTION ChangeApproval~reject RESULT result. + + METHODS retrySync FOR MODIFY + IMPORTING keys FOR ACTION ChangeApproval~retrySync RESULT result. + + METHODS markExecuting FOR MODIFY + IMPORTING keys FOR ACTION ChangeApproval~markExecuting RESULT result. + + METHODS markSynced FOR MODIFY + IMPORTING keys FOR ACTION ChangeApproval~markSynced RESULT result. + + METHODS markFailed FOR MODIFY + IMPORTING keys FOR ACTION ChangeApproval~markFailed RESULT result. + + METHODS setInitialStatus FOR DETERMINE ON MODIFY + IMPORTING keys FOR ChangeApproval~setInitialStatus. + + METHODS checkDuplicate FOR VALIDATE ON SAVE + IMPORTING keys FOR ChangeApproval~checkDuplicate. + +ENDCLASS. + + +CLASS lhc_changeapproval IMPLEMENTATION. + + METHOD get_instance_features. + READ ENTITIES OF zr_changeapproval IN LOCAL MODE + ENTITY ChangeApproval + FIELDS ( Status ) + WITH CORRESPONDING #( keys ) + RESULT DATA(approvals) + FAILED failed. + + result = VALUE #( FOR approval IN approvals + ( %tky = approval-%tky + %features-%action-approve = COND #( WHEN approval-Status = status-pending + THEN if_abap_behv=>fc-o-enabled + ELSE if_abap_behv=>fc-o-disabled ) + %features-%action-reject = COND #( WHEN approval-Status = status-pending + THEN if_abap_behv=>fc-o-enabled + ELSE if_abap_behv=>fc-o-disabled ) + %features-%action-retrySync = COND #( WHEN approval-Status = status-failed + THEN if_abap_behv=>fc-o-enabled + ELSE if_abap_behv=>fc-o-disabled ) + %features-%action-markExecuting = COND #( WHEN approval-Status = status-approved + THEN if_abap_behv=>fc-o-enabled + ELSE if_abap_behv=>fc-o-disabled ) + %features-%action-markSynced = COND #( WHEN approval-Status = status-executing + THEN if_abap_behv=>fc-o-enabled + ELSE if_abap_behv=>fc-o-disabled ) + %features-%action-markFailed = COND #( WHEN approval-Status = status-executing + THEN if_abap_behv=>fc-o-enabled + ELSE if_abap_behv=>fc-o-disabled ) ) ). + ENDMETHOD. + + + METHOD approve. + " PENDING -> APPROVED; records the decision, never writes SapChangeNumber (PRD RAP-1) + DATA approvals_for_update TYPE TABLE FOR UPDATE zr_changeapproval. + + READ ENTITIES OF zr_changeapproval IN LOCAL MODE + ENTITY ChangeApproval + FIELDS ( Status ) + WITH CORRESPONDING #( keys ) + RESULT DATA(approvals). + + LOOP AT keys INTO DATA(key). + ASSIGN approvals[ ID = key-ID ] TO FIELD-SYMBOL(). + IF sy-subrc <> 0. + APPEND VALUE #( %tky = key-%tky + %fail-cause = if_abap_behv=>cause-not_found + %op-%action-approve = if_abap_behv=>mk-on ) TO failed-changeapproval. + CONTINUE. + ENDIF. + + IF -Status <> status-pending. + APPEND VALUE #( %tky = key-%tky + %fail-cause = if_abap_behv=>cause-unspecific + %op-%action-approve = if_abap_behv=>mk-on ) TO failed-changeapproval. + APPEND VALUE #( %tky = key-%tky + %op-%action-approve = if_abap_behv=>mk-on + %msg = new_message_with_text( + severity = if_abap_behv_message=>severity-error + text = |Only PENDING records can be approved (status is { -Status })| ) ) + TO reported-changeapproval. + CONTINUE. + ENDIF. + + APPEND VALUE #( %tky = key-%tky + Status = status-approved + DecidedBy = cl_abap_context_info=>get_user_technical_name( ) + DecidedOn = utclong_current( ) ) TO approvals_for_update. + ENDLOOP. + + IF approvals_for_update IS NOT INITIAL. + MODIFY ENTITIES OF zr_changeapproval IN LOCAL MODE + ENTITY ChangeApproval + UPDATE FIELDS ( Status DecidedBy DecidedOn ) + WITH approvals_for_update + FAILED DATA(update_failed) + REPORTED DATA(update_reported). + + " Propagate the internal UPDATE response into the action response + LOOP AT update_reported-changeapproval ASSIGNING FIELD-SYMBOL(). + -%op-%action-approve = if_abap_behv=>mk-on. + ENDLOOP. + reported = CORRESPONDING #( DEEP APPENDING BASE ( reported ) update_reported ). + + LOOP AT update_failed-changeapproval INTO DATA(update_failure). + APPEND VALUE #( %tky = update_failure-%tky + %fail-cause = COND #( WHEN update_failure-%fail-cause = if_abap_behv=>cause-locked + THEN if_abap_behv=>cause-unspecific + ELSE update_failure-%fail-cause ) + %op-%action-approve = if_abap_behv=>mk-on ) TO failed-changeapproval. + IF update_failure-%fail-cause <> if_abap_behv=>cause-not_found + AND NOT line_exists( update_reported-changeapproval[ ID = update_failure-ID ] ). + APPEND VALUE #( %tky = update_failure-%tky + %op-%action-approve = if_abap_behv=>mk-on + %msg = new_message_with_text( + severity = if_abap_behv_message=>severity-error + text = 'The approval could not be recorded' ) ) + TO reported-changeapproval. + ENDIF. + ENDLOOP. + ENDIF. + + READ ENTITIES OF zr_changeapproval IN LOCAL MODE + ENTITY ChangeApproval + ALL FIELDS WITH CORRESPONDING #( keys ) + RESULT DATA(updated). + + LOOP AT updated INTO DATA(row). + IF NOT line_exists( failed-changeapproval[ ID = row-ID ] ). + APPEND VALUE #( %tky = row-%tky %param = row ) TO result. + ENDIF. + ENDLOOP. + ENDMETHOD. + + + METHOD reject. + " PENDING -> REJECTED with a mandatory reason (PRD RAP-2); REJECTED is terminal + DATA approvals_for_update TYPE TABLE FOR UPDATE zr_changeapproval. + + READ ENTITIES OF zr_changeapproval IN LOCAL MODE + ENTITY ChangeApproval + FIELDS ( Status ) + WITH CORRESPONDING #( keys ) + RESULT DATA(approvals). + + LOOP AT keys INTO DATA(key). + ASSIGN approvals[ ID = key-ID ] TO FIELD-SYMBOL(). + IF sy-subrc <> 0. + APPEND VALUE #( %tky = key-%tky + %fail-cause = if_abap_behv=>cause-not_found + %op-%action-reject = if_abap_behv=>mk-on ) TO failed-changeapproval. + CONTINUE. + ENDIF. + + IF -Status <> status-pending. + APPEND VALUE #( %tky = key-%tky + %fail-cause = if_abap_behv=>cause-unspecific + %op-%action-reject = if_abap_behv=>mk-on ) TO failed-changeapproval. + APPEND VALUE #( %tky = key-%tky + %op-%action-reject = if_abap_behv=>mk-on + %msg = new_message_with_text( + severity = if_abap_behv_message=>severity-error + text = |Only PENDING records can be rejected (status is { -Status })| ) ) + TO reported-changeapproval. + CONTINUE. + ENDIF. + + IF key-%param-RejectReason IS INITIAL. + APPEND VALUE #( %tky = key-%tky + %fail-cause = if_abap_behv=>cause-unspecific + %op-%action-reject = if_abap_behv=>mk-on ) TO failed-changeapproval. + APPEND VALUE #( %tky = key-%tky + %op-%action-reject = if_abap_behv=>mk-on + %msg = new_message_with_text( + severity = if_abap_behv_message=>severity-error + text = 'A reject reason is required' ) ) + TO reported-changeapproval. + CONTINUE. + ENDIF. + + APPEND VALUE #( %tky = key-%tky + Status = status-rejected + RejectReason = key-%param-RejectReason + DecidedBy = cl_abap_context_info=>get_user_technical_name( ) + DecidedOn = utclong_current( ) ) TO approvals_for_update. + ENDLOOP. + + IF approvals_for_update IS NOT INITIAL. + MODIFY ENTITIES OF zr_changeapproval IN LOCAL MODE + ENTITY ChangeApproval + UPDATE FIELDS ( Status RejectReason DecidedBy DecidedOn ) + WITH approvals_for_update + FAILED DATA(update_failed) + REPORTED DATA(update_reported). + + LOOP AT update_reported-changeapproval ASSIGNING FIELD-SYMBOL(). + -%op-%action-reject = if_abap_behv=>mk-on. + ENDLOOP. + reported = CORRESPONDING #( DEEP APPENDING BASE ( reported ) update_reported ). + + LOOP AT update_failed-changeapproval INTO DATA(update_failure). + APPEND VALUE #( %tky = update_failure-%tky + %fail-cause = COND #( WHEN update_failure-%fail-cause = if_abap_behv=>cause-locked + THEN if_abap_behv=>cause-unspecific + ELSE update_failure-%fail-cause ) + %op-%action-reject = if_abap_behv=>mk-on ) TO failed-changeapproval. + IF update_failure-%fail-cause <> if_abap_behv=>cause-not_found + AND NOT line_exists( update_reported-changeapproval[ ID = update_failure-ID ] ). + APPEND VALUE #( %tky = update_failure-%tky + %op-%action-reject = if_abap_behv=>mk-on + %msg = new_message_with_text( + severity = if_abap_behv_message=>severity-error + text = 'The rejection could not be recorded' ) ) + TO reported-changeapproval. + ENDIF. + ENDLOOP. + ENDIF. + + READ ENTITIES OF zr_changeapproval IN LOCAL MODE + ENTITY ChangeApproval + ALL FIELDS WITH CORRESPONDING #( keys ) + RESULT DATA(updated). + + LOOP AT updated INTO DATA(row). + IF NOT line_exists( failed-changeapproval[ ID = row-ID ] ). + APPEND VALUE #( %tky = row-%tky %param = row ) TO result. + ENDIF. + ENDLOOP. + ENDMETHOD. + + + METHOD retrySync. + " FAILED -> APPROVED (decision 2026-09-16): clears the previous error and keeps DecidedBy/DecidedOn of the original + " approval. The worker re-claims the record through markExecuting (APPROVED -> EXECUTING) and re-executes the staged + " payload (ARN-002 amendment, ARN-010). retrySync never writes EXECUTING itself. + DATA approvals_for_update TYPE TABLE FOR UPDATE zr_changeapproval. + + READ ENTITIES OF zr_changeapproval IN LOCAL MODE + ENTITY ChangeApproval + FIELDS ( Status ) + WITH CORRESPONDING #( keys ) + RESULT DATA(approvals). + + LOOP AT keys INTO DATA(key). + ASSIGN approvals[ ID = key-ID ] TO FIELD-SYMBOL(). + IF sy-subrc <> 0. + APPEND VALUE #( %tky = key-%tky + %fail-cause = if_abap_behv=>cause-not_found + %op-%action-retrySync = if_abap_behv=>mk-on ) TO failed-changeapproval. + CONTINUE. + ENDIF. + + IF -Status <> status-failed. + APPEND VALUE #( %tky = key-%tky + %fail-cause = if_abap_behv=>cause-unspecific + %op-%action-retrySync = if_abap_behv=>mk-on ) TO failed-changeapproval. + APPEND VALUE #( %tky = key-%tky + %op-%action-retrySync = if_abap_behv=>mk-on + %msg = new_message_with_text( + severity = if_abap_behv_message=>severity-error + text = |Only FAILED records can be retried (status is { -Status })| ) ) + TO reported-changeapproval. + CONTINUE. + ENDIF. + + APPEND VALUE #( %tky = key-%tky + Status = status-approved + ErrorMessage = '' ) TO approvals_for_update. + ENDLOOP. + + IF approvals_for_update IS NOT INITIAL. + MODIFY ENTITIES OF zr_changeapproval IN LOCAL MODE + ENTITY ChangeApproval + UPDATE FIELDS ( Status ErrorMessage ) + WITH approvals_for_update + FAILED DATA(update_failed) + REPORTED DATA(update_reported). + + LOOP AT update_reported-changeapproval ASSIGNING FIELD-SYMBOL(). + -%op-%action-retrySync = if_abap_behv=>mk-on. + ENDLOOP. + reported = CORRESPONDING #( DEEP APPENDING BASE ( reported ) update_reported ). + + LOOP AT update_failed-changeapproval INTO DATA(update_failure). + APPEND VALUE #( %tky = update_failure-%tky + %fail-cause = COND #( WHEN update_failure-%fail-cause = if_abap_behv=>cause-locked + THEN if_abap_behv=>cause-unspecific + ELSE update_failure-%fail-cause ) + %op-%action-retrySync = if_abap_behv=>mk-on ) TO failed-changeapproval. + IF update_failure-%fail-cause <> if_abap_behv=>cause-not_found + AND NOT line_exists( update_reported-changeapproval[ ID = update_failure-ID ] ). + APPEND VALUE #( %tky = update_failure-%tky + %op-%action-retrySync = if_abap_behv=>mk-on + %msg = new_message_with_text( + severity = if_abap_behv_message=>severity-error + text = 'The retry could not be recorded' ) ) + TO reported-changeapproval. + ENDIF. + ENDLOOP. + ENDIF. + + READ ENTITIES OF zr_changeapproval IN LOCAL MODE + ENTITY ChangeApproval + ALL FIELDS WITH CORRESPONDING #( keys ) + RESULT DATA(updated). + + LOOP AT updated INTO DATA(row). + IF NOT line_exists( failed-changeapproval[ ID = row-ID ] ). + APPEND VALUE #( %tky = row-%tky %param = row ) TO result. + ENDIF. + ENDLOOP. + ENDMETHOD. + + + METHOD markExecuting. + " APPROVED -> EXECUTING (worker claims the record; the UI never offers Approve on an executing record, PRD RAP-6). + " The only place that writes EXECUTING. + DATA approvals_for_update TYPE TABLE FOR UPDATE zr_changeapproval. + + READ ENTITIES OF zr_changeapproval IN LOCAL MODE + ENTITY ChangeApproval + FIELDS ( Status ) + WITH CORRESPONDING #( keys ) + RESULT DATA(approvals). + + LOOP AT keys INTO DATA(key). + ASSIGN approvals[ ID = key-ID ] TO FIELD-SYMBOL(). + IF sy-subrc <> 0. + APPEND VALUE #( %tky = key-%tky + %fail-cause = if_abap_behv=>cause-not_found + %op-%action-markExecuting = if_abap_behv=>mk-on ) TO failed-changeapproval. + CONTINUE. + ENDIF. + + IF -Status <> status-approved. + APPEND VALUE #( %tky = key-%tky + %fail-cause = if_abap_behv=>cause-unspecific + %op-%action-markExecuting = if_abap_behv=>mk-on ) TO failed-changeapproval. + APPEND VALUE #( %tky = key-%tky + %op-%action-markExecuting = if_abap_behv=>mk-on + %msg = new_message_with_text( + severity = if_abap_behv_message=>severity-error + text = |Only APPROVED records can start executing (status is { -Status })| ) ) + TO reported-changeapproval. + CONTINUE. + ENDIF. + + APPEND VALUE #( %tky = key-%tky + Status = status-executing ) TO approvals_for_update. + ENDLOOP. + + IF approvals_for_update IS NOT INITIAL. + MODIFY ENTITIES OF zr_changeapproval IN LOCAL MODE + ENTITY ChangeApproval + UPDATE FIELDS ( Status ) + WITH approvals_for_update + FAILED DATA(update_failed) + REPORTED DATA(update_reported). + + LOOP AT update_reported-changeapproval ASSIGNING FIELD-SYMBOL(). + -%op-%action-markExecuting = if_abap_behv=>mk-on. + ENDLOOP. + reported = CORRESPONDING #( DEEP APPENDING BASE ( reported ) update_reported ). + + LOOP AT update_failed-changeapproval INTO DATA(update_failure). + APPEND VALUE #( %tky = update_failure-%tky + %fail-cause = COND #( WHEN update_failure-%fail-cause = if_abap_behv=>cause-locked + THEN if_abap_behv=>cause-unspecific + ELSE update_failure-%fail-cause ) + %op-%action-markExecuting = if_abap_behv=>mk-on ) TO failed-changeapproval. + IF update_failure-%fail-cause <> if_abap_behv=>cause-not_found + AND NOT line_exists( update_reported-changeapproval[ ID = update_failure-ID ] ). + APPEND VALUE #( %tky = update_failure-%tky + %op-%action-markExecuting = if_abap_behv=>mk-on + %msg = new_message_with_text( + severity = if_abap_behv_message=>severity-error + text = 'The execution start could not be recorded' ) ) + TO reported-changeapproval. + ENDIF. + ENDLOOP. + ENDIF. + + READ ENTITIES OF zr_changeapproval IN LOCAL MODE + ENTITY ChangeApproval + ALL FIELDS WITH CORRESPONDING #( keys ) + RESULT DATA(updated). + + LOOP AT updated INTO DATA(row). + IF NOT line_exists( failed-changeapproval[ ID = row-ID ] ). + APPEND VALUE #( %tky = row-%tky %param = row ) TO result. + ENDIF. + ENDLOOP. + ENDMETHOD. + + + METHOD markSynced. + " EXECUTING -> SYNCED (terminal success); SAP is the ledger, the change number is the receipt (ARN-006) + DATA approvals_for_update TYPE TABLE FOR UPDATE zr_changeapproval. + + READ ENTITIES OF zr_changeapproval IN LOCAL MODE + ENTITY ChangeApproval + FIELDS ( Status ) + WITH CORRESPONDING #( keys ) + RESULT DATA(approvals). + + LOOP AT keys INTO DATA(key). + ASSIGN approvals[ ID = key-ID ] TO FIELD-SYMBOL(). + IF sy-subrc <> 0. + APPEND VALUE #( %tky = key-%tky + %fail-cause = if_abap_behv=>cause-not_found + %op-%action-markSynced = if_abap_behv=>mk-on ) TO failed-changeapproval. + CONTINUE. + ENDIF. + + IF -Status <> status-executing. + APPEND VALUE #( %tky = key-%tky + %fail-cause = if_abap_behv=>cause-unspecific + %op-%action-markSynced = if_abap_behv=>mk-on ) TO failed-changeapproval. + APPEND VALUE #( %tky = key-%tky + %op-%action-markSynced = if_abap_behv=>mk-on + %msg = new_message_with_text( + severity = if_abap_behv_message=>severity-error + text = |Only EXECUTING records can be marked synced (status is { -Status })| ) ) + TO reported-changeapproval. + CONTINUE. + ENDIF. + + IF key-%param-SapChangeNumber IS INITIAL. + APPEND VALUE #( %tky = key-%tky + %fail-cause = if_abap_behv=>cause-unspecific + %op-%action-markSynced = if_abap_behv=>mk-on ) TO failed-changeapproval. + APPEND VALUE #( %tky = key-%tky + %op-%action-markSynced = if_abap_behv=>mk-on + %msg = new_message_with_text( + severity = if_abap_behv_message=>severity-error + text = 'The SAP change number is required to mark a record synced' ) ) + TO reported-changeapproval. + CONTINUE. + ENDIF. + + APPEND VALUE #( %tky = key-%tky + Status = status-synced + SapChangeNumber = key-%param-SapChangeNumber + ErrorMessage = '' ) TO approvals_for_update. + ENDLOOP. + + IF approvals_for_update IS NOT INITIAL. + MODIFY ENTITIES OF zr_changeapproval IN LOCAL MODE + ENTITY ChangeApproval + UPDATE FIELDS ( Status SapChangeNumber ErrorMessage ) + WITH approvals_for_update + FAILED DATA(update_failed) + REPORTED DATA(update_reported). + + LOOP AT update_reported-changeapproval ASSIGNING FIELD-SYMBOL(). + -%op-%action-markSynced = if_abap_behv=>mk-on. + ENDLOOP. + reported = CORRESPONDING #( DEEP APPENDING BASE ( reported ) update_reported ). + + LOOP AT update_failed-changeapproval INTO DATA(update_failure). + APPEND VALUE #( %tky = update_failure-%tky + %fail-cause = COND #( WHEN update_failure-%fail-cause = if_abap_behv=>cause-locked + THEN if_abap_behv=>cause-unspecific + ELSE update_failure-%fail-cause ) + %op-%action-markSynced = if_abap_behv=>mk-on ) TO failed-changeapproval. + IF update_failure-%fail-cause <> if_abap_behv=>cause-not_found + AND NOT line_exists( update_reported-changeapproval[ ID = update_failure-ID ] ). + APPEND VALUE #( %tky = update_failure-%tky + %op-%action-markSynced = if_abap_behv=>mk-on + %msg = new_message_with_text( + severity = if_abap_behv_message=>severity-error + text = 'The sync result could not be recorded' ) ) + TO reported-changeapproval. + ENDIF. + ENDLOOP. + ENDIF. + + READ ENTITIES OF zr_changeapproval IN LOCAL MODE + ENTITY ChangeApproval + ALL FIELDS WITH CORRESPONDING #( keys ) + RESULT DATA(updated). + + LOOP AT updated INTO DATA(row). + IF NOT line_exists( failed-changeapproval[ ID = row-ID ] ). + APPEND VALUE #( %tky = row-%tky %param = row ) TO result. + ENDIF. + ENDLOOP. + ENDMETHOD. + + + METHOD markFailed. + " EXECUTING -> FAILED with the error text; distinct from REJECTED (predecessor defect, ARN-002) + DATA approvals_for_update TYPE TABLE FOR UPDATE zr_changeapproval. + + READ ENTITIES OF zr_changeapproval IN LOCAL MODE + ENTITY ChangeApproval + FIELDS ( Status ) + WITH CORRESPONDING #( keys ) + RESULT DATA(approvals). + + LOOP AT keys INTO DATA(key). + ASSIGN approvals[ ID = key-ID ] TO FIELD-SYMBOL(). + IF sy-subrc <> 0. + APPEND VALUE #( %tky = key-%tky + %fail-cause = if_abap_behv=>cause-not_found + %op-%action-markFailed = if_abap_behv=>mk-on ) TO failed-changeapproval. + CONTINUE. + ENDIF. + + IF -Status <> status-executing. + APPEND VALUE #( %tky = key-%tky + %fail-cause = if_abap_behv=>cause-unspecific + %op-%action-markFailed = if_abap_behv=>mk-on ) TO failed-changeapproval. + APPEND VALUE #( %tky = key-%tky + %op-%action-markFailed = if_abap_behv=>mk-on + %msg = new_message_with_text( + severity = if_abap_behv_message=>severity-error + text = |Only EXECUTING records can be marked failed (status is { -Status })| ) ) + TO reported-changeapproval. + CONTINUE. + ENDIF. + + IF key-%param-ErrorMessage IS INITIAL. + APPEND VALUE #( %tky = key-%tky + %fail-cause = if_abap_behv=>cause-unspecific + %op-%action-markFailed = if_abap_behv=>mk-on ) TO failed-changeapproval. + APPEND VALUE #( %tky = key-%tky + %op-%action-markFailed = if_abap_behv=>mk-on + %msg = new_message_with_text( + severity = if_abap_behv_message=>severity-error + text = 'An error message is required to mark a record failed' ) ) + TO reported-changeapproval. + CONTINUE. + ENDIF. + + APPEND VALUE #( %tky = key-%tky + Status = status-failed + ErrorMessage = key-%param-ErrorMessage ) TO approvals_for_update. + ENDLOOP. + + IF approvals_for_update IS NOT INITIAL. + MODIFY ENTITIES OF zr_changeapproval IN LOCAL MODE + ENTITY ChangeApproval + UPDATE FIELDS ( Status ErrorMessage ) + WITH approvals_for_update + FAILED DATA(update_failed) + REPORTED DATA(update_reported). + + LOOP AT update_reported-changeapproval ASSIGNING FIELD-SYMBOL(). + -%op-%action-markFailed = if_abap_behv=>mk-on. + ENDLOOP. + reported = CORRESPONDING #( DEEP APPENDING BASE ( reported ) update_reported ). + + LOOP AT update_failed-changeapproval INTO DATA(update_failure). + APPEND VALUE #( %tky = update_failure-%tky + %fail-cause = COND #( WHEN update_failure-%fail-cause = if_abap_behv=>cause-locked + THEN if_abap_behv=>cause-unspecific + ELSE update_failure-%fail-cause ) + %op-%action-markFailed = if_abap_behv=>mk-on ) TO failed-changeapproval. + IF update_failure-%fail-cause <> if_abap_behv=>cause-not_found + AND NOT line_exists( update_reported-changeapproval[ ID = update_failure-ID ] ). + APPEND VALUE #( %tky = update_failure-%tky + %op-%action-markFailed = if_abap_behv=>mk-on + %msg = new_message_with_text( + severity = if_abap_behv_message=>severity-error + text = 'The failure could not be recorded' ) ) + TO reported-changeapproval. + ENDIF. + ENDLOOP. + ENDIF. + + READ ENTITIES OF zr_changeapproval IN LOCAL MODE + ENTITY ChangeApproval + ALL FIELDS WITH CORRESPONDING #( keys ) + RESULT DATA(updated). + + LOOP AT updated INTO DATA(row). + IF NOT line_exists( failed-changeapproval[ ID = row-ID ] ). + APPEND VALUE #( %tky = row-%tky %param = row ) TO result. + ENDIF. + ENDLOOP. + ENDMETHOD. + + + METHOD setInitialStatus. + " Every created record starts PENDING with the requester audit fields (ARN-002) + READ ENTITIES OF zr_changeapproval IN LOCAL MODE + ENTITY ChangeApproval + FIELDS ( Status ) + WITH CORRESPONDING #( keys ) + RESULT DATA(approvals). + + DELETE approvals WHERE Status IS NOT INITIAL. + IF approvals IS INITIAL. + RETURN. + ENDIF. + + MODIFY ENTITIES OF zr_changeapproval IN LOCAL MODE + ENTITY ChangeApproval + UPDATE FIELDS ( Status RequestedBy RequestedOn ) + WITH VALUE #( FOR approval IN approvals + ( %tky = approval-%tky + Status = status-pending + RequestedBy = cl_abap_context_info=>get_user_technical_name( ) + RequestedOn = utclong_current( ) ) ) + REPORTED DATA(update_reported). + + reported = CORRESPONDING #( DEEP update_reported ). + ENDMETHOD. + + + METHOD checkDuplicate. + " Semantic key ItemNumber + ChangeType + ArenaEcoId (PRD RAP-8) must be unique among records that are not REJECTED. + " Checks (1) duplicates among the instances of this request and (2) persisted records, read through the CDS view + " with privileged access (released access only; the view is access-controlled, see header sources). + READ ENTITIES OF zr_changeapproval IN LOCAL MODE + ENTITY ChangeApproval + FIELDS ( ItemNumber ChangeType ArenaEcoId ) + WITH CORRESPONDING #( keys ) + RESULT DATA(approvals). + + LOOP AT approvals INTO DATA(approval). + " Invalidate a state message left by an earlier run of this validation for the instance + APPEND VALUE #( %tky = approval-%tky + %state_area = state_area_duplicate ) TO reported-changeapproval. + + DATA(same_key_in_request) = REDUCE i( INIT count = 0 + FOR other IN approvals + WHERE ( ItemNumber = approval-ItemNumber + AND ChangeType = approval-ChangeType + AND ArenaEcoId = approval-ArenaEcoId ) + NEXT count = count + 1 ). + + DATA(is_duplicate) = xsdbool( same_key_in_request > 1 ). + + IF is_duplicate = abap_false. + SELECT SINGLE FROM zr_changeapproval + FIELDS ID + WHERE ItemNumber = @approval-ItemNumber + AND ChangeType = @approval-ChangeType + AND ArenaEcoId = @approval-ArenaEcoId + AND Status <> @status-rejected + AND ID <> @approval-ID + INTO @DATA(existing_id) + OPTIONS PRIVILEGED ACCESS. + is_duplicate = xsdbool( sy-subrc = 0 ). + ENDIF. + + IF is_duplicate = abap_true. + APPEND VALUE #( %tky = approval-%tky + %fail-cause = if_abap_behv=>cause-unspecific ) TO failed-changeapproval. + APPEND VALUE #( %tky = approval-%tky + %state_area = state_area_duplicate + %msg = new_message_with_text( + severity = if_abap_behv_message=>severity-error + text = |A change approval for item { approval-ItemNumber }, | && + |change type { approval-ChangeType } and ECO { approval-ArenaEcoId } | && + |already exists and is not rejected| ) + %element-ItemNumber = if_abap_behv=>mk-on + %element-ChangeType = if_abap_behv=>mk-on + %element-ArenaEcoId = if_abap_behv=>mk-on ) TO reported-changeapproval. + ENDIF. + ENDLOOP. + ENDMETHOD. + +ENDCLASS. diff --git a/docs/handover/rap/zc_changeapproval.bdef.asbdef b/docs/handover/rap/zc_changeapproval.bdef.asbdef new file mode 100644 index 0000000..b90d45c --- /dev/null +++ b/docs/handover/rap/zc_changeapproval.bdef.asbdef @@ -0,0 +1,26 @@ +// TARGET OBJECT : behavior definition ZC_CHANGEAPPROVAL (UI projection BDEF) - README step U9, UI leg +// abap: URI : abap:/repotree-v1/0M7DEJI/System%20Library/ZLOCAL/ZARENA_PLM/Core%20Data%20Services/Behavior%20Definitions/ZC_CHANGEAPPROVAL/zc_changeapproval.bdef.json +// ACTION : create via ADT MCP (objectType BDEF/BDO, implementationType Projection, README U9) once the view +// ZC_CHANGEAPPROVAL is active, replace entire content, save, then activate (BDEF ZR_CHANGEAPPROVAL and +// class ZBP_R_CHANGEAPPROVAL must be active). +// No behavior pool is created for this projection: a projection BDEF needs one only when it defines +// new actions/functions, and this one reuses everything +// [source: abap-docs /abap-docs-cloud/ABENBDL_BDEF_PROJECTION_HEADER, verified 2026-09-15]. +// Contract : approve / reject / retrySync are the bound-action names the Fiori app calls (exact case). +// Sources (verified 2026-09-15): use action, use etag - no automatic inheritance - only what is listed is exposed, +// which is how create/update/delete and the worker actions are kept out of the UI service (PRD RAP-2) +// [abap-docs /abap-docs-cloud/ABENBDL_USE_PROJECTION]. Feature control of reused actions is passed on automatically (same source). +// `use etag` is an entity behavior characteristic: it sits between `define behavior for ... alias ...` and `{`, +// without a semicolon [source: abap-docs /abap-docs-cloud/ABENBDL_CHARACTER_PROJECTION and +// /abap-docs-cloud/ABENBDL_DEFINE_BEH_PROJECTION, verified 2026-09-16]; it must be stated per entity or optimistic +// concurrency control is lost [source: abap-docs /abap-docs-cloud/ABENBDL_USE_PROJECTION, verified 2026-09-16]. +projection; +strict ( 2 ); + +define behavior for ZC_CHANGEAPPROVAL alias ChangeApproval +use etag +{ + use action approve; + use action reject; + use action retrySync; +} diff --git a/docs/handover/rap/zc_changeapproval.dcls.asdcls b/docs/handover/rap/zc_changeapproval.dcls.asdcls new file mode 100644 index 0000000..3c77abe --- /dev/null +++ b/docs/handover/rap/zc_changeapproval.dcls.asdcls @@ -0,0 +1,13 @@ +/* TARGET OBJECT : access control ZC_CHANGEAPPROVAL - README step U11, UI leg + abap: URI : abap:/repotree-v1/0M7DEJI/System%20Library/ZLOCAL/ZARENA_PLM/Core%20Data%20Services/Access%20Controls/ZC_CHANGEAPPROVAL/zc_changeapproval.dcls.acds + ACTION : create empty via ADT MCP (objectType DCLS/DL, README U11), replace entire content, save, then activate + (after view ZC_CHANGEAPPROVAL and access control ZR_CHANGEAPPROVAL). + A transactional query needs its own DCL, inheriting the base view's conditions - there is no implicit inheritance + [source: abap-docs /abap-platform-rap-opensap/week2/unit7#step-5-create-cds-access-control-for-travel-bo-projection-view-and-check-the-result; + /abap-docs-cloud/ABENCDS_DEFINE_VIEW_AS_PROJECTION (access control must be symmetric), verified 2026-09-15]. */ +@EndUserText.label: 'Access control for ZC_CHANGEAPPROVAL' +@MappingRole: true +define role ZC_CHANGEAPPROVAL { + grant select on ZC_CHANGEAPPROVAL + where inheriting conditions from entity ZR_CHANGEAPPROVAL; +} diff --git a/docs/handover/rap/zzc_changeapproval000.ddls.asddls b/docs/handover/rap/zc_changeapproval.ddls.asddls similarity index 59% rename from docs/handover/rap/zzc_changeapproval000.ddls.asddls rename to docs/handover/rap/zc_changeapproval.ddls.asddls index f5d224d..6a0d9c2 100644 --- a/docs/handover/rap/zzc_changeapproval000.ddls.asddls +++ b/docs/handover/rap/zc_changeapproval.ddls.asddls @@ -1,6 +1,10 @@ -/* TARGET OBJECT : data definition ZZC_CHANGEAPPROVAL000 (UI projection, generated 2026-09-15) - abap: URI : abap:/repotree-v1/0M7DEJI/System%20Library/ZLOCAL/ZARENA_PLM/Core%20Data%20Services/Data%20Definitions/ZZC_CHANGEAPPROVAL000/zzc_changeapproval000.ddls.acds - ACTION : replace entire content, then activate (after ZZR_CHANGEAPPROVAL000). +/* TARGET OBJECT : data definition ZC_CHANGEAPPROVAL (UI projection) - README step U8, UI leg + abap: URI : abap:/repotree-v1/0M7DEJI/System%20Library/ZLOCAL/ZARENA_PLM/Core%20Data%20Services/Data%20Definitions/ZC_CHANGEAPPROVAL/zc_changeapproval.ddls.acds + ACTION : create empty via ADT MCP (objectType DDLS/DF, README U8), replace entire content, save, then activate + (after ZR_CHANGEAPPROVAL). + Contract : element names are the OData property names of entity type ChangeApproval_Type in the Fiori app + (app/webapp/localService/mainService/metadata.xml, 17 properties, exact case). StatusText (RAP-3 text + element) and LocalLastChangedAt (etag) are in addition to the app's mock - see README "App contract check". Notes: provider contract transactional_query [source: abap-docs /abap-docs-cloud/ABENCDS_DEFINE_VIEW_AS_PROJECTION, verified 2026-09-15]; @ObjectModel.semanticKey (PRD RAP-8) [source: abap-docs /abap-fiori-showcase/05_object_page_header#handling-semantic-key-fields, verified 2026-09-15]; @Metadata.allowExtensions is required for the metadata extension [source: abap-docs /abap-docs-cloud/ABENCDS_F1_ANNOTATE_VIEW, verified 2026-09-15]. */ @@ -8,9 +12,9 @@ @AccessControl.authorizationCheck: #CHECK @Metadata.allowExtensions: true @ObjectModel.semanticKey: ['ItemNumber', 'ChangeType', 'ArenaEcoId'] -define root view entity ZZC_CHANGEAPPROVAL000 +define root view entity ZC_CHANGEAPPROVAL provider contract transactional_query - as projection on ZZR_CHANGEAPPROVAL000 + as projection on ZR_CHANGEAPPROVAL { key ID, ItemNumber, diff --git a/docs/handover/rap/zzc_changeapproval000.ddlx.asddlx b/docs/handover/rap/zc_changeapproval.ddlx.asddlx similarity index 93% rename from docs/handover/rap/zzc_changeapproval000.ddlx.asddlx rename to docs/handover/rap/zc_changeapproval.ddlx.asddlx index b3c0968..215ff42 100644 --- a/docs/handover/rap/zzc_changeapproval000.ddlx.asddlx +++ b/docs/handover/rap/zc_changeapproval.ddlx.asddlx @@ -1,6 +1,7 @@ -/* TARGET OBJECT : metadata extension ZZC_CHANGEAPPROVAL000 (generated 2026-09-15) - abap: URI : abap:/repotree-v1/0M7DEJI/System%20Library/ZLOCAL/ZARENA_PLM/Core%20Data%20Services/Metadata%20Extensions/ZZC_CHANGEAPPROVAL000/zzc_changeapproval000.ddlx.acds - ACTION : replace entire content, then activate (after ZZC_CHANGEAPPROVAL000 view). +/* TARGET OBJECT : metadata extension ZC_CHANGEAPPROVAL - README step U12, UI leg + abap: URI : abap:/repotree-v1/0M7DEJI/System%20Library/ZLOCAL/ZARENA_PLM/Core%20Data%20Services/Metadata%20Extensions/ZC_CHANGEAPPROVAL/zc_changeapproval.ddlx.acds + ACTION : create empty via ADT MCP (objectType DDLX/EX, README U12), replace entire content, save, then activate + (after ZC_CHANGEAPPROVAL view). Mirrors app/webapp/annotations/annotation.xml (line items, filter bar, header, facets, actions); the app's local annotations still apply on top at runtime. Labels are literals here; the app carries the i18n texts. Sources (all verified 2026-09-15, abap-docs /abap-fiori-showcase): @@ -31,7 +32,7 @@ visualizations: [ { type: #AS_LINEITEM } ] } ] } -annotate entity ZZC_CHANGEAPPROVAL000 with +annotate entity ZC_CHANGEAPPROVAL with { @UI.facet: [ { id: 'StatusPoint', purpose: #HEADER, type: #DATAPOINT_REFERENCE, targetQualifier: 'StatusPoint' }, diff --git a/docs/handover/rap/zc_changeapproval_a2x.bdef.asbdef b/docs/handover/rap/zc_changeapproval_a2x.bdef.asbdef index 582c479..8f4cf5a 100644 --- a/docs/handover/rap/zc_changeapproval_a2x.bdef.asbdef +++ b/docs/handover/rap/zc_changeapproval_a2x.bdef.asbdef @@ -1,16 +1,20 @@ -// TARGET OBJECT : behavior definition ZC_CHANGEAPPROVAL_A2X (A2X projection BDEF) - DOES NOT EXIST YET. -// The ADT MCP refused to create it while ZC_CHANGEAPPROVAL_A2X (DDLS) had no active source -// ("The referenced STOB object ZC_CHANGEAPPROVAL_A2X does not exist", 2026-09-15). -// Create it after the projection view is active: ADT > New Behavior Definition on ZC_CHANGEAPPROVAL_A2X, -// implementation type Projection (or re-run abap_creation-create_object BDEF/BDO, see README), paste, activate. -// Sources: as ZZC_CHANGEAPPROVAL000. use create lets the worker stage records (ARN-010: the staged payload is what -// gets executed); the worker never sees approve/reject/retrySync (PRD RAP-6). +// TARGET OBJECT : behavior definition ZC_CHANGEAPPROVAL_A2X (A2X projection BDEF) - README step W3, worker leg +// abap: URI : abap:/repotree-v1/0M7DEJI/System%20Library/ZLOCAL/ZARENA_PLM/Core%20Data%20Services/Behavior%20Definitions/ZC_CHANGEAPPROVAL_A2X/zc_changeapproval_a2x.bdef.abdl +// ACTION : create via ADT MCP (objectType BDEF/BDO, implementationType Projection, README W3) only once the view +// ZC_CHANGEAPPROVAL_A2X is active - the MCP validates the referenced entity's active version (live +// 2026-09-15: "The referenced STOB object ZC_CHANGEAPPROVAL_A2X does not exist" while the view had no +// active source). Replace entire content, save, then activate. +// Sources: as ZC_CHANGEAPPROVAL (incl. `use etag` as an entity behavior characteristic in the header, not in the body). +// use create lets the worker stage records (ARN-010: the staged payload is what gets executed); the worker never +// sees approve/reject/retrySync (PRD RAP-6). The base BDEF's validation checkDuplicate runs on every create; it is +// not something a projection lists (validations are not in the `use` list [source: abap-docs +// /abap-docs-cloud/ABENBDL_USE_PROJECTION, verified 2026-09-16]). projection; strict ( 2 ); define behavior for ZC_CHANGEAPPROVAL_A2X alias ChangeApproval +use etag { - use etag; use create; use action markExecuting; diff --git a/docs/handover/rap/zc_changeapproval_a2x.dcls.asdcls b/docs/handover/rap/zc_changeapproval_a2x.dcls.asdcls index a99d774..e14a50d 100644 --- a/docs/handover/rap/zc_changeapproval_a2x.dcls.asdcls +++ b/docs/handover/rap/zc_changeapproval_a2x.dcls.asdcls @@ -1,10 +1,11 @@ -/* TARGET OBJECT : access control ZC_CHANGEAPPROVAL_A2X (created empty via ADT MCP 2026-09-15) +/* TARGET OBJECT : access control ZC_CHANGEAPPROVAL_A2X - README step W2, worker leg abap: URI : abap:/repotree-v1/0M7DEJI/System%20Library/ZLOCAL/ZARENA_PLM/Core%20Data%20Services/Access%20Controls/ZC_CHANGEAPPROVAL_A2X/zc_changeapproval_a2x.dcls.acds - ACTION : replace entire content, then activate (after ZC_CHANGEAPPROVAL_A2X view and ZZR_CHANGEAPPROVAL000 DCL). - Sources: as ZZC_CHANGEAPPROVAL000 DCL. */ + ACTION : create empty via ADT MCP (objectType DCLS/DL, README W2), replace entire content, save, then activate + (after ZC_CHANGEAPPROVAL_A2X view and ZR_CHANGEAPPROVAL DCL). + Sources: as ZC_CHANGEAPPROVAL DCL. */ @EndUserText.label: 'Access control for ZC_CHANGEAPPROVAL_A2X' @MappingRole: true define role ZC_CHANGEAPPROVAL_A2X { grant select on ZC_CHANGEAPPROVAL_A2X - where inheriting conditions from entity ZZR_CHANGEAPPROVAL000; + where inheriting conditions from entity ZR_CHANGEAPPROVAL; } diff --git a/docs/handover/rap/zc_changeapproval_a2x.ddls.asddls b/docs/handover/rap/zc_changeapproval_a2x.ddls.asddls index 01b8b44..f62b07e 100644 --- a/docs/handover/rap/zc_changeapproval_a2x.ddls.asddls +++ b/docs/handover/rap/zc_changeapproval_a2x.ddls.asddls @@ -1,15 +1,16 @@ -/* TARGET OBJECT : data definition ZC_CHANGEAPPROVAL_A2X (A2X projection for the worker; created empty via ADT MCP 2026-09-15) +/* TARGET OBJECT : data definition ZC_CHANGEAPPROVAL_A2X (A2X projection for the worker) - README step W1, worker leg abap: URI : abap:/repotree-v1/0M7DEJI/System%20Library/ZLOCAL/ZARENA_PLM/Core%20Data%20Services/Data%20Definitions/ZC_CHANGEAPPROVAL_A2X/zc_changeapproval_a2x.ddls.acds - ACTION : replace entire content, then activate (after ZZR_CHANGEAPPROVAL000). + ACTION : create empty via ADT MCP (objectType DDLS/DF, README W1), replace entire content, save, then activate + (after the UI leg; needs ZR_CHANGEAPPROVAL). Notes: separate projection so the Web API exposes create + mark* while the UI projection stays read-only - (PRD RAP-2 / RAP-6); same syntax sources as ZZC_CHANGEAPPROVAL000. No metadata extension here - + (PRD RAP-2 / RAP-6); same syntax sources as ZC_CHANGEAPPROVAL. No metadata extension here - a Web API binding carries no UI annotations [source: sap-docs sap-help loio 248b36b7ebce4f7e8c7670c9371adee1, verified 2026-09-15]. */ @EndUserText.label: 'Arena change approval - A2X projection (worker)' @AccessControl.authorizationCheck: #CHECK @ObjectModel.semanticKey: ['ItemNumber', 'ChangeType', 'ArenaEcoId'] define root view entity ZC_CHANGEAPPROVAL_A2X provider contract transactional_query - as projection on ZZR_CHANGEAPPROVAL000 + as projection on ZR_CHANGEAPPROVAL { key ID, ItemNumber, diff --git a/docs/handover/rap/zzchngpproval000.tabl.asddls b/docs/handover/rap/zchangeapproval.tabl.asddls similarity index 57% rename from docs/handover/rap/zzchngpproval000.tabl.asddls rename to docs/handover/rap/zchangeapproval.tabl.asddls index 9df0022..1b19c2c 100644 --- a/docs/handover/rap/zzchngpproval000.tabl.asddls +++ b/docs/handover/rap/zchangeapproval.tabl.asddls @@ -1,17 +1,24 @@ -/* TARGET OBJECT : database table ZZCHNGPPROVAL000 (generated 2026-09-15 by x-ui-service) - abap: URI : abap:/repotree-v1/0M7DEJI/System%20Library/ZLOCAL/ZARENA_PLM/Dictionary/Database%20Tables/ZZCHNGPPROVAL000/zzchngpproval000.tabl.json - ACTION : replace entire content, then activate. +/* TARGET OBJECT : database table ZCHANGEAPPROVAL (persistence of the approval BO) - README step U1, UI leg + abap: URI : abap:/repotree-v1/0M7DEJI/System%20Library/ZLOCAL/ZARENA_PLM/Dictionary/Database%20Tables/ZCHANGEAPPROVAL/zchangeapproval.tabl.ddic + (returned by create_object 2026-09-16; activation extension pending live proof - README activation table) + ACTION : create empty via ADT MCP (objectType TABL/DT, README U1), then paste into the .tabl.ddic file, NEVER the + .tabl.json file: in the VS Code ADT workspace zchangeapproval.tabl.json holds only header metadata + (formatVersion, description, originalLanguage, abapLanguageVersion) - live observation 2026-09-16. + Replace entire content of the .tabl.ddic file, save, then activate. + A DDIC database table is edited as Dictionary DDL source code generated from, and saved back into, the + table metadata [source: abap-docs /abap-docs-cloud/ABENDDIC_DEFINE_TABLE, verified 2026-09-16]. + Name : 15 characters; TABL/DT allows 16 [source: adt-mcp abap_creation-get_object_type_details on 0M7DEJI, verified 2026-09-16]. Notes: sap_preview is abap.string(0) (RAP-7; char is capped at 1333 for table fields [source: abap-docs /abap-docs-cloud/ABENDDIC_BUILTIN_TYPES, verified 2026-09-15]; abap.string(0) table-field syntax [source: abap-docs /abap-docs-cloud/ABENDDICDDL_DEFINE_TABLE_COMPS, verified 2026-09-15]). Timestamps are abap.utclong (UTCLONG built-in type, same source). Admin fields use the released - ABP_* data elements and SYSUUID_X16 [source: sap-released-objects public_cloud level A, verified 2026-09-15]. */ + ABP_* data elements and SYSUUID_X16 [source: sap-released-objects public_cloud level A, verified 2026-09-16]. */ @EndUserText.label : 'Arena change approval (staging + state machine)' @AbapCatalog.enhancement.category : #NOT_EXTENSIBLE @AbapCatalog.tableCategory : #TRANSPARENT @AbapCatalog.deliveryClass : #A @AbapCatalog.dataMaintenance : #RESTRICTED -define table zzchngpproval000 { +define table zchangeapproval { key client : abap.clnt not null; key id : sysuuid_x16 not null; item_number : abap.char(18); diff --git a/docs/handover/rap/zd_changeapproval_failedp.ddls.asddls b/docs/handover/rap/zd_changeapproval_failedp.ddls.asddls index 53c534f..784bedf 100644 --- a/docs/handover/rap/zd_changeapproval_failedp.ddls.asddls +++ b/docs/handover/rap/zd_changeapproval_failedp.ddls.asddls @@ -1,6 +1,8 @@ -/* TARGET OBJECT : data definition ZD_CHANGEAPPROVAL_FAILEDP (abstract entity = parameter of action markFailed; created empty via ADT MCP 2026-09-15) +/* TARGET OBJECT : data definition ZD_CHANGEAPPROVAL_FAILEDP (abstract entity = parameter of action markFailed) - README step U4, UI leg abap: URI : abap:/repotree-v1/0M7DEJI/System%20Library/ZLOCAL/ZARENA_PLM/Core%20Data%20Services/Data%20Definitions/ZD_CHANGEAPPROVAL_FAILEDP/zd_changeapproval_failedp.ddls.acds - ACTION : replace entire content, then activate. + ACTION : create empty via ADT MCP (objectType DDLS/DF, README U4), replace entire content, save, then activate. + Leg : UI leg although only the worker calls markFailed - the base BDEF ZR_CHANGEAPPROVAL (U6) names this + entity as the markFailed parameter, so it is ordered before that BDEF. Sources: as ZD_CHANGEAPPROVAL_REJECTP. */ @EndUserText.label: 'Arena change approval: markFailed parameter' define abstract entity ZD_CHANGEAPPROVAL_FAILEDP diff --git a/docs/handover/rap/zd_changeapproval_rejectp.ddls.asddls b/docs/handover/rap/zd_changeapproval_rejectp.ddls.asddls index f66e9eb..6da3e3a 100644 --- a/docs/handover/rap/zd_changeapproval_rejectp.ddls.asddls +++ b/docs/handover/rap/zd_changeapproval_rejectp.ddls.asddls @@ -1,6 +1,6 @@ -/* TARGET OBJECT : data definition ZD_CHANGEAPPROVAL_REJECTP (abstract entity = parameter of action reject; created empty via ADT MCP 2026-09-15) +/* TARGET OBJECT : data definition ZD_CHANGEAPPROVAL_REJECTP (abstract entity = parameter of action reject) - README step U2, UI leg abap: URI : abap:/repotree-v1/0M7DEJI/System%20Library/ZLOCAL/ZARENA_PLM/Core%20Data%20Services/Data%20Definitions/ZD_CHANGEAPPROVAL_REJECTP/zd_changeapproval_rejectp.ddls.acds - ACTION : replace entire content, then activate. + ACTION : create empty via ADT MCP (objectType DDLS/DF, README U2), replace entire content, save, then activate. Notes: action input parameters must be a CDS abstract entity or DDIC type; @EndUserText.label on the element gives the UI label [source: abap-docs /abap-docs-cloud/ABENBDL_ACTION_INPUT_PARAM, verified 2026-09-15]; abstract-entity syntax [source: abap-docs /abap-docs-cloud/ABENBDL_ACTION2_ABEXA, verified 2026-09-15]. diff --git a/docs/handover/rap/zd_changeapproval_syncedp.ddls.asddls b/docs/handover/rap/zd_changeapproval_syncedp.ddls.asddls index 97f912f..99c5724 100644 --- a/docs/handover/rap/zd_changeapproval_syncedp.ddls.asddls +++ b/docs/handover/rap/zd_changeapproval_syncedp.ddls.asddls @@ -1,6 +1,8 @@ -/* TARGET OBJECT : data definition ZD_CHANGEAPPROVAL_SYNCEDP (abstract entity = parameter of action markSynced; created empty via ADT MCP 2026-09-15) +/* TARGET OBJECT : data definition ZD_CHANGEAPPROVAL_SYNCEDP (abstract entity = parameter of action markSynced) - README step U3, UI leg abap: URI : abap:/repotree-v1/0M7DEJI/System%20Library/ZLOCAL/ZARENA_PLM/Core%20Data%20Services/Data%20Definitions/ZD_CHANGEAPPROVAL_SYNCEDP/zd_changeapproval_syncedp.ddls.acds - ACTION : replace entire content, then activate. + ACTION : create empty via ADT MCP (objectType DDLS/DF, README U3), replace entire content, save, then activate. + Leg : UI leg although only the worker calls markSynced - the base BDEF ZR_CHANGEAPPROVAL (U6) names this + entity as the markSynced parameter, so it is ordered before that BDEF. Sources: as ZD_CHANGEAPPROVAL_REJECTP. */ @EndUserText.label: 'Arena change approval: markSynced parameter' define abstract entity ZD_CHANGEAPPROVAL_SYNCEDP diff --git a/docs/handover/rap/zzr_changeapproval000.bdef.asbdef b/docs/handover/rap/zr_changeapproval.bdef.asbdef similarity index 62% rename from docs/handover/rap/zzr_changeapproval000.bdef.asbdef rename to docs/handover/rap/zr_changeapproval.bdef.asbdef index 1df0a7e..0aa9850 100644 --- a/docs/handover/rap/zzr_changeapproval000.bdef.asbdef +++ b/docs/handover/rap/zr_changeapproval.bdef.asbdef @@ -1,6 +1,12 @@ -// TARGET OBJECT : behavior definition ZZR_CHANGEAPPROVAL000 (base BDEF, managed; generated 2026-09-15) -// abap: URI : abap:/repotree-v1/0M7DEJI/System%20Library/ZLOCAL/ZARENA_PLM/Core%20Data%20Services/Behavior%20Definitions/ZZR_CHANGEAPPROVAL000/zzr_changeapproval000.bdef.json -// ACTION : replace entire content, then activate (after the three ZD_* abstract entities and ZZR_CHANGEAPPROVAL000). +// TARGET OBJECT : behavior definition ZR_CHANGEAPPROVAL (base BDEF, managed) - README step U6, UI leg +// abap: URI : abap:/repotree-v1/0M7DEJI/System%20Library/ZLOCAL/ZARENA_PLM/Core%20Data%20Services/Behavior%20Definitions/ZR_CHANGEAPPROVAL/zr_changeapproval.bdef.json +// ACTION : create via ADT MCP (objectType BDEF/BDO, implementationType Managed, README U6) once the view +// ZR_CHANGEAPPROVAL is active, replace entire content, save, then activate BEFORE the behavior pool +// class exists. SAP's RAP exercise activates the BDEF first and only then creates the behavior +// implementation class [source: abap-docs /abap-platform-rap-opensap/week4/unit3#step-3-create-and-implement-the-behavior-implementation-bil-class-for-the-travel-entity, +// verified 2026-09-16]. The three ZD_* abstract entities (U2-U4) must be active. That exercise is an +// unmanaged, non-strict BDEF, so if activation refuses because class ZBP_R_CHANGEAPPROVAL does not exist +// (an error, not a warning), create the class (README U7), paste it, and activate BDEF + class in one call. // Sources (all verified 2026-09-15): // action syntax incl. ( features : instance ), parameter, result [1] $self // [abap-docs /abap-docs-cloud/ABENBDL_ACTION, ABENBDL_ACTIONS_FC, ABENBDL_ACTION_INPUT_PARAM, ABENBDL_ACTION2_ABEXA] @@ -11,16 +17,25 @@ // readonly fields written by determinations/actions IN LOCAL MODE [same source: field ( readonly ) AmountSum + TotalPrice determination] // etag master [abap-docs /abap-docs-cloud/ABENBDL_ETAG]; explicit mapping because CDS aliases differ from table fields [abap-docs /abap-docs-cloud/ABENBDL_TYPE_MAPPING] // authorization master ( none ): "no authorization checks for the entire RAP BO" - access is governed exclusively by the -// IAM app / business catalog (ADR-002, ARN-007) and read access by DCL [abap-docs /abap-docs-cloud/ABENBDL_AUTHORIZATION] +// IAM app / business catalog (ADR-002, ARN-007) and read access by DCL [abap-docs /abap-docs-cloud/ABENBDL_AUTHORIZATION]. +// NOT FINAL: ( none ) vs global authorization is open question PRD OQ-8, decided before business roles are assigned. +// Sources (verified 2026-09-16): +// validation ValName on save { create; } - invoked in the save sequence; a failing validation rejects the complete +// transactional buffer of the RAP transaction; EML MODIFY is not allowed in a validation +// [abap-docs /abap-docs-cloud/ABENBDL_VALIDATIONS] +// State machine (PRD ARN-002, M1 decision 2026-09-16): +// create -> PENDING; approve: PENDING -> APPROVED; reject: PENDING -> REJECTED (terminal); +// markExecuting: APPROVED -> EXECUTING; markSynced: EXECUTING -> SYNCED (terminal); markFailed: EXECUTING -> FAILED; +// retrySync: FAILED -> APPROVED (the worker re-claims it through markExecuting). Only markExecuting writes EXECUTING. // Open point (falsifiable at activation): StatusText/StatusCriticality/IsApprovable/IsRetriable are CDS-calculated and // deliberately absent from the mapping. ABENBDL_TYPE_MAPPING only requires a mapping where table and entity field // NAMES differ; it is silent on calculated elements. If activation reports an error (not a warning) for the unmapped // elements, fall back to persisting them (add the four columns to the table + mapping) and maintain them in the actions. -managed implementation in class zzbp_r_changeapproval000 unique; +managed implementation in class zbp_r_changeapproval unique; strict ( 2 ); -define behavior for ZZR_CHANGEAPPROVAL000 alias ChangeApproval -persistent table zzchngpproval000 +define behavior for ZR_CHANGEAPPROVAL alias ChangeApproval +persistent table zchangeapproval lock master authorization master ( none ) etag master LocalLastChangedAt @@ -36,7 +51,7 @@ etag master LocalLastChangedAt SapChangeNumber, ErrorMessage, RejectReason, CreatedBy, CreatedAt, LastChangedBy, LastChangedAt, LocalLastChangedAt; - // Business-user actions (UI projection): PENDING -> APPROVED | REJECTED, FAILED -> EXECUTING (PRD RAP-1/RAP-2) + // Business-user actions (UI projection): PENDING -> APPROVED | REJECTED, FAILED -> APPROVED (PRD RAP-1/RAP-2) action ( features : instance ) approve result [1] $self; action ( features : instance ) reject parameter ZD_CHANGEAPPROVAL_REJECTP result [1] $self; action ( features : instance ) retrySync result [1] $self; @@ -48,7 +63,10 @@ etag master LocalLastChangedAt determination setInitialStatus on modify { create; } - mapping for zzchngpproval000 + // Semantic key ItemNumber + ChangeType + ArenaEcoId (PRD RAP-8) is unique among non-REJECTED records + validation checkDuplicate on save { create; } + + mapping for zchangeapproval { ID = id; ItemNumber = item_number; diff --git a/docs/handover/rap/zzr_changeapproval000.dcls.asdcls b/docs/handover/rap/zr_changeapproval.dcls.asdcls similarity index 54% rename from docs/handover/rap/zzr_changeapproval000.dcls.asdcls rename to docs/handover/rap/zr_changeapproval.dcls.asdcls index 944ff91..9bbd67b 100644 --- a/docs/handover/rap/zzr_changeapproval000.dcls.asdcls +++ b/docs/handover/rap/zr_changeapproval.dcls.asdcls @@ -1,12 +1,13 @@ -/* TARGET OBJECT : access control ZZR_CHANGEAPPROVAL000 (generated 2026-09-15) - abap: URI : abap:/repotree-v1/0M7DEJI/System%20Library/ZLOCAL/ZARENA_PLM/Core%20Data%20Services/Access%20Controls/ZZR_CHANGEAPPROVAL000/zzr_changeapproval000.dcls.acds - ACTION : replace entire content, then activate. +/* TARGET OBJECT : access control ZR_CHANGEAPPROVAL - README step U10, UI leg + abap: URI : abap:/repotree-v1/0M7DEJI/System%20Library/ZLOCAL/ZARENA_PLM/Core%20Data%20Services/Access%20Controls/ZR_CHANGEAPPROVAL/zr_changeapproval.dcls.acds + ACTION : create empty via ADT MCP (objectType DCLS/DL, README U10), replace entire content, save, then activate + (view ZR_CHANGEAPPROVAL must be active). Full access rule: read access is governed by the IAM app / business catalog (ADR-002); SAP delivers no full-access roles but customers may use them [source: abap-docs /abap-docs-cloud/ABENCDS_DCL_ROLE_GRANT_RULE, verified 2026-09-15]. @MappingRole: true assigns the role to every user [source: abap-docs /abap-platform-rap-opensap/week2/unit7, verified 2026-09-15]. Tighten later with a custom authorization object + pfcg_auth condition if per-user data restrictions are needed. */ -@EndUserText.label: 'Access control for ZZR_CHANGEAPPROVAL000' +@EndUserText.label: 'Access control for ZR_CHANGEAPPROVAL' @MappingRole: true -define role ZZR_CHANGEAPPROVAL000 { - grant select on ZZR_CHANGEAPPROVAL000; +define role ZR_CHANGEAPPROVAL { + grant select on ZR_CHANGEAPPROVAL; } diff --git a/docs/handover/rap/zr_changeapproval.ddls.asddls b/docs/handover/rap/zr_changeapproval.ddls.asddls new file mode 100644 index 0000000..37784d8 --- /dev/null +++ b/docs/handover/rap/zr_changeapproval.ddls.asddls @@ -0,0 +1,100 @@ +/* TARGET OBJECT : data definition ZR_CHANGEAPPROVAL (root view entity) - README step U5, UI leg + abap: URI : abap:/repotree-v1/0M7DEJI/System%20Library/ZLOCAL/ZARENA_PLM/Core%20Data%20Services/Data%20Definitions/ZR_CHANGEAPPROVAL/zr_changeapproval.ddls.acds + ACTION : create empty via ADT MCP (objectType DDLS/DF, README U5), replace entire content, save, then activate + (table ZCHANGEAPPROVAL must be active first). + Notes: StatusText / StatusCriticality / IsApprovable / IsRetriable are derived from Status with CASE + expressions here, because a transactional_query projection list may only contain elements of the + projected entity, path expressions, associations and virtual elements - no expressions + [source: abap-docs /abap-docs-cloud/ABENCDS_DEFINE_VIEW_AS_PROJECTION, verified 2026-09-15]. + CASE syntax [source: abap-docs /abap-docs-cloud/ABENCDS_CASE_EXPRESSION_V2, verified 2026-09-15]; + cast of a case distinction to a data element (abap_boolean, released) [source: abap-docs /abap-docs-cloud/ABENCDS_CAST_EXPRESSION_V2, verified 2026-09-16]. + @ObjectModel.text.element [source: abap-docs /abap-fiori-showcase/06_object_page_content#displaying-text-instead-of-id, verified 2026-09-15]. + @Semantics admin-field annotations [source: abap-docs /abap-platform-rap-opensap/week2/unit3#step-1-create-the-interface-cds-view-for-the-travel-entity, verified 2026-09-15]. + Criticality values 2/2/5/3/1/1 per PRD RAP-1. + ABAP_BOOLEAN is released [source: sap-released-objects DTEL ABAP_BOOLEAN public_cloud level A, verified 2026-09-16]. + Result types of the CASE expressions: + - A CASE result gets the maximum technical data type of all THEN and ELSE branches, and keeps a data element + only if every branch with that type carries the same one [source: abap-docs /abap-docs-cloud/ABENCDS_CASE_EXPRESSION_V2, + verified 2026-09-16]. StatusText is therefore CHAR as long as its longest literal (29), not Status's CHAR(10). + - IsApprovable / IsRetriable cast the WHOLE case distinction to abap_boolean once. Casting inside the branches does + not work: a cast to a data element passes on its semantics, "An exception to this is the use of the CAST expression + within a case distinction" [source: abap-docs /abap-docs-cloud/ABENCDS_CAST_EXPRESSION_V2, verified 2026-09-16]. + - The branches use typed literals abap.char'X' / abap.char' ': length is "derived from the literal value" and "the type + length includes the trimmed trailing blanks", so both are CHAR(1) and the CASE result is CHAR(1) [source: abap-docs + /abap-docs-cloud/ABENCDS_TYPED_LITERAL_V2, verified 2026-09-16]. (Untyped 'X' would not prove it: its DB length is only + "at least as large as" the literal [source: abap-docs /abap-docs-cloud/ABENCDS_UNTYPED_LITERAL_V2, verified 2026-09-16].) + - PRESERVING TYPE needs the operand's built-in type and length to match the data element exactly [source: abap-docs + /abap-docs-cloud/ABENCDS_CAST_EXPRESSION_V2, verified 2026-09-16]. ABAP_BOOLEAN is "of data type CHAR of length 1" + [source: abap-docs /abap-docs-standard/ABENDATAOBJECTS_TRUE_VALUE_GUIDL, verified 2026-09-16], so the types match. + If activation rejects the cast anyway, delete + "preserving type": a plain cast still inherits the data element's semantics, and a same-type warning "can be ignored" + [source: abap-docs /abap-docs-cloud/ABENCDS_CAST_EXPRESSION_V2, verified 2026-09-16]. + - An untyped integer literal is INT1, INT2, INT4 or INT8 depending on its value [source: abap-docs + /abap-docs-cloud/ABENCDS_UNTYPED_LITERAL_V2, verified 2026-09-16], so StatusCriticality (0..5) is expected INT1. + Open (UNVERIFIED 2026-09-16 - no doc found): the OData EDM types the UI binding emits for INT1 (app expects + Edm.Byte), ABAP_BOOLEAN (app expects Edm.Boolean) and UTCLONG (app expects Edm.DateTimeOffset Precision 7). + Read them from the live $metadata after publishing (README step U15); do not change this source without a cited rule. */ +@AccessControl.authorizationCheck: #CHECK +@EndUserText.label: 'Arena change approval' +define root view entity ZR_CHANGEAPPROVAL + as select from zchangeapproval +{ + key id as ID, + item_number as ItemNumber, + item_name as ItemName, + change_type as ChangeType, + arena_eco_id as ArenaEcoId, + + @ObjectModel.text.element: ['StatusText'] + status as Status, + + case status + when 'PENDING' then 'Pending approval' + when 'APPROVED' then 'Approved - awaiting execution' + when 'EXECUTING' then 'Executing in SAP' + when 'SYNCED' then 'Synced to SAP' + when 'REJECTED' then 'Rejected' + when 'FAILED' then 'Execution failed' + else status + end as StatusText, + + case status + when 'PENDING' then 2 + when 'APPROVED' then 2 + when 'EXECUTING' then 5 + when 'SYNCED' then 3 + when 'REJECTED' then 1 + when 'FAILED' then 1 + else 0 + end as StatusCriticality, + + requested_by as RequestedBy, + requested_on as RequestedOn, + decided_by as DecidedBy, + decided_on as DecidedOn, + sap_change_number as SapChangeNumber, + error_message as ErrorMessage, + reject_reason as RejectReason, + sap_preview as SapPreview, + + cast( case status + when 'PENDING' then abap.char'X' + else abap.char' ' + end as abap_boolean preserving type ) as IsApprovable, + + cast( case status + when 'FAILED' then abap.char'X' + else abap.char' ' + end as abap_boolean preserving type ) as IsRetriable, + + @Semantics.user.createdBy: true + created_by as CreatedBy, + @Semantics.systemDateTime.createdAt: true + created_at as CreatedAt, + @Semantics.user.lastChangedBy: true + last_changed_by as LastChangedBy, + @Semantics.systemDateTime.lastChangedAt: true + last_changed_at as LastChangedAt, + @Semantics.systemDateTime.localInstanceLastChangedAt: true + local_last_changed_at as LocalLastChangedAt +} diff --git a/docs/handover/rap/zui_arena_change_approval.srvd.srvdsrv b/docs/handover/rap/zui_arena_change_approval.srvd.srvdsrv new file mode 100644 index 0000000..de2fabd --- /dev/null +++ b/docs/handover/rap/zui_arena_change_approval.srvd.srvdsrv @@ -0,0 +1,6 @@ +@EndUserText.label: 'Arena change approval - UI service' +define service ZUI_ARENA_CHANGE_APPROVAL + provider contracts odata_v4_ui +{ + expose ZC_CHANGEAPPROVAL as ChangeApproval; +} diff --git a/docs/handover/rap/zzbp_r_changeapproval000.clas.abap b/docs/handover/rap/zzbp_r_changeapproval000.clas.abap deleted file mode 100644 index 27d11c6..0000000 --- a/docs/handover/rap/zzbp_r_changeapproval000.clas.abap +++ /dev/null @@ -1,14 +0,0 @@ -" TARGET OBJECT : class ZZBP_R_CHANGEAPPROVAL000 - GLOBAL CLASS include (behavior pool; generated 2026-09-15) -" abap: URI : abap:/repotree-v1/0M7DEJI/System%20Library/ZLOCAL/ZARENA_PLM/Source%20Code%20Library/Classes/ZZBP_R_CHANGEAPPROVAL000/zzbp_r_changeapproval000.clas.abap -" ACTION : replace entire content of the global class; the handlers go into "Local Types" (locals_imp file). -" Source: FOR BEHAVIOR OF defines the ABAP behavior pool; the global class implements nothing itself -" [source: abap-docs /abap-docs-cloud/ABAPCLASS_FOR_BEHAVIOR_OF, ABENABAP_BEHAVIOR_POOLS, verified 2026-09-15]. -CLASS zzbp_r_changeapproval000 DEFINITION - PUBLIC - ABSTRACT - FINAL - FOR BEHAVIOR OF zzr_changeapproval000. -ENDCLASS. - -CLASS zzbp_r_changeapproval000 IMPLEMENTATION. -ENDCLASS. diff --git a/docs/handover/rap/zzbp_r_changeapproval000.clas.locals_imp.abap b/docs/handover/rap/zzbp_r_changeapproval000.clas.locals_imp.abap deleted file mode 100644 index 248a1a5..0000000 --- a/docs/handover/rap/zzbp_r_changeapproval000.clas.locals_imp.abap +++ /dev/null @@ -1,426 +0,0 @@ -" TARGET OBJECT : class ZZBP_R_CHANGEAPPROVAL000 - LOCAL TYPES include (Local Types tab in ADT) -" abap: URI : abap:/repotree-v1/0M7DEJI/System%20Library/ZLOCAL/ZARENA_PLM/Source%20Code%20Library/Classes/ZZBP_R_CHANGEAPPROVAL000/zzbp_r_changeapproval000.clas.locals_imp.abap -" ACTION : replace entire content, then activate the class (BDEF ZZR_CHANGEAPPROVAL000 must be active first). -" -" Sources (all verified 2026-09-15): -" handler class inherits from CL_ABAP_BEHAVIOR_HANDLER (released) [sap-released-objects public_cloud A] -" FOR INSTANCE FEATURES signature + result via %tky / %features-%action- / if_abap_behv=>fc-o-enabled|disabled -" [abap-docs /abap-docs-cloud/ABAPHANDLER_METH_FEATURES, ABAPDERIVED_TYPES_FEATURES, ABENBDL_ACTIONS_FC] -" FOR MODIFY ... FOR ACTION with RESULT; %param carries the action parameter (keys) and the result ($self) -" [abap-docs /abap-docs-cloud/ABENBDL_ACTION2_ABEXA, ABAPDERIVED_TYPES_PARAM] -" READ ENTITIES ... IN LOCAL MODE ... FIELDS ( ) WITH CORRESPONDING #( keys ) RESULT ... FAILED failed -" [abap-docs /abap-docs-cloud/ABENBDL_ACTIONS_FC example] -" MODIFY ENTITIES ... IN LOCAL MODE (skips feature control and authorization) with UPDATE FIELDS -" [abap-docs /abap-docs-cloud/ABAPMODIFY_ENTITY_ENTITIES, ABAPMODIFY_ENTITY_ENTITIES_OP, ABENBDL_AUTHORIZATION] -" %msg = new_message_with_text( ... ) into reported [abap-docs /abap-docs-cloud/ABENBDL_ACTIONS_FC example] -" FOR DETERMINE ON MODIFY signature [abap-docs /abap-docs-cloud/ABAPHANDLER_METH_DET] -" utclong_current( ) [abap-docs /abap-docs-cloud/ABENUTCLONG_CURRENT] -" cl_abap_context_info=>get_user_technical_name( ) (released class) [sap-docs sap-help loio 7be24435b3e24540a9edad9a2535175a; -" sap-released-objects CL_ABAP_CONTEXT_INFO public_cloud A] -" Design notes: -" - Status transitions are enforced twice: get_instance_features disables the action (UI greys it out / EML fails -" with an entry in failed [ABENBDL_ACTIONS_FC]) AND each action re-checks the current status so an EML caller -" IN LOCAL MODE cannot skip the state machine. -" - The six actions repeat the same read/check/update/read pattern on purpose: each action has its own derived -" key type, so a generic helper would need additional derived-type plumbing that is not worth it for six methods. -" - RequestedBy is the technical user of the creator (for the A2X flow: the worker's communication user). -" If the Arena requester should be shown instead, make RequestedBy mandatory:create in the BDEF and drop it -" from the determination. - -CLASS lhc_changeapproval DEFINITION INHERITING FROM cl_abap_behavior_handler. - PRIVATE SECTION. - - CONSTANTS: - BEGIN OF status, - pending TYPE zzchngpproval000-status VALUE 'PENDING', - approved TYPE zzchngpproval000-status VALUE 'APPROVED', - executing TYPE zzchngpproval000-status VALUE 'EXECUTING', - synced TYPE zzchngpproval000-status VALUE 'SYNCED', - rejected TYPE zzchngpproval000-status VALUE 'REJECTED', - failed TYPE zzchngpproval000-status VALUE 'FAILED', - END OF status. - - METHODS get_instance_features FOR INSTANCE FEATURES - IMPORTING keys REQUEST requested_features FOR ChangeApproval RESULT result. - - METHODS approve FOR MODIFY - IMPORTING keys FOR ACTION ChangeApproval~approve RESULT result. - - METHODS reject FOR MODIFY - IMPORTING keys FOR ACTION ChangeApproval~reject RESULT result. - - METHODS retrySync FOR MODIFY - IMPORTING keys FOR ACTION ChangeApproval~retrySync RESULT result. - - METHODS markExecuting FOR MODIFY - IMPORTING keys FOR ACTION ChangeApproval~markExecuting RESULT result. - - METHODS markSynced FOR MODIFY - IMPORTING keys FOR ACTION ChangeApproval~markSynced RESULT result. - - METHODS markFailed FOR MODIFY - IMPORTING keys FOR ACTION ChangeApproval~markFailed RESULT result. - - METHODS setInitialStatus FOR DETERMINE ON MODIFY - IMPORTING keys FOR ChangeApproval~setInitialStatus. - -ENDCLASS. - - -CLASS lhc_changeapproval IMPLEMENTATION. - - METHOD get_instance_features. - READ ENTITIES OF zzr_changeapproval000 IN LOCAL MODE - ENTITY ChangeApproval - FIELDS ( Status ) - WITH CORRESPONDING #( keys ) - RESULT DATA(approvals) - FAILED failed. - - result = VALUE #( FOR approval IN approvals - ( %tky = approval-%tky - %features-%action-approve = COND #( WHEN approval-Status = status-pending - THEN if_abap_behv=>fc-o-enabled - ELSE if_abap_behv=>fc-o-disabled ) - %features-%action-reject = COND #( WHEN approval-Status = status-pending - THEN if_abap_behv=>fc-o-enabled - ELSE if_abap_behv=>fc-o-disabled ) - %features-%action-retrySync = COND #( WHEN approval-Status = status-failed - THEN if_abap_behv=>fc-o-enabled - ELSE if_abap_behv=>fc-o-disabled ) - %features-%action-markExecuting = COND #( WHEN approval-Status = status-approved - THEN if_abap_behv=>fc-o-enabled - ELSE if_abap_behv=>fc-o-disabled ) - %features-%action-markSynced = COND #( WHEN approval-Status = status-executing - THEN if_abap_behv=>fc-o-enabled - ELSE if_abap_behv=>fc-o-disabled ) - %features-%action-markFailed = COND #( WHEN approval-Status = status-executing - THEN if_abap_behv=>fc-o-enabled - ELSE if_abap_behv=>fc-o-disabled ) ) ). - ENDMETHOD. - - - METHOD approve. - " PENDING -> APPROVED; records the decision, never writes SapChangeNumber (PRD RAP-1) - READ ENTITIES OF zzr_changeapproval000 IN LOCAL MODE - ENTITY ChangeApproval - FIELDS ( Status ) - WITH CORRESPONDING #( keys ) - RESULT DATA(approvals) - FAILED failed. - - LOOP AT keys INTO DATA(key). - ASSIGN approvals[ ID = key-ID ] TO FIELD-SYMBOL(). - IF sy-subrc <> 0. - CONTINUE. - ENDIF. - - IF -Status <> status-pending. - APPEND VALUE #( %tky = -%tky ) TO failed-changeapproval. - APPEND VALUE #( %tky = -%tky - %msg = new_message_with_text( - severity = if_abap_behv_message=>severity-error - text = |Only PENDING records can be approved (status is { -Status })| ) ) - TO reported-changeapproval. - CONTINUE. - ENDIF. - - MODIFY ENTITIES OF zzr_changeapproval000 IN LOCAL MODE - ENTITY ChangeApproval - UPDATE FIELDS ( Status DecidedBy DecidedOn ) - WITH VALUE #( ( %tky = -%tky - Status = status-approved - DecidedBy = cl_abap_context_info=>get_user_technical_name( ) - DecidedOn = utclong_current( ) ) ) - FAILED DATA(update_failed) - REPORTED DATA(update_reported). - ENDLOOP. - - READ ENTITIES OF zzr_changeapproval000 IN LOCAL MODE - ENTITY ChangeApproval - ALL FIELDS WITH CORRESPONDING #( keys ) - RESULT DATA(updated). - - result = VALUE #( FOR row IN updated ( %tky = row-%tky %param = row ) ). - ENDMETHOD. - - - METHOD reject. - " PENDING -> REJECTED with a mandatory reason (PRD RAP-2); REJECTED is terminal - READ ENTITIES OF zzr_changeapproval000 IN LOCAL MODE - ENTITY ChangeApproval - FIELDS ( Status ) - WITH CORRESPONDING #( keys ) - RESULT DATA(approvals) - FAILED failed. - - LOOP AT keys INTO DATA(key). - ASSIGN approvals[ ID = key-ID ] TO FIELD-SYMBOL(). - IF sy-subrc <> 0. - CONTINUE. - ENDIF. - - IF -Status <> status-pending. - APPEND VALUE #( %tky = -%tky ) TO failed-changeapproval. - APPEND VALUE #( %tky = -%tky - %msg = new_message_with_text( - severity = if_abap_behv_message=>severity-error - text = |Only PENDING records can be rejected (status is { -Status })| ) ) - TO reported-changeapproval. - CONTINUE. - ENDIF. - - IF key-%param-RejectReason IS INITIAL. - APPEND VALUE #( %tky = -%tky ) TO failed-changeapproval. - APPEND VALUE #( %tky = -%tky - %msg = new_message_with_text( - severity = if_abap_behv_message=>severity-error - text = 'A reject reason is required' ) ) - TO reported-changeapproval. - CONTINUE. - ENDIF. - - MODIFY ENTITIES OF zzr_changeapproval000 IN LOCAL MODE - ENTITY ChangeApproval - UPDATE FIELDS ( Status RejectReason DecidedBy DecidedOn ) - WITH VALUE #( ( %tky = -%tky - Status = status-rejected - RejectReason = key-%param-RejectReason - DecidedBy = cl_abap_context_info=>get_user_technical_name( ) - DecidedOn = utclong_current( ) ) ) - FAILED DATA(update_failed) - REPORTED DATA(update_reported). - ENDLOOP. - - READ ENTITIES OF zzr_changeapproval000 IN LOCAL MODE - ENTITY ChangeApproval - ALL FIELDS WITH CORRESPONDING #( keys ) - RESULT DATA(updated). - - result = VALUE #( FOR row IN updated ( %tky = row-%tky %param = row ) ). - ENDMETHOD. - - - METHOD retrySync. - " FAILED -> EXECUTING; clears the previous error so the worker re-executes the staged payload (ARN-002 amendment) - READ ENTITIES OF zzr_changeapproval000 IN LOCAL MODE - ENTITY ChangeApproval - FIELDS ( Status ) - WITH CORRESPONDING #( keys ) - RESULT DATA(approvals) - FAILED failed. - - LOOP AT keys INTO DATA(key). - ASSIGN approvals[ ID = key-ID ] TO FIELD-SYMBOL(). - IF sy-subrc <> 0. - CONTINUE. - ENDIF. - - IF -Status <> status-failed. - APPEND VALUE #( %tky = -%tky ) TO failed-changeapproval. - APPEND VALUE #( %tky = -%tky - %msg = new_message_with_text( - severity = if_abap_behv_message=>severity-error - text = |Only FAILED records can be retried (status is { -Status })| ) ) - TO reported-changeapproval. - CONTINUE. - ENDIF. - - MODIFY ENTITIES OF zzr_changeapproval000 IN LOCAL MODE - ENTITY ChangeApproval - UPDATE FIELDS ( Status ErrorMessage ) - WITH VALUE #( ( %tky = -%tky - Status = status-executing - ErrorMessage = '' ) ) - FAILED DATA(update_failed) - REPORTED DATA(update_reported). - ENDLOOP. - - READ ENTITIES OF zzr_changeapproval000 IN LOCAL MODE - ENTITY ChangeApproval - ALL FIELDS WITH CORRESPONDING #( keys ) - RESULT DATA(updated). - - result = VALUE #( FOR row IN updated ( %tky = row-%tky %param = row ) ). - ENDMETHOD. - - - METHOD markExecuting. - " APPROVED -> EXECUTING (worker claims the record; the UI never offers Approve on an executing record, PRD RAP-6) - READ ENTITIES OF zzr_changeapproval000 IN LOCAL MODE - ENTITY ChangeApproval - FIELDS ( Status ) - WITH CORRESPONDING #( keys ) - RESULT DATA(approvals) - FAILED failed. - - LOOP AT keys INTO DATA(key). - ASSIGN approvals[ ID = key-ID ] TO FIELD-SYMBOL(). - IF sy-subrc <> 0. - CONTINUE. - ENDIF. - - IF -Status <> status-approved. - APPEND VALUE #( %tky = -%tky ) TO failed-changeapproval. - APPEND VALUE #( %tky = -%tky - %msg = new_message_with_text( - severity = if_abap_behv_message=>severity-error - text = |Only APPROVED records can start executing (status is { -Status })| ) ) - TO reported-changeapproval. - CONTINUE. - ENDIF. - - MODIFY ENTITIES OF zzr_changeapproval000 IN LOCAL MODE - ENTITY ChangeApproval - UPDATE FIELDS ( Status ) - WITH VALUE #( ( %tky = -%tky - Status = status-executing ) ) - FAILED DATA(update_failed) - REPORTED DATA(update_reported). - ENDLOOP. - - READ ENTITIES OF zzr_changeapproval000 IN LOCAL MODE - ENTITY ChangeApproval - ALL FIELDS WITH CORRESPONDING #( keys ) - RESULT DATA(updated). - - result = VALUE #( FOR row IN updated ( %tky = row-%tky %param = row ) ). - ENDMETHOD. - - - METHOD markSynced. - " EXECUTING -> SYNCED (terminal success); SAP is the ledger, the change number is the receipt (ARN-006) - READ ENTITIES OF zzr_changeapproval000 IN LOCAL MODE - ENTITY ChangeApproval - FIELDS ( Status ) - WITH CORRESPONDING #( keys ) - RESULT DATA(approvals) - FAILED failed. - - LOOP AT keys INTO DATA(key). - ASSIGN approvals[ ID = key-ID ] TO FIELD-SYMBOL(). - IF sy-subrc <> 0. - CONTINUE. - ENDIF. - - IF -Status <> status-executing. - APPEND VALUE #( %tky = -%tky ) TO failed-changeapproval. - APPEND VALUE #( %tky = -%tky - %msg = new_message_with_text( - severity = if_abap_behv_message=>severity-error - text = |Only EXECUTING records can be marked synced (status is { -Status })| ) ) - TO reported-changeapproval. - CONTINUE. - ENDIF. - - IF key-%param-SapChangeNumber IS INITIAL. - APPEND VALUE #( %tky = -%tky ) TO failed-changeapproval. - APPEND VALUE #( %tky = -%tky - %msg = new_message_with_text( - severity = if_abap_behv_message=>severity-error - text = 'The SAP change number is required to mark a record synced' ) ) - TO reported-changeapproval. - CONTINUE. - ENDIF. - - MODIFY ENTITIES OF zzr_changeapproval000 IN LOCAL MODE - ENTITY ChangeApproval - UPDATE FIELDS ( Status SapChangeNumber ErrorMessage ) - WITH VALUE #( ( %tky = -%tky - Status = status-synced - SapChangeNumber = key-%param-SapChangeNumber - ErrorMessage = '' ) ) - FAILED DATA(update_failed) - REPORTED DATA(update_reported). - ENDLOOP. - - READ ENTITIES OF zzr_changeapproval000 IN LOCAL MODE - ENTITY ChangeApproval - ALL FIELDS WITH CORRESPONDING #( keys ) - RESULT DATA(updated). - - result = VALUE #( FOR row IN updated ( %tky = row-%tky %param = row ) ). - ENDMETHOD. - - - METHOD markFailed. - " EXECUTING -> FAILED with the error text; distinct from REJECTED (predecessor defect, ARN-002) - READ ENTITIES OF zzr_changeapproval000 IN LOCAL MODE - ENTITY ChangeApproval - FIELDS ( Status ) - WITH CORRESPONDING #( keys ) - RESULT DATA(approvals) - FAILED failed. - - LOOP AT keys INTO DATA(key). - ASSIGN approvals[ ID = key-ID ] TO FIELD-SYMBOL(). - IF sy-subrc <> 0. - CONTINUE. - ENDIF. - - IF -Status <> status-executing. - APPEND VALUE #( %tky = -%tky ) TO failed-changeapproval. - APPEND VALUE #( %tky = -%tky - %msg = new_message_with_text( - severity = if_abap_behv_message=>severity-error - text = |Only EXECUTING records can be marked failed (status is { -Status })| ) ) - TO reported-changeapproval. - CONTINUE. - ENDIF. - - IF key-%param-ErrorMessage IS INITIAL. - APPEND VALUE #( %tky = -%tky ) TO failed-changeapproval. - APPEND VALUE #( %tky = -%tky - %msg = new_message_with_text( - severity = if_abap_behv_message=>severity-error - text = 'An error message is required to mark a record failed' ) ) - TO reported-changeapproval. - CONTINUE. - ENDIF. - - MODIFY ENTITIES OF zzr_changeapproval000 IN LOCAL MODE - ENTITY ChangeApproval - UPDATE FIELDS ( Status ErrorMessage ) - WITH VALUE #( ( %tky = -%tky - Status = status-failed - ErrorMessage = key-%param-ErrorMessage ) ) - FAILED DATA(update_failed) - REPORTED DATA(update_reported). - ENDLOOP. - - READ ENTITIES OF zzr_changeapproval000 IN LOCAL MODE - ENTITY ChangeApproval - ALL FIELDS WITH CORRESPONDING #( keys ) - RESULT DATA(updated). - - result = VALUE #( FOR row IN updated ( %tky = row-%tky %param = row ) ). - ENDMETHOD. - - - METHOD setInitialStatus. - " Every created record starts PENDING with the requester audit fields (ARN-002) - READ ENTITIES OF zzr_changeapproval000 IN LOCAL MODE - ENTITY ChangeApproval - FIELDS ( Status ) - WITH CORRESPONDING #( keys ) - RESULT DATA(approvals). - - DELETE approvals WHERE Status IS NOT INITIAL. - IF approvals IS INITIAL. - RETURN. - ENDIF. - - MODIFY ENTITIES OF zzr_changeapproval000 IN LOCAL MODE - ENTITY ChangeApproval - UPDATE FIELDS ( Status RequestedBy RequestedOn ) - WITH VALUE #( FOR approval IN approvals - ( %tky = approval-%tky - Status = status-pending - RequestedBy = cl_abap_context_info=>get_user_technical_name( ) - RequestedOn = utclong_current( ) ) ) - REPORTED DATA(update_reported). - - reported = CORRESPONDING #( DEEP update_reported ). - ENDMETHOD. - -ENDCLASS. diff --git a/docs/handover/rap/zzc_changeapproval000.bdef.asbdef b/docs/handover/rap/zzc_changeapproval000.bdef.asbdef deleted file mode 100644 index 9856d5a..0000000 --- a/docs/handover/rap/zzc_changeapproval000.bdef.asbdef +++ /dev/null @@ -1,20 +0,0 @@ -// TARGET OBJECT : behavior definition ZZC_CHANGEAPPROVAL000 (UI projection BDEF; generated 2026-09-15) -// abap: URI : abap:/repotree-v1/0M7DEJI/System%20Library/ZLOCAL/ZARENA_PLM/Core%20Data%20Services/Behavior%20Definitions/ZZC_CHANGEAPPROVAL000/zzc_changeapproval000.bdef.json -// ACTION : replace entire content, then activate (after ZZR_CHANGEAPPROVAL000 BDEF + ZZC_CHANGEAPPROVAL000 view). -// Then DELETE the generated class ZZBP_C_CHANGEAPPROVAL000 in ADT: a projection BDEF needs no behavior -// pool unless it defines new actions/functions, and this one reuses everything -// [source: abap-docs /abap-docs-cloud/ABENBDL_BDEF_PROJECTION_HEADER, verified 2026-09-15]. -// Sources (verified 2026-09-15): use action / use etag; no automatic inheritance - only what is listed is exposed, -// which is how create/update/delete and the worker actions are kept out of the UI service (PRD RAP-2) -// [abap-docs /abap-docs-cloud/ABENBDL_USE_PROJECTION]. Feature control of reused actions is passed on automatically (same source). -projection; -strict ( 2 ); - -define behavior for ZZC_CHANGEAPPROVAL000 alias ChangeApproval -{ - use etag; - - use action approve; - use action reject; - use action retrySync; -} diff --git a/docs/handover/rap/zzc_changeapproval000.dcls.asdcls b/docs/handover/rap/zzc_changeapproval000.dcls.asdcls deleted file mode 100644 index cb31d02..0000000 --- a/docs/handover/rap/zzc_changeapproval000.dcls.asdcls +++ /dev/null @@ -1,12 +0,0 @@ -/* TARGET OBJECT : access control ZZC_CHANGEAPPROVAL000 (generated 2026-09-15) - abap: URI : abap:/repotree-v1/0M7DEJI/System%20Library/ZLOCAL/ZARENA_PLM/Core%20Data%20Services/Access%20Controls/ZZC_CHANGEAPPROVAL000/zzc_changeapproval000.dcls.acds - ACTION : replace entire content, then activate (after ZZR_CHANGEAPPROVAL000 DCL). - A transactional query needs its own DCL, inheriting the base view's conditions - there is no implicit inheritance - [source: abap-docs /abap-platform-rap-opensap/week2/unit7#step-5-create-cds-access-control-for-travel-bo-projection-view-and-check-the-result; - /abap-docs-cloud/ABENCDS_DEFINE_VIEW_AS_PROJECTION (access control must be symmetric), verified 2026-09-15]. */ -@EndUserText.label: 'Access control for ZZC_CHANGEAPPROVAL000' -@MappingRole: true -define role ZZC_CHANGEAPPROVAL000 { - grant select on ZZC_CHANGEAPPROVAL000 - where inheriting conditions from entity ZZR_CHANGEAPPROVAL000; -} diff --git a/docs/handover/rap/zzr_changeapproval000.ddls.asddls b/docs/handover/rap/zzr_changeapproval000.ddls.asddls deleted file mode 100644 index 9ad4310..0000000 --- a/docs/handover/rap/zzr_changeapproval000.ddls.asddls +++ /dev/null @@ -1,76 +0,0 @@ -/* TARGET OBJECT : data definition ZZR_CHANGEAPPROVAL000 (root view entity, generated 2026-09-15) - abap: URI : abap:/repotree-v1/0M7DEJI/System%20Library/ZLOCAL/ZARENA_PLM/Core%20Data%20Services/Data%20Definitions/ZZR_CHANGEAPPROVAL000/zzr_changeapproval000.ddls.acds - ACTION : replace entire content, then activate (table ZZCHNGPPROVAL000 must be active first). - Notes: StatusText / StatusCriticality / IsApprovable / IsRetriable are derived from Status with CASE - expressions here, because a transactional_query projection list may only contain elements of the - projected entity, path expressions, associations and virtual elements - no expressions - [source: abap-docs /abap-docs-cloud/ABENCDS_DEFINE_VIEW_AS_PROJECTION, verified 2026-09-15]. - CASE syntax [source: abap-docs /abap-docs-cloud/ABENCDS_CASE_EXPRESSION_V2, verified 2026-09-15]; - cast of a literal to a data element (abap_boolean, released) [source: abap-docs /abap-docs-cloud/ABENCDS_CAST_EXPRESSION_V2, verified 2026-09-15]. - @ObjectModel.text.element [source: abap-docs /abap-fiori-showcase/06_object_page_content#displaying-text-instead-of-id, verified 2026-09-15]. - @Semantics admin-field annotations [source: abap-docs /abap-platform-rap-opensap/week2/unit3#step-1-create-the-interface-cds-view-for-the-travel-entity, verified 2026-09-15]. - Criticality values 2/2/5/3/1/1 per PRD RAP-1. */ -@AccessControl.authorizationCheck: #CHECK -@EndUserText.label: 'Arena change approval' -define root view entity ZZR_CHANGEAPPROVAL000 - as select from zzchngpproval000 -{ - key id as ID, - item_number as ItemNumber, - item_name as ItemName, - change_type as ChangeType, - arena_eco_id as ArenaEcoId, - - @ObjectModel.text.element: ['StatusText'] - status as Status, - - case status - when 'PENDING' then 'Pending approval' - when 'APPROVED' then 'Approved - awaiting execution' - when 'EXECUTING' then 'Executing in SAP' - when 'SYNCED' then 'Synced to SAP' - when 'REJECTED' then 'Rejected' - when 'FAILED' then 'Execution failed' - else status - end as StatusText, - - case status - when 'PENDING' then 2 - when 'APPROVED' then 2 - when 'EXECUTING' then 5 - when 'SYNCED' then 3 - when 'REJECTED' then 1 - when 'FAILED' then 1 - else 0 - end as StatusCriticality, - - requested_by as RequestedBy, - requested_on as RequestedOn, - decided_by as DecidedBy, - decided_on as DecidedOn, - sap_change_number as SapChangeNumber, - error_message as ErrorMessage, - reject_reason as RejectReason, - sap_preview as SapPreview, - - case status - when 'PENDING' then cast( 'X' as abap_boolean ) - else cast( ' ' as abap_boolean ) - end as IsApprovable, - - case status - when 'FAILED' then cast( 'X' as abap_boolean ) - else cast( ' ' as abap_boolean ) - end as IsRetriable, - - @Semantics.user.createdBy: true - created_by as CreatedBy, - @Semantics.systemDateTime.createdAt: true - created_at as CreatedAt, - @Semantics.user.lastChangedBy: true - last_changed_by as LastChangedBy, - @Semantics.systemDateTime.lastChangedAt: true - last_changed_at as LastChangedAt, - @Semantics.systemDateTime.localInstanceLastChangedAt: true - local_last_changed_at as LocalLastChangedAt -} diff --git a/docs/handover/rap/zzui_arena_change_apprvl000_o4.srvd.srvdsrv b/docs/handover/rap/zzui_arena_change_apprvl000_o4.srvd.srvdsrv deleted file mode 100644 index 8c106c5..0000000 --- a/docs/handover/rap/zzui_arena_change_apprvl000_o4.srvd.srvdsrv +++ /dev/null @@ -1,13 +0,0 @@ -/* TARGET OBJECT : service definition ZZUI_ARENA_CHANGE_APPRVL000_O4 (generated 2026-09-15) - abap: URI : abap:/repotree-v1/0M7DEJI/System%20Library/ZLOCAL/ZARENA_PLM/Business%20Services/Service%20Definitions/ZZUI_ARENA_CHANGE_APPRVL000_O4/zzui_arena_change_apprvl000_o4.srvd.acds - ACTION : replace entire content, then activate (after ZZC_CHANGEAPPROVAL000 view + BDEF). - expose ... as : the alias becomes the entity set (ChangeApproval) and the entity type (ChangeApproval_Type) - [source: abap-docs /abap-fiori-showcase/06_object_page_content (Service Definition), verified 2026-09-15]. - provider contracts odata_v4_ui switches on the UI-binding syntax checks and must match the binding type - [source: abap-docs /abap-docs-cloud/ABENSRVD_PROVIDER_CONTRACT, verified 2026-09-15]. */ -@EndUserText.label: 'Arena change approval - UI service' -define service ZZUI_ARENA_CHANGE_APPRVL000_O4 - provider contracts odata_v4_ui -{ - expose ZZC_CHANGEAPPROVAL000 as ChangeApproval; -} diff --git a/docs/v2/zui_arena_change_appr_o4.metadata.xml b/docs/v2/zui_arena_change_appr_o4.metadata.xml new file mode 100644 index 0000000..5096540 --- /dev/null +++ b/docs/v2/zui_arena_change_appr_o4.metadata.xml @@ -0,0 +1,593 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +ArenaEcoId +ChangeType +ItemNumber + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +@SAP__UI.LineItem + + + + + + +ItemNumber +Status +ChangeType +ArenaEcoId + + + + + + + + + + + + + + + + + + + + + + + +__OperationControl + + + + + + + + +SapPreview +__OperationControl + + + + + + + + + + + + + + + + + + + + + + + + + + +LocalLastChangedAt + + + + + + + +eq +ne +gt +ge +lt +le +and +or +contains +startswith +endswith +any +all + + + + + + + + + + +application/json +application/pdf + + + + + + + + + + + + + + + + + + + + + + + + + + + + +application/json +multipart/mixed + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/download.php?file=%2Ftechnology%2Fepp%2Fdownloads%2Frelease%2F2026-09%2FR%2Feclipse-java-2026-09-R-win32-x86_64.zip b/download.php?file=%2Ftechnology%2Fepp%2Fdownloads%2Frelease%2F2026-09%2FR%2Feclipse-java-2026-09-R-win32-x86_64.zip deleted file mode 100644 index 2340dee..0000000 --- a/download.php?file=%2Ftechnology%2Fepp%2Fdownloads%2Frelease%2F2026-09%2FR%2Feclipse-java-2026-09-R-win32-x86_64.zip +++ /dev/null @@ -1,653 +0,0 @@ - - - - - - - - - - - - Eclipse downloads - Select a mirror | The Eclipse Foundation - - - - - - - - - - - - - - - - - - - - - Skip to main content -
-
- -
-
- -
-
- -
-
-
-

All downloads are provided under the terms and conditions of the Eclipse Foundation - Software User Agreement unless otherwise specified.

-
-
-
- Download - -

Download from: Canada - Rafal Rzeczkowski (https)

-

File: eclipse-java-2026-09-R-win32-x86_64.zip SHA-512

-
-
-
-

-
-
-
-

-
-
- -

Choose a mirror close to you

-
-
-
-

North America

- -
-
-

Europe

- -
- -
-

Australia/Oceania

- -
-
-

South America

- -
- Show all -
-
-

Main Download Site

- -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Other options for this file
- -
- -
- -
-
-
-
-
- - - - - - - - diff --git a/tools/adt-reentrance-login/README.md b/tools/adt-reentrance-login/README.md new file mode 100644 index 0000000..b0d8733 --- /dev/null +++ b/tools/adt-reentrance-login/README.md @@ -0,0 +1,232 @@ +# adt-reentrance-login + +Capture an SAP **ADT reentrance ticket** through a **localhost redirect**, so +`abap-adt-mcp` can log on to the S/4HANA Cloud development tenant from this +headless / SSH-only host — without a browser window on the server's desktop. + +Node 24, standard library only, no dependencies, no install step. + +--- + +## Why this exists + +`abap-adt-mcp` 2.1.0 can do browser SSO, but only by launching a **server-side** +Chrome window (`headless: false`) and harvesting cookies over the DevTools +protocol; there is no localhost redirect in that path, so over multi-hop SSH +neither the desktop nor a DevTools relay is reachable in practice +[KNOWLEDGE.md, 2026-09-16]. + +ADT for VS Code does it differently. It opens + +``` +https:///sap/bc/adt/core/http/reentranceticket?redirect-url=http://localhost:/adt/redirect +``` + +and reads the ticket back out of the redirect + +``` +http://localhost:/adt/redirect?...&reentrance-ticket= +``` + +(observed 2026-09-16). That half is portable: **your** browser does the SSO, the +tenant redirects to `localhost`, and a forwarded TCP port puts "localhost" back +on this host. This script is that listener. + +--- + +## Run it + +Three steps, three terminals-worth of context. Values below use the defaults +(tenant `my431353`, port `42010`, 300 s timeout). + +### 1. Start the helper on this host + +```bash +node /home/alexz/projects/sap-integrations-mcl/arena-plm/tools/adt-reentrance-login/login.mjs +``` + +It prints (to stderr) the port to forward and the URL to open, then waits up to +300 seconds for exactly one redirect. + +### 2. Forward the port to the machine running your browser + +**VS Code:** open the **Ports** panel (`Ctrl+Shift+P` → *Ports: Focus on Ports +View*) → **Forward a Port** → enter `42010`. VS Code forwards it to your local +`localhost:42010`. + +**Plain SSH**, from your laptop: + +```bash +ssh -L 42010:127.0.0.1:42010 +``` + +The browser must reach **this host's** localhost. Without the forward the tenant +redirects your browser to its own machine and nothing is captured. + +### 3. Open the URL in that browser and complete SSO + +``` +https://my431353.s4hana.cloud.sap/sap/bc/adt/core/http/reentranceticket?redirect-url=http://localhost:42010/adt/redirect +``` + +(Or capture it programmatically: `node login.mjs --print-url-only`.) + +After SSO the tab lands on "Ticket captured", the helper writes +`~/.abap-adt-mcp/ticket.json` (mode 0600) and exits 0. + +### 4. Make the MCP server pick it up + +`~/.abap-adt-mcp/ticket-provider.sh` already `cat`s that file, and +`abap-adt-mcp` caches the login per destination — it only re-runs the provider +on an explicit `login` call or a `reauthenticate` +(`dist/index.js:268-292`, `615-625`, read 2026-09-18). So after capturing a +fresh ticket, call the MCP `login` tool for the destination. + +--- + +## Options + +| Flag | Default | Meaning | +|---|---|---| +| `--tenant ` | `https://my431353.s4hana.cloud.sap` | Tenant base URL. The UI host, not the `-api` host. | +| `--port ` | `42010` | Loopback port. Bound to `127.0.0.1` **only**; there is no flag to change the bind address. | +| `--timeout ` | `300` | Give up and write nothing. | +| `--out ` | `~/.abap-adt-mcp/ticket.json` | Output file, mode 0600 (directory created 0700). | +| `--client ` | *not sent* | Adds `sap-client=`. See "Which client?" below. | +| `--allow-host ` | — | Additionally accept this `Host` header (repeatable), for tunnelled forwards that are not `localhost`. | +| `--probe` | off | After capture, test cookie vs header transport (below). | +| `--print-url-only` | off | Print the logon URL to **stdout** and exit; start no listener. | +| `--help` | — | Usage. | + +Exit codes: `0` captured · `2` usage · `3` timeout · `4` cannot bind · `5` +redirect arrived without a usable ticket. + +### Which client? + +Nothing is hard-coded. The client the browser session uses is unconfirmed; the +published service URL shows `sap-client=080`, but that is the API host's +service URL, not proof about the UI logon. Run without `--client` first; only +add `--client 080` if the tenant needs it. + +--- + +## Probe mode (`--probe`) + +Open question this settles: SAP documents a **logon ticket** in the `MYSAPSSO2` +**cookie** and an **assertion ticket** in a `mysapsso2` **header** +[source: sap-docs sap-help loio 3354605668 and 3361378221, verified +2026-09-18]. Which one a *reentrance* ticket behaves as is **unknown**, and it +decides whether `abap-adt-mcp`'s `sso2` mode can work at all — that mode sends +the value strictly as a **cookie** (`dist/lib/sso2TicketProvider.js`, read +2026-09-18). + +With `--probe`, after capture the helper makes two GETs to +`/sap/bc/adt/core/discovery` with `Accept: application/atomsvc+xml` and +**no other credentials**: + +- **(a)** `Cookie: MYSAPSSO2=` +- **(b)** `MYSAPSSO2: ` + +and reports only the HTTP status of each, whether a `set-cookie` with +`SAP_SESSIONID` came back, and which succeeded. Bodies are discarded unread. A +redirect is reported by **host only** (a SAML `Location` can carry request +material). + +Read the verdict as: + +- **cookie worked** → `abap-adt-mcp`'s `sso2` mode works as-is. +- **only the header worked** → `sso2` mode **cannot** use this ticket; the MCP + server would need a header transport it does not have. +- **neither** → expired ticket, wrong client, or wrong mechanism entirely. + +--- + +## Ticket lifetime + +- **The lifetime of a reentrance ticket is not verified here.** Treat it as + short-lived and re-run the helper whenever logon fails. Do not assume the + 8-hour default of a classic SAP logon ticket applies. +- Whether a reentrance ticket is **single-use** is also unverified. If a + captured ticket authenticates once and then stops, assume single-use and + re-capture. +- Expiry surfaces in the ADT MCP only as **"Your user was logged off."** + `abap_list_destinations` and `get_object_type_details` still answer, so + neither proves a live session; `abap_creation-run_validation` is the cheapest + read-only liveness check [KNOWLEDGE.md, 2026-09-16]. + +--- + +## Security rules + +These are contracts, not preferences. The ticket is a **live logon credential** +for the tenant. + +1. **The ticket is never printed, logged, or put in an error message, argv, or + an environment variable.** The only place it is written is `--out`, mode + 0600. Validation failures report the *reason* and the parameter *names* + present — never values, never offending characters or positions. +2. **stdout stays empty.** Every human-readable message goes to stderr, so + nothing sensitive can be captured by a `$(...)` around the helper. The single + exception is `--print-url-only`, which prints the (secret-free) logon URL. +3. **The listener binds `127.0.0.1` only** and is one-shot. There is no flag to + bind a wider interface. A `Host` header that is not a loopback name is + refused (cheap DNS-rebinding guard); allow a specific tunnel name with + `--allow-host`. +4. **Never paste the redirect URL** into a chat, issue, log, or file — it + contains the ticket. Note that the URL also lands in **your browser's + history**; clear it on a shared machine. +5. The output file is written with permissions set **before** any ticket byte + reaches the disk (`open` + `fchmod` + `write`), so a pre-existing + world-readable file cannot briefly expose it. +6. Ticket values are validated against the RFC 6265 cookie-octet set copied + verbatim from `abap-adt-mcp` — a ticket the MCP server would reject is + rejected here instead of being written to a file that cannot work. + +### Percent-decoding, and why not `URLSearchParams` + +The ticket is URL-decoded **exactly once** with `decodeURIComponent` over the +raw query pair, and never re-encoded. + +`URLSearchParams` is deliberately **not** used: it applies +`application/x-www-form-urlencoded` rules and turns a literal `+` into a space. +Measured on Node v24.15.0, 2026-09-18: + +``` +'?reentrance-ticket=AB%2BCD+EF%3d' + searchParams.get(...) -> 'AB+CD EF=' <-- space, corrupt + decodeURIComponent(raw pair) -> 'AB+CD+EF=' <-- correct +``` + +A space is not a legal cookie-octet, so a base64 ticket carrying `+` would be +silently corrupted and then rejected. + +--- + +## Configuration (handled by the repo owner, documented here for reference) + +This script does **not** touch `~/.abap-adt-mcp/systems.json` or `.mcp.json`. +For the record, `abap-adt-mcp` 2.1.0 requires for `authType: "sso2"` +(`dist/lib/systems.js:73-149`, read 2026-09-18): + +- `sso2.command` must be an **absolute executable path** (`sso2.args` optional, + `sso2.timeoutMs` 1000–300000); +- the system `url` must be **HTTPS** ("so the ticket is never sent in + plaintext"); +- `insecureTls` is refused in combination with `sso2`; +- `authType: "ticket"` is accepted as an alias for `sso2`; +- the server warns at startup that every remote caller of an `sso2` destination + shares the SAP identity of the user running it — one server instance per + person. + +--- + +## Troubleshooting + +| Symptom | Cause | +|---|---| +| Browser cannot reach `localhost:42010` | Port not forwarded, or forwarded to the wrong host. Re-check step 2. | +| `port ... is already in use` (exit 4) | Another listener. Use `--port ` and forward **that** port. | +| `Timed out ... Nothing was written.` (exit 3) | SSO never completed, or the redirect went to the wrong machine. | +| `the redirect carried no reentrance-ticket parameter` (exit 5) | The tenant redirected without a ticket — usually a failed or cancelled SSO. The parameter names it *did* send are listed. | +| `ticket contains characters outside the RFC 6265 cookie-octet set` (exit 5) | The value is not usable as a `MYSAPSSO2` cookie; `abap-adt-mcp` would reject it too. | +| `Refused a redirect with Host: ...` | Forward is not a plain localhost forward. Re-run with `--allow-host `. | diff --git a/tools/adt-reentrance-login/login.mjs b/tools/adt-reentrance-login/login.mjs new file mode 100755 index 0000000..d377ed4 --- /dev/null +++ b/tools/adt-reentrance-login/login.mjs @@ -0,0 +1,441 @@ +#!/usr/bin/env node +/** + * adt-reentrance-login — capture an SAP ADT reentrance ticket over a localhost + * redirect, so `abap-adt-mcp` can log on to an S/4HANA Cloud tenant from a + * headless / SSH-only host. + * + * ADT for VS Code logs on by opening + * https:///sap/bc/adt/core/http/reentranceticket?redirect-url=http://localhost:/adt/redirect + * and reading the ticket out of the redirect + * http://localhost:/adt/redirect?...&reentrance-ticket= + * (observed 2026-09-16). This script plays the "localhost" half of that dance: + * it listens on 127.0.0.1 only, captures exactly one ticket, and writes it as + * {"ticket":"..."} to a 0600 file for ~/.abap-adt-mcp/ticket-provider.sh. + * + * SECURITY CONTRACT (do not relax): + * - The ticket is a live logon credential. It is NEVER printed, logged, put + * in an error message, passed in argv, or exported to the environment. + * The only place it is written is the --out file, mode 0600. + * - The listener binds 127.0.0.1 exclusively. There is no flag to change it. + * - All human-facing output goes to stderr. stdout stays empty, except in + * --print-url-only mode, which prints the (secret-free) logon URL so it can + * be captured with $(...). + * + * Node 24, standard library only, no dependencies. + */ + +import { createServer } from 'node:http'; +import { request as httpsRequest } from 'node:https'; +import { parseArgs } from 'node:util'; +import { chmodSync, closeSync, fchmodSync, mkdirSync, openSync, writeSync } from 'node:fs'; +import { dirname, join } from 'node:path'; +import { homedir } from 'node:os'; + +const DEFAULT_TENANT = 'https://my431353.s4hana.cloud.sap'; +const DEFAULT_PORT = 42010; +const DEFAULT_TIMEOUT_SECONDS = 300; +const DEFAULT_OUT = join(homedir(), '.abap-adt-mcp', 'ticket.json'); + +const BIND_ADDRESS = '127.0.0.1'; // loopback only, by design +const REDIRECT_PATH = '/adt/redirect'; +const TICKET_PARAM = 'reentrance-ticket'; +const TICKET_ENDPOINT = '/sap/bc/adt/core/http/reentranceticket'; +const DISCOVERY_PATH = '/sap/bc/adt/core/discovery'; + +/** + * RFC 6265 cookie-octet. Copied verbatim from abap-adt-mcp 2.1.0 + * dist/lib/sso2TicketProvider.js (read 2026-09-18): a ticket that fails this + * test is rejected by the MCP server itself, so fail here instead of writing a + * file that cannot work. Note \x3d (=) IS allowed, so base64 padding is fine; + * space, comma, semicolon, double quote and backslash are not. + */ +const COOKIE_OCTET = /^[\x21\x23-\x2b\x2d-\x3a\x3c-\x5b\x5d-\x7e]+$/; +const MAX_TICKET_CHARS = 64 * 1024; // same ceiling as the MCP server + +/** Hosts a browser may legitimately use to reach this listener. */ +const LOOPBACK_HOSTS = new Set(['localhost', '127.0.0.1', '::1', '[::1]']); + +const EXIT_OK = 0; +const EXIT_USAGE = 2; +const EXIT_TIMEOUT = 3; +const EXIT_BIND = 4; +const EXIT_BAD_TICKET = 5; +const EXIT_INTERRUPTED = 130; + +/** Every human-readable message goes to stderr. Never pass ticket material. */ +const note = (msg = '') => process.stderr.write(`${msg}\n`); + +function usage() { + note(`adt-reentrance-login — capture an ADT reentrance ticket via a localhost redirect + + node login.mjs [options] + + --tenant S/4HANA tenant base URL (default ${DEFAULT_TENANT}) + --port loopback port to listen on (default ${DEFAULT_PORT}, bound to ${BIND_ADDRESS} only) + --timeout give up after this long (default ${DEFAULT_TIMEOUT_SECONDS}) + --out where to write {"ticket":"..."} (default ${DEFAULT_OUT}, mode 0600) + --client add sap-client= to the logon and probe URLs (default: not sent) + --allow-host additionally accept this Host header (for tunnelled forwards) + --probe after capture, test the ticket as a cookie and as a header + --print-url-only print the logon URL to stdout and exit; start no listener + --help this text + + Exit codes: 0 captured, 2 usage, 3 timeout, 4 cannot bind, 5 bad/absent ticket.`); +} + +function fail(code, msg) { + note(`ERROR: ${msg}`); + process.exit(code); +} + +// --------------------------------------------------------------------------- +// Arguments +// --------------------------------------------------------------------------- + +let values; +try { + ({ values } = parseArgs({ + options: { + tenant: { type: 'string', default: DEFAULT_TENANT }, + port: { type: 'string', default: String(DEFAULT_PORT) }, + timeout: { type: 'string', default: String(DEFAULT_TIMEOUT_SECONDS) }, + out: { type: 'string', default: DEFAULT_OUT }, + client: { type: 'string' }, + 'allow-host': { type: 'string', multiple: true, default: [] }, + probe: { type: 'boolean', default: false }, + 'print-url-only': { type: 'boolean', default: false }, + help: { type: 'boolean', default: false }, + }, + strict: true, + allowPositionals: false, + })); +} catch (err) { + usage(); + fail(EXIT_USAGE, err.message); +} + +if (values.help) { + usage(); + process.exit(EXIT_OK); +} + +let tenant; +try { + const parsed = new URL(values.tenant); + if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') { + throw new Error('tenant must be an http(s) URL'); + } + tenant = `${parsed.protocol}//${parsed.host}`; +} catch (err) { + fail(EXIT_USAGE, `--tenant is not a usable URL: ${err.message}`); +} + +const port = Number(values.port); +if (!Number.isInteger(port) || port < 1 || port > 65535) { + fail(EXIT_USAGE, `--port must be an integer 1..65535 (got ${JSON.stringify(values.port)})`); +} + +const timeoutSeconds = Number(values.timeout); +if (!Number.isFinite(timeoutSeconds) || timeoutSeconds <= 0) { + fail(EXIT_USAGE, `--timeout must be a positive number of seconds (got ${JSON.stringify(values.timeout)})`); +} + +const client = values.client; +if (client !== undefined && !/^[0-9]{3}$/.test(client)) { + fail(EXIT_USAGE, '--client must be a three-digit SAP client number'); +} + +const outPath = values.out; +const allowedHosts = new Set([...LOOPBACK_HOSTS, ...values['allow-host']]); + +// --------------------------------------------------------------------------- +// Logon URL +// --------------------------------------------------------------------------- + +/** + * Built by string concatenation on purpose: ADT for VS Code sends the + * redirect-url value literally (observed 2026-09-16), and URLSearchParams would + * percent-encode the ':' and '/'. Colons and slashes are legal in a query + * string (RFC 3986), so the literal form is both valid and wire-identical to + * what the tenant already accepts from ADT. + */ +function buildLogonUrl() { + const redirect = `http://localhost:${port}${REDIRECT_PATH}`; + const clientParam = client ? `&sap-client=${encodeURIComponent(client)}` : ''; + return `${tenant}${TICKET_ENDPOINT}?redirect-url=${redirect}${clientParam}`; +} + +const logonUrl = buildLogonUrl(); + +if (values['print-url-only']) { + // The only thing this script ever writes to stdout. Contains no secret. + process.stdout.write(`${logonUrl}\n`); + process.exit(EXIT_OK); +} + +// --------------------------------------------------------------------------- +// Ticket extraction +// --------------------------------------------------------------------------- + +/** + * Pull the ticket out of the raw request target. + * + * Deliberately NOT URLSearchParams: it applies + * application/x-www-form-urlencoded rules and turns a literal '+' into a space + * (measured on Node v24.15.0, 2026-09-18: '?t=AB%2BCD+EF%3d' -> 'AB+CD EF='). + * A space is not a legal cookie-octet, so a base64 ticket carrying '+' would be + * silently corrupted and then rejected. Splitting the raw query and decoding + * once with decodeURIComponent preserves '+' ('AB+CD+EF='). + * + * Returns { ok: true, ticket } or { ok: false, reason, names }. + * `reason` NEVER contains ticket material. + */ +function extractTicket(requestTarget) { + const q = requestTarget.indexOf('?'); + if (q < 0) return { ok: false, reason: 'the redirect carried no query string', names: [] }; + + const names = []; + let raw = null; + for (const pair of requestTarget.slice(q + 1).split('&')) { + if (!pair) continue; + const eq = pair.indexOf('='); + const rawName = eq < 0 ? pair : pair.slice(0, eq); + let name = rawName; + try { name = decodeURIComponent(rawName); } catch { /* keep the raw name */ } + names.push(name); + if (name === TICKET_PARAM && raw === null) raw = eq < 0 ? '' : pair.slice(eq + 1); + } + + if (raw === null) return { ok: false, reason: `the redirect carried no ${TICKET_PARAM} parameter`, names }; + if (raw === '') return { ok: false, reason: `${TICKET_PARAM} was empty`, names }; + + let ticket; + try { + ticket = decodeURIComponent(raw); // exactly once; do not re-encode + } catch { + return { ok: false, reason: `${TICKET_PARAM} is not valid percent-encoding`, names }; + } + + if (ticket.length === 0) return { ok: false, reason: `${TICKET_PARAM} decoded to an empty string`, names }; + if (ticket.length > MAX_TICKET_CHARS) { + return { ok: false, reason: `ticket is longer than ${MAX_TICKET_CHARS} characters; abap-adt-mcp would reject it`, names }; + } + if (!COOKIE_OCTET.test(ticket)) { + // No positions, no sample characters — that would leak ticket material. + return { ok: false, reason: 'ticket contains characters outside the RFC 6265 cookie-octet set', names }; + } + return { ok: true, ticket }; +} + +/** Write {"ticket":"..."} with 0600 before any ticket byte reaches the disk. */ +function writeTicketFile(path, ticket) { + const dir = dirname(path); + const created = mkdirSync(dir, { recursive: true, mode: 0o700 }); + if (created) chmodSync(created, 0o700); // only tighten what we created + + // 'w' truncates on open; fchmod before write closes the window in which a + // pre-existing world-readable file would briefly hold the ticket. + const fd = openSync(path, 'w', 0o600); + try { + fchmodSync(fd, 0o600); + writeSync(fd, `${JSON.stringify({ ticket })}\n`); + } finally { + closeSync(fd); + } +} + +// --------------------------------------------------------------------------- +// Probe (--probe) +// --------------------------------------------------------------------------- + +/** + * One GET to the ADT discovery endpoint carrying the ticket either as a + * MYSAPSSO2 cookie or as a MYSAPSSO2 header, and nothing else. Resolves to + * status/redirect-host/session-cookie facts only — never headers, never a body. + */ +function probeOnce(mode, ticket) { + return new Promise((resolve) => { + const target = new URL(DISCOVERY_PATH, tenant); + if (client) target.searchParams.set('sap-client', client); + + const headers = { Accept: 'application/atomsvc+xml' }; + if (mode === 'cookie') headers.Cookie = `MYSAPSSO2=${ticket}`; + else headers.MYSAPSSO2 = ticket; + + const req = httpsRequest( + { + hostname: target.hostname, + port: target.port || 443, + path: `${target.pathname}${target.search}`, + method: 'GET', + headers, + timeout: 30_000, + }, + (res) => { + const setCookie = res.headers['set-cookie'] ?? []; + const sessionCookie = setCookie.some((c) => /^\s*SAP_SESSIONID/i.test(c)); + let redirectHost = null; + if (res.headers.location) { + // Host only: a SAML redirect URL can carry request material. + try { redirectHost = new URL(res.headers.location, target).host; } + catch { redirectHost = '(unparseable)'; } + } + res.resume(); // discard the body; it is never read or logged + res.on('end', () => resolve({ status: res.statusCode, sessionCookie, redirectHost })); + }, + ); + req.on('timeout', () => req.destroy(new Error('request timed out'))); + req.on('error', (err) => resolve({ error: err.code ?? err.message })); + req.end(); + }); +} + +function describeProbe(label, result) { + if (result.error) return ` ${label}: transport error (${result.error})`; + const bits = [`HTTP ${result.status}`]; + bits.push(result.sessionCookie ? 'set-cookie SAP_SESSIONID: yes' : 'set-cookie SAP_SESSIONID: no'); + if (result.redirectHost) bits.push(`redirect host: ${result.redirectHost}`); + return ` ${label}: ${bits.join(' ')}`; +} + +async function runProbe(ticket) { + note(''); + note(`Probing ${tenant}${DISCOVERY_PATH} (Accept: application/atomsvc+xml, no other credentials)...`); + const asCookie = await probeOnce('cookie', ticket); + const asHeader = await probeOnce('header', ticket); + + note(describeProbe('(a) MYSAPSSO2 cookie', asCookie)); + note(describeProbe('(b) MYSAPSSO2 header', asHeader)); + + const cookieOk = asCookie.status === 200; + const headerOk = asHeader.status === 200; + if (cookieOk && headerOk) note("verdict: both worked — abap-adt-mcp's cookie-based sso2 mode can work."); + else if (cookieOk) note("verdict: the COOKIE worked — abap-adt-mcp's sso2 mode (MYSAPSSO2 cookie) can work."); + else if (headerOk) note('verdict: only the HEADER worked — abap-adt-mcp 2.1.0 sends a cookie, so its sso2 mode cannot use this ticket as-is.'); + else note('verdict: neither returned HTTP 200 — this ticket is not accepted by either transport (expired, wrong client, or wrong mechanism).'); +} + +// --------------------------------------------------------------------------- +// One-shot loopback listener +// --------------------------------------------------------------------------- + +const PAGE_OK = ` + + +ADT logon captured + +

Ticket captured

+

The reentrance ticket was written to the login helper's output file. You can close this tab.

+

This URL contains a live credential and stays in your browser history. Clear it if the machine is shared.

+ +`; + +const PAGE_FAIL = ` + + +ADT logon failed + +

No usable ticket

+

The redirect did not carry a usable reentrance ticket. See the helper's terminal output.

+ +`; + +let finished = false; + +const server = createServer((req, res) => { + // Keep the shared secret-free headers on every answer. + const send = (status, body, cb) => { + res.writeHead(status, { + 'Content-Type': 'text/html; charset=utf-8', + 'Cache-Control': 'no-store', + 'Referrer-Policy': 'no-referrer', + Connection: 'close', + }); + if (req.method === 'HEAD') res.end(cb); + else res.end(body, cb); + }; + + if (finished) { send(503, PAGE_FAIL); return; } + + const target = req.url ?? '/'; + const pathOnly = target.split('?')[0]; + + // Dropped rather than fatal: browsers ask for /favicon.ico unprompted. + if (pathOnly !== REDIRECT_PATH) { send(404, PAGE_FAIL); return; } + + // Cheap DNS-rebinding guard. The socket is loopback-only anyway. + const hostHeader = (req.headers.host ?? '').replace(/:\d+$/, ''); + if (hostHeader && !allowedHosts.has(hostHeader)) { + note(`Refused a redirect with Host: ${hostHeader} (expected localhost). Re-run with --allow-host ${hostHeader} if that forward is yours.`); + send(403, PAGE_FAIL); + return; + } + + const result = extractTicket(target); + if (!result.ok) { + finished = true; + send(400, PAGE_FAIL, () => { + note(`Redirect received on ${REDIRECT_PATH}, but ${result.reason}.`); + if (result.names?.length) note(`Parameter names present (values withheld): ${result.names.join(', ')}`); + server.close(); + process.exit(EXIT_BAD_TICKET); + }); + return; + } + + finished = true; + try { + writeTicketFile(outPath, result.ticket); + } catch (err) { + send(500, PAGE_FAIL, () => { + note(`ERROR: could not write ${outPath}: ${err.message}`); + server.close(); + process.exit(EXIT_BAD_TICKET); + }); + return; + } + + send(200, PAGE_OK, async () => { + note(`Ticket captured and written to ${outPath} (mode 0600). Its value is not shown anywhere.`); + if (values.probe) { + try { await runProbe(result.ticket); } + catch (err) { note(`Probe failed to run: ${err.message}`); } + } + server.close(); + process.exit(EXIT_OK); + }); +}); + +server.on('error', (err) => { + if (err.code === 'EADDRINUSE') { + fail(EXIT_BIND, `port ${port} is already in use on ${BIND_ADDRESS}. Pick another with --port (and forward that one).`); + } + fail(EXIT_BIND, `cannot listen on ${BIND_ADDRESS}:${port}: ${err.message}`); +}); + +const timer = setTimeout(() => { + note(`Timed out after ${timeoutSeconds}s without a redirect. Nothing was written.`); + server.close(); + process.exit(EXIT_TIMEOUT); +}, timeoutSeconds * 1000); + +process.on('SIGINT', () => { + clearTimeout(timer); + note('Interrupted. Nothing was written.'); + server.close(); + process.exit(EXIT_INTERRUPTED); +}); + +server.listen({ host: BIND_ADDRESS, port, exclusive: true }, () => { + note(`Listening on http://${BIND_ADDRESS}:${port}${REDIRECT_PATH} (loopback only) for ${timeoutSeconds}s.`); + note(''); + note(`1. Forward TCP port ${port} from this host to your machine.`); + note(` VS Code: Ports panel -> Forward a Port -> ${port} (or: ssh -L ${port}:127.0.0.1:${port} )`); + note(' Your browser must reach THIS host\'s localhost, or the redirect lands nowhere.'); + note('2. Open this URL in that browser and complete SSO:'); + note(''); + note(` ${logonUrl}`); + note(''); + note(`3. The tenant redirects to http://localhost:${port}${REDIRECT_PATH}?... and the ticket is captured here.`); + note(` It is written to ${outPath} only. Never paste the redirect URL into a chat, log or file.`); +});