upload current sources
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
# ADR-001 — CAP-first rebuild; Express predecessor demoted to reference
|
||||
|
||||
Date: 2026-09-04 · Status: Accepted — **amended by ADR-002 (2026-09-04)**: the
|
||||
rebuild target is now on-stack RAP + Fiori Elements in the tenant, with a
|
||||
side-by-side connectivity worker; CAP exits the architecture. This ADR's core
|
||||
rationale — the SAP toolchain must engage, and the Express shape is not the
|
||||
pattern — stands unchanged.
|
||||
|
||||
## Context
|
||||
|
||||
The working predecessor (`~/projects/sap-apps/arena-plm`) is Express.js + axios
|
||||
with hand-rolled SAP patterns. It functions, but it sits outside the SAP-native
|
||||
toolchain: the CAP, Fiori, and UI5 MCP servers and skills had nothing to attach
|
||||
to, so development guidance never engaged, and the implementation drifted from
|
||||
SAP best practice — hand-managed CSRF tokens, an approval store in droplet-local
|
||||
SQLite that vanishes on restart, JWT validation with no scope checks. The estate
|
||||
is explore-phase; there is no production cutover to protect, so preserving the
|
||||
legacy shape has no value beyond reference.
|
||||
|
||||
## Decision
|
||||
|
||||
I rebuild arena-plm as a CAP (Node.js) application: the approval domain modeled
|
||||
in CDS, all S/4HANA calls through CAP remote services / SAP Cloud SDK via the
|
||||
`mc-s4hana` destination, and Fiori Elements evaluated before any custom UI5.
|
||||
The Express app is reference-only — consulted for behavior and tombstones,
|
||||
never extended.
|
||||
|
||||
## Consequences
|
||||
|
||||
- The SAP toolchain becomes load-bearing: `cap-mcp-server` is consulted on
|
||||
every model and service change, which is the corrective for the
|
||||
tooling-never-engaged failure mode.
|
||||
- Auth, CSRF, and destination handling move onto framework-managed code paths,
|
||||
eliminating the predecessor's hand-rolled plumbing.
|
||||
- The approval workflow domain must be re-modeled in CDS; behavior parity is
|
||||
checked against the reference app, never assumed from it.
|
||||
- Whether the sync-pipeline half stays inside CAP or becomes a separate
|
||||
hardened worker is deliberately deferred (PRD OQ-4).
|
||||
@@ -0,0 +1,85 @@
|
||||
# ADR-002 — On-stack first: RAP approval domain + Fiori app in the tenant; side-by-side reduced to a connectivity worker
|
||||
|
||||
Date: 2026-09-04 · Status: Accepted (amends ADR-001)
|
||||
|
||||
## Context
|
||||
|
||||
The decisive test came from outside: the showcased UI apps present the data
|
||||
well, but they are invisible from inside S/4HANA — not in the Fiori launchpad,
|
||||
not discoverable in the Custom CDS Views app, reachable only via standalone
|
||||
Cloud Foundry URLs behind an ad-hoc route structure with weak access control.
|
||||
For an S/4HANA user, an application that cannot be found in the tenant does not
|
||||
exist.
|
||||
|
||||
ADR-001 correctly rejected the Express shape but still assumed a side-by-side
|
||||
BTP application (CAP) as the rebuild target. That assumption fails the same
|
||||
test: a CAP app on BTP is still a standalone URL outside the tenant.
|
||||
|
||||
S/4HANA Cloud Public Edition offers two in-tenant extensibility tiers, both
|
||||
verified against SAP documentation (2026-09-04, via sap-docs MCP):
|
||||
|
||||
- **Developer extensibility**: upgrade-stable ABAP Cloud development directly
|
||||
on the stack (ADT against the development tenant) — CDS entities, RAP
|
||||
business objects with behavior, custom Fiori apps deployed into the tenant,
|
||||
authorization via IAM app → business catalog → business role.
|
||||
- **Key-user extensibility**: Custom Business Objects, Custom CDS Views (on
|
||||
released C1 view entities), Custom Analytical Queries, and Business
|
||||
Configuration maintenance via the Custom Business Configurations app.
|
||||
|
||||
The estate's standing rule already says extensibility-first: check what the
|
||||
tenant can host before building side-by-side.
|
||||
|
||||
## Decision
|
||||
|
||||
I move the application into the tenant and shrink the side-by-side footprint to
|
||||
what genuinely cannot run there:
|
||||
|
||||
**On-stack (developer extensibility, ABAP Cloud / RAP):**
|
||||
- A custom RAP business object for the Arena change approval domain — staging
|
||||
data, approval state machine (pending → approved/rejected), audit fields —
|
||||
persisted in the tenant.
|
||||
- A Fiori Elements List Report / Object Page on that BO, deployed to the
|
||||
tenant's Fiori launchpad; approve/reject as RAP actions.
|
||||
- IAM app, business catalog, and business roles as the only access control —
|
||||
no user-facing custom URLs anywhere.
|
||||
- Custom CDS views over the approval BO (joinable with released views such as
|
||||
I_Product) so the data is discoverable in the Custom CDS Views app and usable
|
||||
in analytical queries — the discoverability test that triggered this ADR.
|
||||
- Business mappings evaluated as Business Configuration maintenance objects
|
||||
(Custom Business Configurations app) instead of JSON files in git.
|
||||
|
||||
**Side-by-side (one hardened TypeScript worker, no UI, no user URLs):**
|
||||
- Arena connectivity only: webhook receipt, delta polling, session management
|
||||
(25-min TTL, login race), file streaming.
|
||||
- Initially also the sync executor: on approval, it calls the released OData
|
||||
APIs (Product/BOM/ChangeMaster/Attachment) exactly as the predecessor did —
|
||||
every KNOWLEDGE.md tombstone stays valid unchanged.
|
||||
- It exchanges data with the tenant through the approval BO's service binding
|
||||
exposed via a custom communication scenario/arrangement with a technical
|
||||
user.
|
||||
|
||||
CAP exits the architecture: with the domain, UI, and persistence on-stack, the
|
||||
remaining side-by-side piece is a pipeline, and per the two-archetype standard
|
||||
a pipeline is a worker, not a CAP service. This resolves ADR-001's deferred
|
||||
question and PRD OQ-1 through OQ-4 in one move.
|
||||
|
||||
## Consequences
|
||||
|
||||
- The Fiori launchpad becomes the application's only front door; the shared
|
||||
BTP gateway, XSUAA role juggling, and standalone routes disappear from this
|
||||
package entirely.
|
||||
- Persistence and audit live in the tenant — the predecessor's
|
||||
lost-on-restart approval queue defect is solved structurally, not patched.
|
||||
- The toolchain shifts: ABAP Cloud + RAP + Fiori Elements, so the MCP loadout
|
||||
moves to sap-docs / abap-docs / sap-released-objects / fiori / ui5, and
|
||||
cap-mcp-server leaves the loadout.
|
||||
- Verification items before build (POC gates):
|
||||
- V-1: confirm developer-extensibility access on my429480 (development
|
||||
tenant reachable via ADT, developer business role assigned).
|
||||
- V-2: confirm a custom communication scenario can expose the approval BO's
|
||||
OData service to the worker's technical user.
|
||||
- V-3: evaluate on-stack Application Jobs vs the worker for Arena delta
|
||||
polling; and Business Configuration objects vs config files (ARN-004).
|
||||
- Fallback recorded: if developer extensibility proves unavailable on this
|
||||
tenant, the key-user tier (Custom Business Object with generated UI) is the
|
||||
degraded-but-still-in-tenant variant; side-by-side is no longer the fallback.
|
||||
@@ -0,0 +1,94 @@
|
||||
# ADR-003 — Ground-truth enforcement: hooks, not advice
|
||||
|
||||
Date: 2026-09-04 · Status: Accepted
|
||||
|
||||
## Context
|
||||
|
||||
The old estate proved that advisory guidance fails: an MCP routing table, skills,
|
||||
and operating rules existed for months, yet the servers and skills were never
|
||||
consulted during development — even on the CAP packages where they obviously
|
||||
applied. Causes: tool-selection dilution under a saturated loadout, zero
|
||||
enforcement (prose loses to momentum), friction (cold starts, deferred tool
|
||||
loading), and model overconfidence in trained SAP knowledge. The requirement for
|
||||
this repo: consultation must be structural — the agent must forget what it
|
||||
thinks it knows, consult the MCP servers and context7 before work starts, and be
|
||||
able to cite sources on demand.
|
||||
|
||||
## Decision
|
||||
|
||||
Four mechanisms in `.claude/settings.json` + `.claude/hooks/` (project scope,
|
||||
part of the founding kit for every future package):
|
||||
|
||||
1. **SessionStart injection** — every session opens with the grounding charter:
|
||||
training knowledge is not a citable source; the five MCP servers + context7
|
||||
are; if tools are deferred, loading them is the first action.
|
||||
2. **Per-prompt reflex (UserPromptSubmit)** — a SAP-scoped analogue of the
|
||||
machine-global TOOL-REFLEX hook: never classify whether a prompt "needs" a
|
||||
consult; the classification step is the known failure.
|
||||
3. **Consultation gate (PreToolUse deny)** — every successful `mcp__*`,
|
||||
WebSearch, or WebFetch call stamps `.claude/.last-mcp-consult`; a Write/Edit
|
||||
on a SAP artifact (`.cds`, `.abap`, `.bdef`, annotations, views,
|
||||
`manifest.json`, `srv/`, `app/`, `worker/`) is **denied** unless a
|
||||
consultation happened within 30 minutes, with the remedy stated in the denial
|
||||
reason. Governance `.md`, `docs/`, and `.claude/` paths are exempt so
|
||||
documentation never jams.
|
||||
4. **Citation duty** — every SAP behavioral claim recorded in PRD/ADR/KNOWLEDGE
|
||||
carries `[source: <server> <doc-id/URL>, verified YYYY-MM-DD]` (CLAUDE.md
|
||||
rule 6); a PostToolUse reminder fires on governance-doc edits. Uncited =
|
||||
hypothesis.
|
||||
|
||||
## On "load tools automatically at session start"
|
||||
|
||||
**Corrected 2026-09-04** after reading the 2026-09-03 "Investigate performance
|
||||
lag issue" session transcript: my original claim ("no setting exists to preload
|
||||
deferred tools") was true of settings *keys* but wrong in practice. The
|
||||
`ENABLE_TOOL_SEARCH=false` environment variable disables deferral entirely
|
||||
(verified against the binary in that session). It must be set before process
|
||||
bootstrap — the settings.json `env` block loads too late, per that session's
|
||||
finding — so it lives in the `claude()` wrapper in `~/.bashrc`, where it is
|
||||
**already active machine-wide**, including for sessions started in this repo.
|
||||
History: preloading was first measured at ~98k tokens/turn at the old ~950-tool
|
||||
surface and rejected in favor of the TOOL-REFLEX hook; once plugin localization
|
||||
cut the surface to ~40 tools, the same session adopted preloading via the
|
||||
wrapper. This repo's slim 5-server loadout keeps the preload cost small.
|
||||
Mechanism 1 (SessionStart load-first order) remains as the fallback for launch
|
||||
paths that bypass the wrapper (e.g. IDE-spawned sessions).
|
||||
|
||||
## Consequences
|
||||
|
||||
- Consultation stops being a virtue and becomes a precondition; skipping it now
|
||||
produces a denied edit instead of silent drift.
|
||||
- The 30-minute freshness window is a tunable; if it proves too tight or too
|
||||
loose in practice, adjust it in `sap-consult-gate.sh` and note it here.
|
||||
- The gate measures that *a* consultation happened, not that the *right* one
|
||||
did — citation duty and review cover relevance.
|
||||
- Hooks activate on the first session started inside this repo (project hooks
|
||||
are read at session start).
|
||||
|
||||
## Amendment 2026-09-14 — gate hardened after code review
|
||||
|
||||
The 2026-09-04 gate was advisory in practice: the `Write|Edit` matcher missed
|
||||
Bash writes (sed -i, heredocs, tee, cp/mv, interpreter one-liners), MultiEdit,
|
||||
NotebookEdit and filesystem-MCP edits; the `mcp__.*` PostToolUse matcher let
|
||||
any MCP call (memory, chrome, mcp-manage) refresh the 30-minute marker; hook
|
||||
commands hard-coded this checkout's absolute path; and a missing `jq` produced
|
||||
a silent allow. I changed the mechanism, not the prose:
|
||||
|
||||
- PreToolUse gate matches `Write|Edit|MultiEdit|NotebookEdit|Bash|
|
||||
mcp__filesystem__write_file|mcp__filesystem__edit_file|mcp__filesystem__move_file`;
|
||||
for Bash the script gates only commands that combine a write indicator with
|
||||
a gated path (documented heuristic, known blind spots: variable-built paths,
|
||||
`cd`-relative paths, writes by invoked scripts).
|
||||
- The consult marker is refreshed only by the grounding servers (sap-docs,
|
||||
abap-docs, sap-released-objects, the Fiori/UI5 servers, context7, CAP) and
|
||||
WebSearch/WebFetch — anchored regex in `settings.json`.
|
||||
- Hook commands resolve via `"${CLAUDE_PROJECT_DIR}"`; scripts prefer that
|
||||
variable over `$0` [source: https://code.claude.com/docs/en/hooks —
|
||||
"`${CLAUDE_PROJECT_DIR}`: the project root where the session started",
|
||||
verified 2026-09-14].
|
||||
- Missing `jq` or an unresolvable marker now denies (fail closed); `jq` is a
|
||||
hard dependency of any session in this repo.
|
||||
- `*/worker/*` left the gated set: the TypeScript worker is governed by
|
||||
context7 and the OpenAPI contract, not the SAP gate.
|
||||
- `.claude/hooks/tests/run.sh` pipe-tests both scripts (37 cases) against an
|
||||
override marker so the real marker is never touched.
|
||||
@@ -0,0 +1,64 @@
|
||||
# Specialist Agent Roster — founding-kit design
|
||||
|
||||
Compiled 2026-09-04 from a 4-agent verified research sweep (3 researchers +
|
||||
1 adversarial verifier; 37 findings, 22 confirmed, 14 downgraded/refuted and
|
||||
discarded). Every design choice below cites a CONFIRMED source. This is the
|
||||
"dedicated agents" layer of the founding kit; the files live in
|
||||
`.claude/agents/`.
|
||||
|
||||
## Design principles (adopted from confirmed sources)
|
||||
|
||||
1. **Tool restrictions enforce role — prompts merely request it.** A reviewer
|
||||
without Write cannot introduce bugs; a test agent without deploy tools
|
||||
cannot ship. [source: https://www.pubnub.com/blog/best-practices-for-claude-code-sub-agents/ + https://github.com/VoltAgent/awesome-claude-code-subagents, verified 2026-09-04]
|
||||
2. **Every agent carries the MCP routing table for its artifacts** — the
|
||||
pattern SAP itself demonstrates in its official agentic CAP reference app
|
||||
(AGENTS.md maps each artifact type to its grounding server). [source: https://github.com/SAP-samples/cap-agentic-engineered, verified 2026-09-04]
|
||||
3. **Model per role:** implementation agents inherit the session model;
|
||||
the test agent runs on Haiku (cheap, mechanical work). [source: https://github.com/lst97/claude-code-sub-agents test-automator pattern — repo verified WEAK on scale claims but the model-tiering pattern confirmed on page, 2026-09-04]
|
||||
4. **Reviewer filters false positives explicitly** (excluded classes declared
|
||||
up front), per Anthropic's own security-review agent. [source: https://github.com/anthropics/claude-code-security-review, verified 2026-09-04]
|
||||
5. All agents inherit this repo's enforcement layer (ADR-003 gate + citation
|
||||
duty) automatically — hooks are project-scoped, so no per-agent wiring.
|
||||
|
||||
## The roster (5 — the cap)
|
||||
|
||||
| Agent | Scope | Tools (restriction is the contract) | Model |
|
||||
|---|---|---|---|
|
||||
| `rap-developer` | On-stack ABAP Cloud: RAP BO, CDS, behavior, BDEF, IAM artifacts | Edit/Write/Read/Grep/Glob/Bash + abap-docs, sap-docs, sap-released-objects | inherit |
|
||||
| `fiori-developer` | Fiori Elements app, annotations, manifest, launchpad content | Edit/Write/Read/Grep/Glob/Bash + fiori-mcp-server (3-step workflow), ui5-mcp-server | inherit |
|
||||
| `worker-developer` | TS connectivity worker: Arena API, Cloud SDK OData writes | Edit/Write/Read/Grep/Glob/Bash + context7 | inherit |
|
||||
| `sap-code-reviewer` | Clean-core, security, contract conformance review | **Read/Grep/Glob + doc MCPs only — no Write, no Bash** | inherit |
|
||||
| `test-engineer` | Unit/integration tests for worker + service layers | Edit/Write/Read/Grep/Glob/Bash (test commands only by instruction) | haiku |
|
||||
|
||||
## MCP additions (recommended, gated — not installed)
|
||||
|
||||
The on-stack RAP work needs an **ADT MCP server** (live syntax check, ATC,
|
||||
activation, repository objects). Verified options, in preference order per the
|
||||
prefer-standard rule:
|
||||
|
||||
1. **SAP's official ABAP MCP path** — SAP's agentic-ABAP direction (VS Code
|
||||
ADT + zero-config MCP) is announced and rolling out; `sapse.adt-vscode` is
|
||||
already installed on this machine. Evaluate first at V-1.
|
||||
[source: https://community.sap.com/t5/technology-blog-posts-by-sap/entering-the-new-era-of-agentic-ai-for-abap-development/ba-p/14394643 + community post ba-p/14408186, verified 2026-09-04]
|
||||
2. **ARC-1** (`arc-mcp/arc-1`) — enterprise ABAP/ADT MCP, maintained.
|
||||
[source: https://github.com/arc-mcp/arc-1, verified 2026-09-04]
|
||||
3. **`yzonur/sap-adt-mcp`** — 27 ADT tools incl. quality gates, read-only
|
||||
flag, audit logging. [source: https://github.com/yzonur/sap-adt-mcp, verified 2026-09-04]
|
||||
|
||||
Optional later: SAP's official **BTP Administration MCP** for the devops side.
|
||||
[source: https://community.sap.com/t5/technology-blog-posts-by-sap/public-release-of-the-mcp-server-for-sap-btp-administration/ba-p/14471692, verified 2026-09-04]
|
||||
|
||||
Registry for future discovery: https://github.com/marianfoo/sap-ai-mcp-servers
|
||||
(verified 2026-09-04). Note: `marianfoo/mcp-sap-notes` is archived (June 2026),
|
||||
superseded by `marianfoo/sap-mcp-servers` — do not adopt the archived one.
|
||||
|
||||
## Discarded by verification (recorded so they are not re-researched)
|
||||
|
||||
- `MariusAlbrecht/mcp-adt` — does not exist (404; the real ADT bridge is the
|
||||
separate "vs-punk" project).
|
||||
- SAP GUI automation MCP — cited listing 404s; GUI automation irrelevant to
|
||||
Public Cloud anyway.
|
||||
- `rap-skills`, `claude-abap-skills` — real but 1–14 commits; mine their
|
||||
*scenario decomposition ideas* (generator/CDS/behavior/testing/troubleshoot),
|
||||
do not depend on the repos.
|
||||
@@ -0,0 +1,731 @@
|
||||
{
|
||||
"openapi": "3.1.0",
|
||||
"info": {
|
||||
"title": "arena-plm connectivity worker API",
|
||||
"version": "0.2.1",
|
||||
"x-sap-shortText": "Inbound machine-to-machine surface of the arena-plm connectivity worker: Arena PLM webhook intake, health probe, and sync-run control.",
|
||||
"x-sap-stateInfo": {
|
||||
"state": "beta"
|
||||
},
|
||||
"description": "POC-stage contract for the inbound HTTP surface of the arena-plm connectivity worker (a machine-to-machine TypeScript worker on BTP Cloud Foundry; no UI, no user-facing URLs). This document governs the implementation (API-first).\n\nScope: inbound endpoints only. The worker's outbound legs — the Arena PLM REST API (`api.arenasolutions.com/v1`) and the SAP S/4HANA released OData APIs — are governed by their providers' contracts and are out of scope for this document.\n\nStability: this is a POC-stage contract. The Arena webhook payload (ArenaWebhookEvent) is provisional pending the Arena POC, and the authentication scheme is a placeholder pending PRD open question OQ-6."
|
||||
},
|
||||
"servers": [
|
||||
{
|
||||
"url": "/",
|
||||
"description": "CF route, decided at deployment (OQ-6)"
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
{
|
||||
"name": "system",
|
||||
"description": "Liveness/readiness for Cloud Foundry health checks."
|
||||
},
|
||||
{
|
||||
"name": "webhooks",
|
||||
"description": "Inbound event intake from Arena PLM (ARN-005)."
|
||||
},
|
||||
{
|
||||
"name": "admin",
|
||||
"description": "Machine-to-machine administrative operations (sync run control)."
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"/health": {
|
||||
"get": {
|
||||
"operationId": "getHealth",
|
||||
"summary": "Liveness/readiness probe",
|
||||
"description": "Used by Cloud Foundry health checks. Unauthenticated by design; exposes no business data. `arenaSession.state` reflects the worker's serialized Arena session lifecycle (Arena sessions have a ~25-minute TTL and logins must be serialized; see KNOWLEDGE.md). `poller` reports the delta poller's most recent tick; it is absent until the poller has ticked at least once.",
|
||||
"tags": [
|
||||
"system"
|
||||
],
|
||||
"security": [],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Worker is up (HTTP 200 in both states — this is a liveness probe, not a readiness gate). `status` is `degraded` when the most recent delta-poll tick failed (an Arena error, or the ingest buffer being full at poll time — the tick is failed without advancing the cursor so the window is re-covered) or the most recent Arena login failed; it returns to `ok` on the next successful tick/login.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HealthStatus"
|
||||
},
|
||||
"examples": {
|
||||
"ok": {
|
||||
"summary": "Fully operational",
|
||||
"value": {
|
||||
"status": "ok",
|
||||
"version": "0.1.0",
|
||||
"uptimeSeconds": 5231.7,
|
||||
"arenaSession": {
|
||||
"state": "active"
|
||||
},
|
||||
"poller": {
|
||||
"lastRunAt": "2026-09-14T08:15:00Z",
|
||||
"lastSuccessAt": "2026-09-14T08:15:00Z"
|
||||
}
|
||||
}
|
||||
},
|
||||
"degraded": {
|
||||
"summary": "Last poll tick failed",
|
||||
"value": {
|
||||
"status": "degraded",
|
||||
"version": "0.1.0",
|
||||
"uptimeSeconds": 5531.2,
|
||||
"arenaSession": {
|
||||
"state": "expired"
|
||||
},
|
||||
"poller": {
|
||||
"lastRunAt": "2026-09-14T08:20:00Z",
|
||||
"lastSuccessAt": "2026-09-14T08:15:00Z",
|
||||
"lastError": "Upstream Arena PLM request failed (HTTP 503)."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"default": {
|
||||
"$ref": "#/components/responses/Default"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/webhooks/arena": {
|
||||
"post": {
|
||||
"operationId": "receiveArenaWebhook",
|
||||
"summary": "Arena PLM event intake",
|
||||
"description": "Receives Arena PLM webhook events (ARN-005). The worker validates only that the body is a JSON object, then queues the event for asynchronous processing; acceptance (202) does not imply the event was processed or will result in a staged change. The ingest queue is a bounded transient buffer (`WEBHOOK_QUEUE_MAX_SIZE`, default 10000; never durable worker-local storage — ARN-003); when it is full the event is NOT accepted and the sender must retry (503 with `Retry-After`). The request body schema (ArenaWebhookEvent) is PROVISIONAL pending the Arena POC and will be tightened then.",
|
||||
"tags": [
|
||||
"webhooks"
|
||||
],
|
||||
"security": [
|
||||
{
|
||||
"bearerAuth": []
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ArenaWebhookEvent"
|
||||
},
|
||||
"example": {
|
||||
"eventType": "ITEM_MODIFIED",
|
||||
"itemGuid": "K3C9D0E1F2",
|
||||
"itemNumber": "900-00042",
|
||||
"occurredAt": "2026-09-10T08:15:30Z"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"202": {
|
||||
"description": "Event accepted and queued for asynchronous processing.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/WebhookAccepted"
|
||||
},
|
||||
"example": {
|
||||
"received": true
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Malformed request (e.g. body is not a JSON object).",
|
||||
"content": {
|
||||
"application/problem+json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Problem"
|
||||
},
|
||||
"example": {
|
||||
"title": "Bad Request",
|
||||
"status": 400,
|
||||
"detail": "Request body must be a JSON object."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"$ref": "#/components/responses/Unauthorized"
|
||||
},
|
||||
"413": {
|
||||
"$ref": "#/components/responses/PayloadTooLarge"
|
||||
},
|
||||
"415": {
|
||||
"$ref": "#/components/responses/UnsupportedMediaType"
|
||||
},
|
||||
"503": {
|
||||
"description": "Ingest queue is full (bounded transient buffer at `WEBHOOK_QUEUE_MAX_SIZE`). The event was NOT queued; the sender should retry after the `Retry-After` delay. Backpressure is preferred over unbounded memory growth or spilling to worker-local storage (ARN-003).",
|
||||
"headers": {
|
||||
"Retry-After": {
|
||||
"description": "Seconds the sender should wait before retrying (RFC 9110 §10.2.3, delay-seconds form). The worker sends 30.",
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"minimum": 1
|
||||
},
|
||||
"example": 30
|
||||
}
|
||||
},
|
||||
"content": {
|
||||
"application/problem+json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Problem"
|
||||
},
|
||||
"example": {
|
||||
"title": "Service Unavailable",
|
||||
"status": 503,
|
||||
"detail": "Ingest queue is full; retry later."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"default": {
|
||||
"$ref": "#/components/responses/Default"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/admin/sync-runs": {
|
||||
"post": {
|
||||
"operationId": "startSyncRun",
|
||||
"summary": "Start a sync run",
|
||||
"description": "Starts a sync run (full or delta). When `objects` is omitted, the run uses the worker's configured chain order (default PRODUCT → CHANGE_MASTER → BOM → DOCUMENTS; may be a subset via SYNC_OBJECT_ORDER) — requirement ARN-001: config-driven sync chain order. When `objects` is provided, it selects and orders the object types for this run.",
|
||||
"tags": [
|
||||
"admin"
|
||||
],
|
||||
"security": [
|
||||
{
|
||||
"bearerAuth": []
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/SyncRunRequest"
|
||||
},
|
||||
"example": {
|
||||
"scope": "full"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "Sync run created. Execution is asynchronous; poll `GET /admin/sync-runs/{runId}` for progress. All entries in `objects` start as `pending` with no `processed` counter (it appears once the object has started).",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/SyncRun"
|
||||
},
|
||||
"example": {
|
||||
"runId": "7b1e9a4c-2f6d-4c3a-9e8b-5d0f1a2b3c4d",
|
||||
"scope": "full",
|
||||
"state": "queued",
|
||||
"startedAt": "2026-09-10T08:20:00Z",
|
||||
"objects": [
|
||||
{
|
||||
"objectType": "PRODUCT",
|
||||
"state": "pending"
|
||||
},
|
||||
{
|
||||
"objectType": "CHANGE_MASTER",
|
||||
"state": "pending"
|
||||
},
|
||||
{
|
||||
"objectType": "BOM",
|
||||
"state": "pending"
|
||||
},
|
||||
{
|
||||
"objectType": "DOCUMENTS",
|
||||
"state": "pending"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Malformed request: invalid `scope` value, `objects` containing an unknown or duplicate object type, a property of the wrong type (no coercion — `{\"scope\": [\"full\"]}` or `{\"objects\": \"BOM\"}` are rejected), or any property not defined by SyncRunRequest (strict rejection, `additionalProperties: false`).",
|
||||
"content": {
|
||||
"application/problem+json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Problem"
|
||||
},
|
||||
"example": {
|
||||
"title": "Bad Request",
|
||||
"status": 400,
|
||||
"detail": "Invalid value 'incremental' for 'scope'; expected 'full' or 'delta'."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"$ref": "#/components/responses/Unauthorized"
|
||||
},
|
||||
"413": {
|
||||
"$ref": "#/components/responses/PayloadTooLarge"
|
||||
},
|
||||
"415": {
|
||||
"$ref": "#/components/responses/UnsupportedMediaType"
|
||||
},
|
||||
"default": {
|
||||
"$ref": "#/components/responses/Default"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/admin/sync-runs/{runId}": {
|
||||
"get": {
|
||||
"operationId": "getSyncRun",
|
||||
"summary": "Get a sync run by ID",
|
||||
"description": "Returns the current state of a sync run, including per-object progress in execution order.",
|
||||
"tags": [
|
||||
"admin"
|
||||
],
|
||||
"security": [
|
||||
{
|
||||
"bearerAuth": []
|
||||
}
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "runId",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"description": "Identifier of the sync run, as returned by `POST /admin/sync-runs`.",
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "The sync run.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/SyncRun"
|
||||
},
|
||||
"example": {
|
||||
"runId": "7b1e9a4c-2f6d-4c3a-9e8b-5d0f1a2b3c4d",
|
||||
"scope": "full",
|
||||
"state": "failed",
|
||||
"startedAt": "2026-09-10T08:20:00Z",
|
||||
"finishedAt": "2026-09-10T08:34:12Z",
|
||||
"error": "BOM: Upstream Arena PLM request failed (HTTP 503).",
|
||||
"objects": [
|
||||
{
|
||||
"objectType": "PRODUCT",
|
||||
"state": "completed",
|
||||
"processed": 15
|
||||
},
|
||||
{
|
||||
"objectType": "CHANGE_MASTER",
|
||||
"state": "completed",
|
||||
"processed": 4
|
||||
},
|
||||
{
|
||||
"objectType": "BOM",
|
||||
"state": "failed",
|
||||
"processed": 9,
|
||||
"error": "Upstream Arena PLM request failed (HTTP 503)."
|
||||
},
|
||||
{
|
||||
"objectType": "DOCUMENTS",
|
||||
"state": "pending"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"$ref": "#/components/responses/Unauthorized"
|
||||
},
|
||||
"404": {
|
||||
"description": "No sync run exists with the given `runId`.",
|
||||
"content": {
|
||||
"application/problem+json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Problem"
|
||||
},
|
||||
"example": {
|
||||
"title": "Not Found",
|
||||
"status": 404,
|
||||
"detail": "Sync run 7b1e9a4c-2f6d-4c3a-9e8b-5d0f1a2b3c4d not found."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"default": {
|
||||
"$ref": "#/components/responses/Default"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"securitySchemes": {
|
||||
"bearerAuth": {
|
||||
"type": "http",
|
||||
"scheme": "bearer",
|
||||
"description": "PLACEHOLDER. The final authentication scheme for webhook and admin access is an OPEN decision (PRD OQ-6: webhook receiver hosting and auth for the worker on CF). This scheme stands in for whatever is decided (e.g. XSUAA-issued JWT, static bearer token) and this contract will be revised when OQ-6 is closed. Only `/health` is unauthenticated."
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"Unauthorized": {
|
||||
"description": "Missing or invalid bearer credentials.",
|
||||
"content": {
|
||||
"application/problem+json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Problem"
|
||||
},
|
||||
"example": {
|
||||
"title": "Unauthorized",
|
||||
"status": 401,
|
||||
"detail": "Missing or invalid bearer token."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"BadGateway": {
|
||||
"description": "An upstream Arena PLM request failed (network error, timeout, or non-success HTTP status from `api.arenasolutions.com`). RESERVED mapping: every operation that invokes Arena synchronously responds with this instead of 500. The upstream response body is never forwarded — `detail` is a fixed, generic phrase — so Arena error payloads (which may echo request data) cannot leak through the worker. No operation in this version calls Arena synchronously (sync runs and polls are asynchronous); the mapping is defined here so it is contract-governed before the first such operation lands.",
|
||||
"content": {
|
||||
"application/problem+json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Problem"
|
||||
},
|
||||
"example": {
|
||||
"title": "Bad Gateway",
|
||||
"status": 502,
|
||||
"detail": "Upstream Arena PLM request failed."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Default": {
|
||||
"description": "Any status code not listed explicitly for the operation. Every response the worker emits is governed: the body is always a `Problem` (RFC 9457, `application/problem+json`). Cases: an unmatched route or method (404, see `x-unmatched-routes`); a status raised by the HTTP framework before the handler runs (e.g. 400 for a `Content-Length` mismatch or an empty `application/json` body); an unhandled error (500). 5xx bodies never carry a `detail` derived from an error message — `title` mirrors the HTTP status phrase and nothing else is disclosed.",
|
||||
"content": {
|
||||
"application/problem+json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Problem"
|
||||
},
|
||||
"example": {
|
||||
"title": "Internal Server Error",
|
||||
"status": 500
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"UnsupportedMediaType": {
|
||||
"description": "The request `Content-Type` is not `application/json`. Only `application/json` is parsed; every other media type (including `text/plain`, whose built-in parser is removed) and a missing `Content-Type` on a request with a body is rejected before the handler runs. [source: context7 /fastify/fastify docs/Reference/Errors.md FST_ERR_CTP_INVALID_MEDIA_TYPE; docs/Reference/ContentTypeParser.md removeContentTypeParser, verified 2026-09-14]",
|
||||
"content": {
|
||||
"application/problem+json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Problem"
|
||||
},
|
||||
"example": {
|
||||
"title": "Unsupported Media Type",
|
||||
"status": 415,
|
||||
"detail": "Unsupported Media Type"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"PayloadTooLarge": {
|
||||
"description": "The request body exceeds the worker's body limit (framework default 1 MiB = 1048576 bytes; the worker does not raise it). The body is not parsed and nothing is queued or created. [source: context7 /fastify/fastify docs/Reference/Server.md `bodyLimit`; docs/Reference/Errors.md FST_ERR_CTP_BODY_TOO_LARGE, verified 2026-09-14]",
|
||||
"content": {
|
||||
"application/problem+json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Problem"
|
||||
},
|
||||
"example": {
|
||||
"title": "Payload Too Large",
|
||||
"status": 413,
|
||||
"detail": "Request body is too large"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"schemas": {
|
||||
"HealthStatus": {
|
||||
"type": "object",
|
||||
"description": "Worker liveness/readiness report.",
|
||||
"required": [
|
||||
"status",
|
||||
"version",
|
||||
"uptimeSeconds",
|
||||
"arenaSession"
|
||||
],
|
||||
"properties": {
|
||||
"status": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"ok",
|
||||
"degraded"
|
||||
],
|
||||
"description": "`ok` when fully operational; `degraded` when the most recent delta-poll tick failed or the most recent Arena login failed. A tick fails when Arena cannot be reached/answers an error, or when the ingest buffer is full at poll time (before the fetch, or partially while enqueuing) — the tick is failed rather than dropping changes, the poll cursor holds, and the next tick re-covers the window (duplicates on recovery are acceptable: SAP is the ledger, KNOWLEDGE.md 2026-09-04). Derived from the worker's health registry, which the poller and the Arena session manager report into."
|
||||
},
|
||||
"version": {
|
||||
"type": "string",
|
||||
"description": "Deployed worker version.",
|
||||
"examples": [
|
||||
"0.1.0"
|
||||
]
|
||||
},
|
||||
"uptimeSeconds": {
|
||||
"type": "number",
|
||||
"minimum": 0,
|
||||
"description": "Seconds since the worker process started."
|
||||
},
|
||||
"arenaSession": {
|
||||
"type": "object",
|
||||
"description": "State of the worker's (serialized) Arena PLM API session.",
|
||||
"required": [
|
||||
"state"
|
||||
],
|
||||
"properties": {
|
||||
"state": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"none",
|
||||
"active",
|
||||
"expired",
|
||||
"refreshing"
|
||||
],
|
||||
"description": "`none`: no session has been established since process start; `active`: a cached session exists and is within its configured TTL; `expired`: a cached session exists but is past its TTL (the next Arena call re-logs in lazily); `refreshing`: a login is in flight (serialized behind the promise lock — concurrent callers join it). `refreshing` takes precedence over the other three while a login is in flight."
|
||||
}
|
||||
}
|
||||
},
|
||||
"poller": {
|
||||
"type": "object",
|
||||
"description": "Most recent delta-poll tick. Absent until the poller has ticked at least once. Timestamps are the worker's clock at the start of the tick.",
|
||||
"properties": {
|
||||
"lastRunAt": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"description": "Start time of the most recent tick (successful or not)."
|
||||
},
|
||||
"lastSuccessAt": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"description": "Start time of the most recent tick that completed without error. Absent if no tick has succeeded yet."
|
||||
},
|
||||
"lastError": {
|
||||
"type": "string",
|
||||
"description": "Fixed, non-sensitive phrase classifying the failure of the most recent tick (allowlist keyed on error class / upstream HTTP status, e.g. `Upstream Arena PLM request failed (HTTP 503).`, `Ingest buffer full at poll time.`, `Unclassified worker error; see worker logs.`); absent when the most recent tick succeeded. Never the raw error message — upstream response bodies, credentials, and session tokens cannot appear here; the full error stays in the worker log."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"ArenaWebhookEvent": {
|
||||
"type": "object",
|
||||
"additionalProperties": true,
|
||||
"description": "PROVISIONAL envelope for Arena PLM webhook events. All fields are optional and unknown properties are accepted (`additionalProperties: true`) because the real Arena webhook payload shape has not yet been verified against the live Arena API. This schema is provisional pending the Arena POC and WILL be tightened (required fields, closed property set, event-type enum) once the POC establishes ground truth. Do not code against fields beyond those named here.",
|
||||
"properties": {
|
||||
"eventType": {
|
||||
"type": "string",
|
||||
"description": "Arena event type identifier (exact vocabulary to be established by the Arena POC).",
|
||||
"examples": [
|
||||
"ITEM_MODIFIED"
|
||||
]
|
||||
},
|
||||
"itemGuid": {
|
||||
"type": "string",
|
||||
"description": "Arena internal identifier (GUID) of the affected item, if the event concerns an item."
|
||||
},
|
||||
"itemNumber": {
|
||||
"type": "string",
|
||||
"description": "Arena item number of the affected item, if the event concerns an item.",
|
||||
"examples": [
|
||||
"900-00042"
|
||||
]
|
||||
},
|
||||
"occurredAt": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"description": "When the event occurred in Arena, if provided."
|
||||
}
|
||||
}
|
||||
},
|
||||
"WebhookAccepted": {
|
||||
"type": "object",
|
||||
"description": "Acknowledgement that a webhook event was accepted and queued.",
|
||||
"required": [
|
||||
"received"
|
||||
],
|
||||
"properties": {
|
||||
"received": {
|
||||
"type": "boolean",
|
||||
"const": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"SyncRunRequest": {
|
||||
"type": "object",
|
||||
"description": "Request to start a sync run. STRICT: unknown properties are rejected with 400 (seam ruling 2026-09-10 — on a machine-to-machine admin API a typo'd property such as `object` instead of `objects` must never silently fall back to defaults). Values are validated as sent: no type coercion (`{\"scope\": [\"full\"]}` is 400, not unwrapped to `\"full\"`) and no server-side default injection.",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"scope"
|
||||
],
|
||||
"properties": {
|
||||
"scope": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"full",
|
||||
"delta"
|
||||
],
|
||||
"description": "`full`: initial/complete sync; `delta`: changes since the last successful run."
|
||||
},
|
||||
"objects": {
|
||||
"type": "array",
|
||||
"description": "Object types to sync, in execution order. Omit to use the worker's configured chain order (default PRODUCT → CHANGE_MASTER → BOM → DOCUMENTS; may be a subset via SYNC_OBJECT_ORDER) — requirement ARN-001. No schema `default` is declared: the server injects nothing (`useDefaults: false`) and the effective order is the worker's configuration, not a contract constant.",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/SyncObjectType"
|
||||
},
|
||||
"minItems": 1,
|
||||
"uniqueItems": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"SyncObjectType": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"PRODUCT",
|
||||
"CHANGE_MASTER",
|
||||
"BOM",
|
||||
"DOCUMENTS"
|
||||
],
|
||||
"description": "Object types handled by the sync chain (ARN-001: PRODUCT → CHANGE_MASTER → BOM → DOCUMENTS). The token is `DOCUMENTS` (plural), as in the requirement."
|
||||
},
|
||||
"SyncRun": {
|
||||
"type": "object",
|
||||
"description": "A sync run and its current state. The run executes its object types sequentially in the order of `objects` (ARN-001) and stops the chain at the first failed object — remaining objects stay `pending`. `finishedAt` is set when the run reaches `completed` or `failed`.",
|
||||
"required": [
|
||||
"runId",
|
||||
"scope",
|
||||
"state",
|
||||
"startedAt",
|
||||
"objects"
|
||||
],
|
||||
"properties": {
|
||||
"runId": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "Server-assigned identifier of the run."
|
||||
},
|
||||
"scope": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"full",
|
||||
"delta"
|
||||
]
|
||||
},
|
||||
"state": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"queued",
|
||||
"running",
|
||||
"completed",
|
||||
"failed"
|
||||
],
|
||||
"description": "`completed`: every object in the chain completed. `failed`: the run aborted — the chain stopped at a failed object or a run-level error occurred; see `error` and the per-object entries in `objects`."
|
||||
},
|
||||
"startedAt": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"description": "When the run was created/accepted."
|
||||
},
|
||||
"finishedAt": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"description": "When the run reached a terminal state (`completed` or `failed`). Absent while queued or running."
|
||||
},
|
||||
"objects": {
|
||||
"type": "array",
|
||||
"description": "Per-object progress in execution order for this run (ARN-001). An ordered progress array communicates chain order and stop-on-failure directly, which a keyed counts map cannot. All entries are `pending` at run creation.",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/SyncObjectProgress"
|
||||
},
|
||||
"minItems": 1
|
||||
},
|
||||
"error": {
|
||||
"type": "string",
|
||||
"description": "Run-level abort reason; present when `state` is `failed`. The failed object type followed by the same fixed, non-sensitive phrase recorded in that object's `error` (e.g. `BOM: Upstream Arena PLM request failed (HTTP 503).`), or `<type>: no executor registered`. Never a raw error message."
|
||||
}
|
||||
}
|
||||
},
|
||||
"SyncObjectProgress": {
|
||||
"type": "object",
|
||||
"description": "Progress of one object type within a sync run's sequential execution chain.",
|
||||
"required": [
|
||||
"objectType",
|
||||
"state"
|
||||
],
|
||||
"properties": {
|
||||
"objectType": {
|
||||
"$ref": "#/components/schemas/SyncObjectType"
|
||||
},
|
||||
"state": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"pending",
|
||||
"running",
|
||||
"completed",
|
||||
"failed"
|
||||
],
|
||||
"description": "All entries are `pending` at run creation; each advances to `running`, then `completed` or `failed`, in chain order. A `failed` entry stops the chain; entries after it remain `pending`."
|
||||
},
|
||||
"processed": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"description": "Number of records processed for this object type; present once the object has run (from `running` onwards), absent while `pending`. POC-stage granularity: this will be refined into created/updated/failed counters when the S/4HANA execution leg lands (same tightening wave as ArenaWebhookEvent)."
|
||||
},
|
||||
"error": {
|
||||
"type": "string",
|
||||
"description": "Fixed, non-sensitive phrase classifying the failure (allowlist keyed on error class / upstream HTTP status — the same mapping as `HealthStatus.poller.lastError`); present when this object's `state` is `failed`. Never the raw error message: upstream response bodies, credentials, and session tokens cannot appear here; the full error stays in the worker log."
|
||||
}
|
||||
}
|
||||
},
|
||||
"Problem": {
|
||||
"type": "object",
|
||||
"description": "Error envelope per RFC 9457 (application/problem+json).",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"format": "uri-reference",
|
||||
"description": "URI reference identifying the problem type. Defaults to `about:blank`.",
|
||||
"default": "about:blank"
|
||||
},
|
||||
"title": {
|
||||
"type": "string",
|
||||
"description": "Short, human-readable summary of the problem type."
|
||||
},
|
||||
"status": {
|
||||
"type": "integer",
|
||||
"minimum": 100,
|
||||
"maximum": 599,
|
||||
"description": "HTTP status code for this occurrence."
|
||||
},
|
||||
"detail": {
|
||||
"type": "string",
|
||||
"description": "Human-readable explanation specific to this occurrence."
|
||||
},
|
||||
"instance": {
|
||||
"type": "string",
|
||||
"format": "uri-reference",
|
||||
"description": "URI reference identifying this specific occurrence."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"x-unmatched-routes": "Any request whose method+path is not listed under `paths` is answered 404 with a `Problem` body (application/problem+json), including a known path with an unlisted method — the router answers 404, not 405, for a method mismatch (probed against the worker 2026-09-14; Fastify default not-found handling, see `components.responses.Default`). It is the route-level case of every operation's `default` response and cannot be attached to an operation because no operation matches."
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
# RAP handover — Arena change-approval BO (stage 1, 2026-09-15)
|
||||
|
||||
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".
|
||||
|
||||
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).
|
||||
|
||||
## What exists in the tenant right now
|
||||
|
||||
Generated by `x-ui-service` (all **active**, ATC 0 findings on 12 objects, 2026-09-15):
|
||||
|
||||
| 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 |
|
||||
|
||||
Created empty via `create_object` (exist, **no source, inactive**):
|
||||
|
||||
| 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) |
|
||||
|
||||
Not yet creatable (the MCP validation refuses until the referenced objects are active):
|
||||
|
||||
| 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" |
|
||||
|
||||
## Paste-and-activate sequence
|
||||
|
||||
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.
|
||||
|
||||
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).
|
||||
|
||||
## Activation URIs for `abap_activate_objects` (after pasting)
|
||||
|
||||
Pattern learned live 2026-09-15: `abap:/repotree-v1/0M7DEJI/System%20Library/ZLOCAL/ZARENA_PLM/<folder>/<NAME>/<name>.<ext>`
|
||||
with these folders/extensions (all verified by a successful activation):
|
||||
|
||||
| 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` |
|
||||
|
||||
In sequence order (`B` = `abap:/repotree-v1/0M7DEJI/System%20Library/ZLOCAL/ZARENA_PLM`):
|
||||
|
||||
```
|
||||
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
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
## Design decisions baked into the files (falsifiable, dated 2026-09-15)
|
||||
|
||||
- **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.
|
||||
|
||||
## Landmines learned live (KNOWLEDGE.md candidates)
|
||||
|
||||
- [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 →
|
||||
"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.
|
||||
@@ -0,0 +1,10 @@
|
||||
/* 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
|
||||
{
|
||||
expose ZC_CHANGEAPPROVAL_A2X as ChangeApproval;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
// 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).
|
||||
projection;
|
||||
strict ( 2 );
|
||||
|
||||
define behavior for ZC_CHANGEAPPROVAL_A2X alias ChangeApproval
|
||||
{
|
||||
use etag;
|
||||
use create;
|
||||
|
||||
use action markExecuting;
|
||||
use action markSynced;
|
||||
use action markFailed;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
/* TARGET OBJECT : access control ZC_CHANGEAPPROVAL_A2X (created empty via ADT MCP 2026-09-15)
|
||||
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. */
|
||||
@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;
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/* TARGET OBJECT : data definition ZC_CHANGEAPPROVAL_A2X (A2X projection for the worker; created empty via ADT MCP 2026-09-15)
|
||||
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).
|
||||
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 -
|
||||
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
|
||||
{
|
||||
key ID,
|
||||
ItemNumber,
|
||||
ItemName,
|
||||
ChangeType,
|
||||
ArenaEcoId,
|
||||
@ObjectModel.text.element: ['StatusText']
|
||||
Status,
|
||||
StatusText,
|
||||
StatusCriticality,
|
||||
RequestedBy,
|
||||
RequestedOn,
|
||||
DecidedBy,
|
||||
DecidedOn,
|
||||
SapChangeNumber,
|
||||
ErrorMessage,
|
||||
RejectReason,
|
||||
SapPreview,
|
||||
IsApprovable,
|
||||
IsRetriable,
|
||||
CreatedBy,
|
||||
CreatedAt,
|
||||
LastChangedBy,
|
||||
LastChangedAt,
|
||||
LocalLastChangedAt
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
/* TARGET OBJECT : data definition ZD_CHANGEAPPROVAL_FAILEDP (abstract entity = parameter of action markFailed; created empty via ADT MCP 2026-09-15)
|
||||
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.
|
||||
Sources: as ZD_CHANGEAPPROVAL_REJECTP. */
|
||||
@EndUserText.label: 'Arena change approval: markFailed parameter'
|
||||
define abstract entity ZD_CHANGEAPPROVAL_FAILEDP
|
||||
{
|
||||
@EndUserText.label: 'Error message'
|
||||
ErrorMessage : abap.char(255);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
/* TARGET OBJECT : data definition ZD_CHANGEAPPROVAL_REJECTP (abstract entity = parameter of action reject; created empty via ADT MCP 2026-09-15)
|
||||
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.
|
||||
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].
|
||||
The OData parameter name is the element name -> RejectReason, as the Fiori app expects (PRD RAP-2). */
|
||||
@EndUserText.label: 'Arena change approval: reject parameter'
|
||||
define abstract entity ZD_CHANGEAPPROVAL_REJECTP
|
||||
{
|
||||
@EndUserText.label: 'Reject reason'
|
||||
RejectReason : abap.char(255);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
/* TARGET OBJECT : data definition ZD_CHANGEAPPROVAL_SYNCEDP (abstract entity = parameter of action markSynced; created empty via ADT MCP 2026-09-15)
|
||||
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.
|
||||
Sources: as ZD_CHANGEAPPROVAL_REJECTP. */
|
||||
@EndUserText.label: 'Arena change approval: markSynced parameter'
|
||||
define abstract entity ZD_CHANGEAPPROVAL_SYNCEDP
|
||||
{
|
||||
@EndUserText.label: 'SAP change number'
|
||||
SapChangeNumber : abap.char(12);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
" 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.
|
||||
@@ -0,0 +1,426 @@
|
||||
" 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-<name> / 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(<approval>).
|
||||
IF sy-subrc <> 0.
|
||||
CONTINUE.
|
||||
ENDIF.
|
||||
|
||||
IF <approval>-Status <> status-pending.
|
||||
APPEND VALUE #( %tky = <approval>-%tky ) TO failed-changeapproval.
|
||||
APPEND VALUE #( %tky = <approval>-%tky
|
||||
%msg = new_message_with_text(
|
||||
severity = if_abap_behv_message=>severity-error
|
||||
text = |Only PENDING records can be approved (status is { <approval>-Status })| ) )
|
||||
TO reported-changeapproval.
|
||||
CONTINUE.
|
||||
ENDIF.
|
||||
|
||||
MODIFY ENTITIES OF zzr_changeapproval000 IN LOCAL MODE
|
||||
ENTITY ChangeApproval
|
||||
UPDATE FIELDS ( Status DecidedBy DecidedOn )
|
||||
WITH VALUE #( ( %tky = <approval>-%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(<approval>).
|
||||
IF sy-subrc <> 0.
|
||||
CONTINUE.
|
||||
ENDIF.
|
||||
|
||||
IF <approval>-Status <> status-pending.
|
||||
APPEND VALUE #( %tky = <approval>-%tky ) TO failed-changeapproval.
|
||||
APPEND VALUE #( %tky = <approval>-%tky
|
||||
%msg = new_message_with_text(
|
||||
severity = if_abap_behv_message=>severity-error
|
||||
text = |Only PENDING records can be rejected (status is { <approval>-Status })| ) )
|
||||
TO reported-changeapproval.
|
||||
CONTINUE.
|
||||
ENDIF.
|
||||
|
||||
IF key-%param-RejectReason IS INITIAL.
|
||||
APPEND VALUE #( %tky = <approval>-%tky ) TO failed-changeapproval.
|
||||
APPEND VALUE #( %tky = <approval>-%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 = <approval>-%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(<approval>).
|
||||
IF sy-subrc <> 0.
|
||||
CONTINUE.
|
||||
ENDIF.
|
||||
|
||||
IF <approval>-Status <> status-failed.
|
||||
APPEND VALUE #( %tky = <approval>-%tky ) TO failed-changeapproval.
|
||||
APPEND VALUE #( %tky = <approval>-%tky
|
||||
%msg = new_message_with_text(
|
||||
severity = if_abap_behv_message=>severity-error
|
||||
text = |Only FAILED records can be retried (status is { <approval>-Status })| ) )
|
||||
TO reported-changeapproval.
|
||||
CONTINUE.
|
||||
ENDIF.
|
||||
|
||||
MODIFY ENTITIES OF zzr_changeapproval000 IN LOCAL MODE
|
||||
ENTITY ChangeApproval
|
||||
UPDATE FIELDS ( Status ErrorMessage )
|
||||
WITH VALUE #( ( %tky = <approval>-%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(<approval>).
|
||||
IF sy-subrc <> 0.
|
||||
CONTINUE.
|
||||
ENDIF.
|
||||
|
||||
IF <approval>-Status <> status-approved.
|
||||
APPEND VALUE #( %tky = <approval>-%tky ) TO failed-changeapproval.
|
||||
APPEND VALUE #( %tky = <approval>-%tky
|
||||
%msg = new_message_with_text(
|
||||
severity = if_abap_behv_message=>severity-error
|
||||
text = |Only APPROVED records can start executing (status is { <approval>-Status })| ) )
|
||||
TO reported-changeapproval.
|
||||
CONTINUE.
|
||||
ENDIF.
|
||||
|
||||
MODIFY ENTITIES OF zzr_changeapproval000 IN LOCAL MODE
|
||||
ENTITY ChangeApproval
|
||||
UPDATE FIELDS ( Status )
|
||||
WITH VALUE #( ( %tky = <approval>-%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(<approval>).
|
||||
IF sy-subrc <> 0.
|
||||
CONTINUE.
|
||||
ENDIF.
|
||||
|
||||
IF <approval>-Status <> status-executing.
|
||||
APPEND VALUE #( %tky = <approval>-%tky ) TO failed-changeapproval.
|
||||
APPEND VALUE #( %tky = <approval>-%tky
|
||||
%msg = new_message_with_text(
|
||||
severity = if_abap_behv_message=>severity-error
|
||||
text = |Only EXECUTING records can be marked synced (status is { <approval>-Status })| ) )
|
||||
TO reported-changeapproval.
|
||||
CONTINUE.
|
||||
ENDIF.
|
||||
|
||||
IF key-%param-SapChangeNumber IS INITIAL.
|
||||
APPEND VALUE #( %tky = <approval>-%tky ) TO failed-changeapproval.
|
||||
APPEND VALUE #( %tky = <approval>-%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 = <approval>-%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(<approval>).
|
||||
IF sy-subrc <> 0.
|
||||
CONTINUE.
|
||||
ENDIF.
|
||||
|
||||
IF <approval>-Status <> status-executing.
|
||||
APPEND VALUE #( %tky = <approval>-%tky ) TO failed-changeapproval.
|
||||
APPEND VALUE #( %tky = <approval>-%tky
|
||||
%msg = new_message_with_text(
|
||||
severity = if_abap_behv_message=>severity-error
|
||||
text = |Only EXECUTING records can be marked failed (status is { <approval>-Status })| ) )
|
||||
TO reported-changeapproval.
|
||||
CONTINUE.
|
||||
ENDIF.
|
||||
|
||||
IF key-%param-ErrorMessage IS INITIAL.
|
||||
APPEND VALUE #( %tky = <approval>-%tky ) TO failed-changeapproval.
|
||||
APPEND VALUE #( %tky = <approval>-%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 = <approval>-%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.
|
||||
@@ -0,0 +1,20 @@
|
||||
// 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;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
/* 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;
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/* 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).
|
||||
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]. */
|
||||
@EndUserText.label: 'Arena change approval - UI projection'
|
||||
@AccessControl.authorizationCheck: #CHECK
|
||||
@Metadata.allowExtensions: true
|
||||
@ObjectModel.semanticKey: ['ItemNumber', 'ChangeType', 'ArenaEcoId']
|
||||
define root view entity ZZC_CHANGEAPPROVAL000
|
||||
provider contract transactional_query
|
||||
as projection on ZZR_CHANGEAPPROVAL000
|
||||
{
|
||||
key ID,
|
||||
ItemNumber,
|
||||
ItemName,
|
||||
ChangeType,
|
||||
ArenaEcoId,
|
||||
@ObjectModel.text.element: ['StatusText']
|
||||
Status,
|
||||
StatusText,
|
||||
StatusCriticality,
|
||||
RequestedBy,
|
||||
RequestedOn,
|
||||
DecidedBy,
|
||||
DecidedOn,
|
||||
SapChangeNumber,
|
||||
ErrorMessage,
|
||||
RejectReason,
|
||||
SapPreview,
|
||||
IsApprovable,
|
||||
IsRetriable,
|
||||
LocalLastChangedAt
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
/* 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).
|
||||
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):
|
||||
@Metadata.layer + ANNOTATE ENTITY [abap-docs /abap-docs-cloud/ABENCDS_F1_ANNOTATE_VIEW]
|
||||
@UI.headerInfo typeName/typeNamePlural/typeImageUrl/title/description [05_object_page_header#title-and-subtitle]
|
||||
@UI.presentationVariant sortOrder + visualizations [03_list_report_content (Defining the Default Sort Order)]
|
||||
@UI.facet purpose/type/id/label/position/targetQualifier [06_object_page_content (Nested Tabs, RAPTreeview)]
|
||||
@UI.dataPoint qualifier/title/criticality + #DATAPOINT_REFERENCE header facet [05_object_page_header (Key Value, Progress)]
|
||||
@UI.identification #FOR_ACTION dataAction/label [05_object_page_header (Regular Actions)]
|
||||
@UI.lineItem position/importance/criticality [03_list_report_content (Configuring Tables); 06 (Table)]
|
||||
@UI.selectionField position [02_list_report_header#selection-fields]
|
||||
@UI.fieldGroup qualifier/position [06_object_page_content (Multi Input Field)]
|
||||
@UI.textArrangement #TEXT_ONLY [06_object_page_content#displaying-text-instead-of-id]
|
||||
@UI.multiLineText [05_object_page_header (Plain Text)]; @UI.hidden [06_object_page_content (Hiding Features)]
|
||||
Default filter Status = PENDING is NOT set here (string-literal escaping inside @UI.selectionVariant.filter is
|
||||
unverified); the app's annotation.xml SelectionVariant#Default provides it. */
|
||||
@Metadata.layer: #CUSTOMER
|
||||
@UI: {
|
||||
headerInfo: {
|
||||
typeName: 'Change Approval',
|
||||
typeNamePlural: 'Change Approvals',
|
||||
typeImageUrl: 'sap-icon://approvals',
|
||||
title: { type: #STANDARD, value: 'ItemNumber' },
|
||||
description: { type: #STANDARD, value: 'ItemName' }
|
||||
},
|
||||
presentationVariant: [ {
|
||||
sortOrder: [ { by: 'Status', direction: #ASC }, { by: 'RequestedOn', direction: #DESC } ],
|
||||
visualizations: [ { type: #AS_LINEITEM } ]
|
||||
} ]
|
||||
}
|
||||
annotate entity ZZC_CHANGEAPPROVAL000 with
|
||||
{
|
||||
@UI.facet: [
|
||||
{ id: 'StatusPoint', purpose: #HEADER, type: #DATAPOINT_REFERENCE, targetQualifier: 'StatusPoint' },
|
||||
{ id: 'Keys', purpose: #HEADER, type: #FIELDGROUP_REFERENCE, targetQualifier: 'Keys', label: 'Reference numbers' },
|
||||
{ id: 'Change', purpose: #STANDARD, type: #FIELDGROUP_REFERENCE, targetQualifier: 'Change', label: 'Change', position: 10 },
|
||||
{ id: 'Payload', purpose: #STANDARD, type: #FIELDGROUP_REFERENCE, targetQualifier: 'Payload', label: 'Staged payload', position: 20 },
|
||||
{ id: 'Approval', purpose: #STANDARD, type: #FIELDGROUP_REFERENCE, targetQualifier: 'Approval', label: 'Approval', position: 30 },
|
||||
{ id: 'Execution', purpose: #STANDARD, type: #FIELDGROUP_REFERENCE, targetQualifier: 'Execution', label: 'Execution', position: 40 }
|
||||
]
|
||||
@UI.identification: [
|
||||
{ position: 10, type: #FOR_ACTION, dataAction: 'approve', label: 'Approve' },
|
||||
{ position: 20, type: #FOR_ACTION, dataAction: 'reject', label: 'Reject' },
|
||||
{ position: 30, type: #FOR_ACTION, dataAction: 'retrySync', label: 'Retry sync' }
|
||||
]
|
||||
@UI.hidden: true
|
||||
ID;
|
||||
|
||||
@UI: {
|
||||
lineItem: [ { position: 10, importance: #HIGH } ],
|
||||
selectionField: [ { position: 10 } ],
|
||||
fieldGroup: [ { qualifier: 'Change', position: 10 } ]
|
||||
}
|
||||
@EndUserText.label: 'Item number'
|
||||
ItemNumber;
|
||||
|
||||
@UI: {
|
||||
lineItem: [ { position: 20, importance: #LOW } ],
|
||||
fieldGroup: [ { qualifier: 'Change', position: 20 } ]
|
||||
}
|
||||
@EndUserText.label: 'Item name'
|
||||
ItemName;
|
||||
|
||||
@UI: {
|
||||
lineItem: [ { position: 30, importance: #HIGH } ],
|
||||
selectionField: [ { position: 30 } ],
|
||||
fieldGroup: [ { qualifier: 'Change', position: 30 } ]
|
||||
}
|
||||
@EndUserText.label: 'Change type'
|
||||
ChangeType;
|
||||
|
||||
@UI: {
|
||||
lineItem: [ { position: 60, importance: #MEDIUM } ],
|
||||
selectionField: [ { position: 40 } ],
|
||||
fieldGroup: [ { qualifier: 'Change', position: 40 }, { qualifier: 'Keys', position: 10 } ]
|
||||
}
|
||||
@EndUserText.label: 'Arena ECO'
|
||||
ArenaEcoId;
|
||||
|
||||
@UI: {
|
||||
lineItem: [ { position: 40, importance: #HIGH, criticality: 'StatusCriticality' } ],
|
||||
selectionField: [ { position: 20 } ],
|
||||
fieldGroup: [ { qualifier: 'Approval', position: 10, criticality: 'StatusCriticality' } ],
|
||||
dataPoint: { qualifier: 'StatusPoint', title: 'Status', criticality: 'StatusCriticality' },
|
||||
textArrangement: #TEXT_ONLY
|
||||
}
|
||||
@EndUserText.label: 'Status'
|
||||
Status;
|
||||
|
||||
@UI.hidden: true
|
||||
StatusText;
|
||||
|
||||
@UI.hidden: true
|
||||
StatusCriticality;
|
||||
|
||||
@UI.fieldGroup: [ { qualifier: 'Approval', position: 20 } ]
|
||||
@EndUserText.label: 'Requested by'
|
||||
RequestedBy;
|
||||
|
||||
@UI: {
|
||||
lineItem: [ { position: 50, importance: #MEDIUM } ],
|
||||
fieldGroup: [ { qualifier: 'Approval', position: 30 } ]
|
||||
}
|
||||
@EndUserText.label: 'Requested on'
|
||||
RequestedOn;
|
||||
|
||||
@UI.fieldGroup: [ { qualifier: 'Approval', position: 40 } ]
|
||||
@EndUserText.label: 'Decided by'
|
||||
DecidedBy;
|
||||
|
||||
@UI.fieldGroup: [ { qualifier: 'Approval', position: 50 } ]
|
||||
@EndUserText.label: 'Decided on'
|
||||
DecidedOn;
|
||||
|
||||
@UI.fieldGroup: [ { qualifier: 'Approval', position: 60 } ]
|
||||
@EndUserText.label: 'Reject reason'
|
||||
RejectReason;
|
||||
|
||||
@UI.fieldGroup: [ { qualifier: 'Execution', position: 10 }, { qualifier: 'Keys', position: 20 } ]
|
||||
@EndUserText.label: 'SAP change number'
|
||||
SapChangeNumber;
|
||||
|
||||
@UI.fieldGroup: [ { qualifier: 'Execution', position: 20 } ]
|
||||
@EndUserText.label: 'Error message'
|
||||
ErrorMessage;
|
||||
|
||||
@UI: {
|
||||
fieldGroup: [ { qualifier: 'Payload', position: 10 } ],
|
||||
multiLineText: true
|
||||
}
|
||||
@EndUserText.label: 'Staged payload'
|
||||
SapPreview;
|
||||
|
||||
@UI.hidden: true
|
||||
IsApprovable;
|
||||
|
||||
@UI.hidden: true
|
||||
IsRetriable;
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/* 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.
|
||||
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]. */
|
||||
@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 {
|
||||
key client : abap.clnt not null;
|
||||
key id : sysuuid_x16 not null;
|
||||
item_number : abap.char(18);
|
||||
item_name : abap.char(40);
|
||||
change_type : abap.char(15);
|
||||
arena_eco_id : abap.char(20);
|
||||
status : abap.char(10);
|
||||
requested_by : abap.char(12);
|
||||
requested_on : abap.utclong;
|
||||
decided_by : abap.char(12);
|
||||
decided_on : abap.utclong;
|
||||
sap_change_number : abap.char(12);
|
||||
error_message : abap.char(255);
|
||||
reject_reason : abap.char(255);
|
||||
sap_preview : abap.string(0);
|
||||
created_by : abp_creation_user;
|
||||
created_at : abp_creation_tstmpl;
|
||||
last_changed_by : abp_lastchange_user;
|
||||
last_changed_at : abp_lastchange_tstmpl;
|
||||
local_last_changed_at : abp_locinst_lastchange_tstmpl;
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
// 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).
|
||||
// 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]
|
||||
// internal operations (usable only from the behavior pool) [abap-docs /abap-docs-cloud/ABENBDL_INTERNAL]
|
||||
// field ( readonly, numbering : managed ) for a UUID key [abap-docs /abap-docs-cloud/ABENBDL_FIELD_NUMBERING; /abap-fiori-showcase/01_general_features#readonly]
|
||||
// field ( readonly : update, mandatory : create ) combination [abap-docs /abap-fiori-showcase/06_object_page_content (StreamIsReadOnly)]
|
||||
// determination on modify { create; } sets the initial status [abap-docs /abap-docs-cloud/ABENBDL_DETERMINATIONS (setStatustoNew)]
|
||||
// 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]
|
||||
// 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;
|
||||
strict ( 2 );
|
||||
|
||||
define behavior for ZZR_CHANGEAPPROVAL000 alias ChangeApproval
|
||||
persistent table zzchngpproval000
|
||||
lock master
|
||||
authorization master ( none )
|
||||
etag master LocalLastChangedAt
|
||||
{
|
||||
create;
|
||||
internal update;
|
||||
|
||||
field ( readonly, numbering : managed ) ID;
|
||||
field ( readonly : update, mandatory : create ) ItemNumber, ChangeType, ArenaEcoId;
|
||||
field ( mandatory : create ) SapPreview;
|
||||
field ( readonly ) Status, StatusText, StatusCriticality, IsApprovable, IsRetriable,
|
||||
RequestedBy, RequestedOn, DecidedBy, DecidedOn,
|
||||
SapChangeNumber, ErrorMessage, RejectReason,
|
||||
CreatedBy, CreatedAt, LastChangedBy, LastChangedAt, LocalLastChangedAt;
|
||||
|
||||
// Business-user actions (UI projection): PENDING -> APPROVED | REJECTED, FAILED -> EXECUTING (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;
|
||||
|
||||
// Worker actions (A2X projection): APPROVED -> EXECUTING -> SYNCED | FAILED (PRD RAP-6)
|
||||
action ( features : instance ) markExecuting result [1] $self;
|
||||
action ( features : instance ) markSynced parameter ZD_CHANGEAPPROVAL_SYNCEDP result [1] $self;
|
||||
action ( features : instance ) markFailed parameter ZD_CHANGEAPPROVAL_FAILEDP result [1] $self;
|
||||
|
||||
determination setInitialStatus on modify { create; }
|
||||
|
||||
mapping for zzchngpproval000
|
||||
{
|
||||
ID = id;
|
||||
ItemNumber = item_number;
|
||||
ItemName = item_name;
|
||||
ChangeType = change_type;
|
||||
ArenaEcoId = arena_eco_id;
|
||||
Status = status;
|
||||
RequestedBy = requested_by;
|
||||
RequestedOn = requested_on;
|
||||
DecidedBy = decided_by;
|
||||
DecidedOn = decided_on;
|
||||
SapChangeNumber = sap_change_number;
|
||||
ErrorMessage = error_message;
|
||||
RejectReason = reject_reason;
|
||||
SapPreview = sap_preview;
|
||||
CreatedBy = created_by;
|
||||
CreatedAt = created_at;
|
||||
LastChangedBy = last_changed_by;
|
||||
LastChangedAt = last_changed_at;
|
||||
LocalLastChangedAt = local_last_changed_at;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
/* 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.
|
||||
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'
|
||||
@MappingRole: true
|
||||
define role ZZR_CHANGEAPPROVAL000 {
|
||||
grant select on ZZR_CHANGEAPPROVAL000;
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
/* 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
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
/* 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 <alias>: 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;
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
# Predecessor flow — how the Express implementation actually works
|
||||
|
||||
Extracted 2026-09-10 by a full read of `~/projects/sap-apps/arena-plm`
|
||||
(reference-only). File:line cites refer to that repo, mostly
|
||||
`backend/src/arena-plm.js`. This record informs the rebuild's design; the
|
||||
durable behavioral tombstones harvested from it live in `KNOWLEDGE.md`, and
|
||||
the requirements it produced are in `PRD.md` (ARN-010, sharpened ARN-002/004/006).
|
||||
|
||||
**Shape of the system:** not event-driven — an **operator-driven staging
|
||||
workbench**. Arena is the read-only source, SAP is the write target *and* the
|
||||
durable ledger. The webhook and poller exist but are effectively inert (§1).
|
||||
Real syncs are triggered by a human clicking Approve in the UI5 worklist.
|
||||
|
||||
## 1. Ingestion — three paths, only one live
|
||||
|
||||
**Webhook (`POST /api/arena/webhook`, routes/arena-webhook.js)** — mounted
|
||||
before the XSUAA gate, Basic auth (timing-safe), raw-text body of any
|
||||
content-type (delivery shape never pinned down), always answers 200 so Arena
|
||||
never retries. `WEBHOOK_CAPTURE_ONLY` logs+acks; otherwise
|
||||
`pollOutboundIntegration({dryRun})` with dryRun defaulting true. **The
|
||||
consumer never syncs**: `arena-plm.js:1252-1256` is a literal TODO — non-dry
|
||||
runs only mark event items reconciled in Arena. The webhook path writes
|
||||
nothing to SAP.
|
||||
|
||||
**Outbound-integration pull queue (`arena-plm.js:1197-1263`)** — Arena holds
|
||||
the cursor: `GET /outboundintegrations/{guid}/events?itemsReconciled=false`,
|
||||
per-event `…/items`, then `PUT …/items/{itemGuid} {reconciled:true}`
|
||||
(idempotent). No local state.
|
||||
|
||||
**Change poller (`routes/poll.js` → `arena-plm.js:1117-1191`)** — manual only
|
||||
(no scheduler exists in the repo). Watermark precedence: explicit `since` →
|
||||
newest `A_ChangeMaster.ChangeNumberValidFromDate` where
|
||||
`startswith(ReasonForChangeText,'ARENA ')` → `defaults.arenaChangeCutoff`;
|
||||
refuses non-dry runs with neither (`POLL/NO_CUTOFF`). Pages the whole Arena
|
||||
`/changes` feed client-side (no working server-side date filter), filters
|
||||
`effectiveDateTime > since`. Dedup via one bulk read of every `ARENA `-marked
|
||||
change master out of SAP into a Map. Real runs create change masters only.
|
||||
|
||||
## 2. Staging & approval
|
||||
|
||||
SQLite via `node:sqlite` on the **ephemeral CF filesystem** (`db.js`):
|
||||
`approvals(id, item_number, change_type PRODUCT|CHANGE_MASTER|BOM|DOCUMENTS,
|
||||
status PENDING|APPROVED|REJECTED, arena_data, sap_preview, sap_result, error,
|
||||
reject_reason, …)` — one row per (item × change type); queue lost on restart.
|
||||
|
||||
- Seeding (`POST /api/approvals/seed`): `previewFromArena()` per item — all
|
||||
four syncs dry-run under `Promise.allSettled` (concurrency 6). **No dedup**
|
||||
on (item, changeType): re-seeding duplicates rows.
|
||||
- Approve: `bulk-approve` returns immediately; work runs in a detached async
|
||||
IIFE guarded by a **module-level in-memory `postJob` singleton** (progress
|
||||
source AND concurrency guard; forces `instances: 1`; on restart, in-flight
|
||||
jobs leave rows PENDING forever). `chain-approve` is synchronous per item,
|
||||
halts on first failure. Reject is a pure DB flip.
|
||||
- **On approval only `item_number` is passed** — the stored `sap_preview` is
|
||||
never replayed; every sync re-fetches Arena and recomputes. What was
|
||||
approved is not necessarily what gets written (→ rebuild ARN-010).
|
||||
- **`REJECTED` is overloaded**: human rejection AND SAP write failure land in
|
||||
the same status, distinguishable only by `error` vs `reject_reason`; no
|
||||
retry path from REJECTED except re-seeding (→ rebuild ARN-002 states).
|
||||
|
||||
## 3. Sync execution — the SAP write chain
|
||||
|
||||
Order from `config/sync-chain-order.json`: PRODUCT → CHANGE_MASTER → BOM →
|
||||
DOCUMENTS (CM's material object record needs the material; BOM's
|
||||
`EngineeringChangeDocForEdit` needs the CM). Services as in PRD interface
|
||||
contracts (V4 product, V2 product fallback, BOM write `;v=0002` / read v1,
|
||||
change master `;v=0002`, CV attachment).
|
||||
|
||||
**PRODUCT (`:1336-1663`)** — exists-check (V4 GET, 404 ⇒ create path).
|
||||
Create: `ProductType` strictly from Arena attr "SAP Product Type" (missing ⇒
|
||||
`422 SYNC/NO_PRODUCT_TYPE` — deliberately "not a material"); UOM via config +
|
||||
hardcoded ISO map; plants from attr "SAP Plant" through `plantMapping`; weight
|
||||
g→×1000, dims mm→×1000; description truncated 40. Deep insert with
|
||||
`_ProductDescription`, `_ProductPlant` only when no `AvailabilityCheckType`;
|
||||
else per-plant V2 `A_ProductPlant` POSTs; `ProcurementType` requires `MRPType`
|
||||
in the same `ProductPlantSupplyPlanning` PATCH. Best-effort CM link keyed on
|
||||
`startswith(ChangeNumberDescription,'ECO:')` — a different key than everything
|
||||
else (landmine). Update path: `ProductType`/`BaseUnit` immutable;
|
||||
`ProductDescription` PATCH by full key (nav PATCH → 501), POST fallback on
|
||||
404/405.
|
||||
|
||||
**CHANGE_MASTER (`:1794-1873`)** — EFFECTIVE revision with `change.number`
|
||||
via `/revisions` (`/items/{guid}/changes` 404s — does not exist).
|
||||
Double-condition dedup: `ReasonForChangeText eq 'ARENA <eco>'` AND a material
|
||||
object record for *this* item. Create: `A_ChangeMaster` with
|
||||
`ChangeNumberDescription: "ECO: title"[0..40]`, `/Date(ms)/` valid-from,
|
||||
`ReasonForChangeText: "ARENA <eco>"`; then object-type assignments Material +
|
||||
MaterialBOM (`IsActiveForChangeNumber: true` mandatory — else 201 +
|
||||
silently discarded), then the material object record. Errors returned, not
|
||||
thrown.
|
||||
|
||||
**BOM (`:1665-1792`)** — first mapped plant wins; lines `quantity > 0`, item
|
||||
numbers (i+1)*10 zero-padded 4; category via `bomlineByCategory` default 'L'.
|
||||
Opportunistic ECM lookup (bare catch — silently drops ECM and creates the BOM
|
||||
plain if the object-type assignment fails: traceability quietly lost).
|
||||
Component pre-check: if all components missing in SAP → early return, no
|
||||
orphan header (SAP's real error is an opaque consistency-check message).
|
||||
Header POST with `EngineeringChangeDocForEdit` (never
|
||||
`EngineeringChangeDocument` — creatable=false, silently dropped), validity end
|
||||
`/Date(253402214400000)/`. Items POSTed serially; each item re-reads the
|
||||
header on the v1 read service; ECM BOMs must carry `HeaderChangeDocument`.
|
||||
`BOMItemIsSalesRelevant:'X'` hardcoded. Partial failure is a return value.
|
||||
**Not idempotent — re-approval creates a second BOM header.**
|
||||
|
||||
**DOCUMENTS (`:1875-1913`)** — URL links only, no bytes: filters files to
|
||||
`storageMethodName === 'WEB' && file.location` (blob-stored files silently
|
||||
skipped), then V2 function import `CreateUrlAsAttachment` with hardcoded
|
||||
`BusinessObjectTypeName BUS1001006`, `MIMEType 'text/url'`, description
|
||||
"<item> <category> Rev <edition>"[0..40]. The editable
|
||||
`arena-doc-category-to-sap-doctype.json` config is **ignored by code**.
|
||||
**Not idempotent — re-approval duplicates attachments.**
|
||||
|
||||
**Cross-cutting:** CSRF token+cookie cached per service, invalidated only on
|
||||
403 with exactly one retry; PRODUCT services reject bare-root token fetch
|
||||
(seed from `Product?$top=1` / `A_Product?$top=1`). Arena 401 → re-login, one
|
||||
retry. No backoff, no retry queue, no transactions. Idempotency exists only
|
||||
for CHANGE_MASTER (double-check) and weakly for PRODUCT (exists→PATCH).
|
||||
|
||||
## 4. Arena reads
|
||||
|
||||
Session `POST /login {email, password, workspaceId}` → `arena_session_id`
|
||||
header, cached 24 min vs 25-min expiry, single-flight promise lock, one 401
|
||||
retry. Endpoints used by sync: `/items?number=` (resolves EFFECTIVE),
|
||||
`/items/{guid}`, `/items/{guid}/revisions`, `/items/{guid}/bom`,
|
||||
`/items/{guid}/files`, `/changes/{guid}`, `/changes?limit&offset`,
|
||||
`/changes/{guid}/items`, `/settings/items/categories`, outbound-integration
|
||||
trio. Write-back (`POST /changes/statuschanges`) is parked behind
|
||||
`ARENA_WRITEBACK_ENABLED`.
|
||||
|
||||
**Load-bearing subtlety — WORKING-revision overlay (`:508-552`):** the seven
|
||||
SAP classification attributes (SAP Object Type / Product Type / Product
|
||||
Category / Sub-Category / Product Family / Business Unit / Plant) are only
|
||||
writable on the WORKING revision (Arena error 3224), while `/items?number=`
|
||||
returns the EFFECTIVE revision — so those seven are merged from WORKING over
|
||||
EFFECTIVE. The overlay is best-effort: a transient Arena failure silently
|
||||
loses "SAP Product Type" and turns a valid sync into `NO_PRODUCT_TYPE`.
|
||||
|
||||
## 5. Config actually read vs dead
|
||||
|
||||
Live (read by sync code, hot-reloaded, git-backed via external
|
||||
`sap-apps-config` repo): `defaults.json`, `plant-mapping.json`,
|
||||
`uom-arena-to-sap.json`, `material-group-by-product-type.json`,
|
||||
`availability-check-by-product-type.json`, `lifecycle-to-xplant-status.json`,
|
||||
`bomline-category-by-arena-category.json`, `sync-chain-order.json`.
|
||||
|
||||
Dead (loaded, zero read sites — confirms the KNOWLEDGE.md landmine):
|
||||
`arena-category-to-sap.json`, `procurement-type.json` (code uses a hardcoded
|
||||
map keyed on the first char of Arena "Item Type"),
|
||||
`catalog-to-planning-strategy.json`. Ignored-but-editable:
|
||||
`arena-doc-category-to-sap-doctype.json`. Hardcoded despite the "no
|
||||
hardcoding" comment: ISO UOM map, status map, `ARENA ` marker, procurement
|
||||
map, service paths, working-attrs list, BOM validity end, sales-relevant
|
||||
flag, Language 'EN', 40-char truncations, BUS1001006.
|
||||
|
||||
## 6. Fragilities the rebuild must not inherit
|
||||
|
||||
1. In-memory `postJob` singleton = progress + concurrency guard; restart
|
||||
orphans PENDING rows; forces `instances: 1`.
|
||||
2. Fire-and-forget approval worker; no resume, no cancellation.
|
||||
3. Seed dedup absent; BOM and DOCUMENTS syncs non-idempotent.
|
||||
4. `REJECTED` overload (human vs SAP failure).
|
||||
5. Silent ECM degradation (bare catches around ECO lookup, object-type
|
||||
assignment, revision overlay).
|
||||
6. Two disagreeing CM lookup keys (`ChangeNumberDescription` startswith vs
|
||||
`ReasonForChangeText` eq; 40-char truncation can split them).
|
||||
7. The `ARENA <eco>` free-text marker is the entire ledger — dedup key,
|
||||
watermark, and status all hang on an editable 40-char field.
|
||||
8. O(history) paging on every poll (Arena full feed; up to 100k SAP change
|
||||
masters).
|
||||
9. Arena credentials base64 in SQLite on the ephemeral filesystem; the whole
|
||||
approvals queue dies on restart/redeploy.
|
||||
10. Approved preview is decorative — execution re-fetches and recomputes.
|
||||
Reference in New Issue
Block a user