Managed RAP Part 3: Determinations, Validations, Actions and Feature Control
Give the Purchase Request real business behavior: default and calculated values with determinations, error checks with validations, a Submit/Approve/Reject workflow with actions, and state-based feature control.
Explanation
📌 KEY POINTS
✅ Determination = automatic value; validation = blocks invalid save; action = explicit step.
✅ Determinations must be idempotent; validations only read and report.
✅ Use IN LOCAL MODE inside the BO to change read-only fields.
⚠️ State rules ("only when Submitted") belong in instance feature control.
▶ WHERE WE ARE
After Part 2 the Purchase Request has numbers, locks, ETags and authorization. It still has no business rules: a request can be saved without a description, items can have zero quantity, and there is no approval workflow. This part adds the four behavior elements that carry business logic.
━━━━━━━━━━━━━━━━━━━━
▶ THE FOUR TOOLS AND WHEN TO USE EACH
- Determination — the system calculates or defaults a value automatically. Nobody triggers it explicitly. Examples: set status to New on create; recalculate the total when an item changes.
- Validation — the system checks a rule and rejects the save if it is broken. Examples: description is mandatory; quantity must be greater than zero.
- Action — the user or an API explicitly triggers a business step. Examples: Submit, Approve, Reject.
- Feature control — decides whether an operation or action is currently allowed based on the instance's state. Examples: Approve is only enabled when status is Submitted; an approved request cannot be edited.
Rule of thumb: if the user should press a button, it is an action; if it should just happen, it is a determination; if it should stop the save, it is a validation; if it should grey out a button, it is feature control.
━━━━━━━━━━━━━━━━━━━━
▶ STATUS MODEL FOR OUR EXAMPLE
N = New → (Submit) → S = Submitted → (Approve) → A = Approved, or (Reject) → R = Rejected.
━━━━━━━━━━━━━━━━━━━━
▶ DETERMINATIONS: ON MODIFY VS ON SAVE
A determination declares when it runs (the trigger) and on what (trigger operations and fields):
- "on modify { create; }" — runs immediately in the interaction phase when an instance is created. The user sees the result at once (with draft, even before saving). setInitialStatus uses this.
- "on modify { create; field Quantity, Price; }" — runs when an item is created or when Quantity or Price change. calculateTotal uses this, on the Item entity, and writes the total to the parent.
- "on save { create; }" — runs once, during the save sequence. setRequestID from Part 2 uses this.
Determinations must be idempotent: running twice must produce the same result. That is why setInitialStatus only sets the status when it is still initial.
━━━━━━━━━━━━━━━━━━━━
▶ VALIDATIONS
Validations always run on save and declare which operations and fields trigger them: "validation validateDescription on save { create; field Description; }". A validation reads the data, and for each broken instance it appends the key to FAILED (this stops the save) and a message to REPORTED (this tells the user why). Set %element-<field> in the message so Fiori highlights the field.
The %state_area pattern (clear first, then add the message with the same state area) matters once draft is enabled: it removes the old message when the user fixes the problem.
ACTIONS
"action ( features : instance ) approve result [1] $self;" declares an instance action that returns the updated instance. The implementation changes data through MODIFY ENTITIES IN LOCAL MODE and returns the instance in result so the UI can refresh.
An action may also refuse: submit checks that the request has at least one item; if not, it returns FAILED and a message and nothing changes.
Other action types to know: static actions (no instance, e.g. "copy from template"), factory actions (create a new instance, e.g. "copy request"), and actions with a parameter (declared with "parameter" and an abstract entity, e.g. a rejection reason).
━━━━━━━━━━━━━━━━━━━━
▶ FEATURE CONTROL
"update ( features : instance )" and "action ( features : instance ) approve" make RAP call get_instance_features before the operation and whenever the UI asks which buttons to enable. The method returns enabled or disabled per instance and per operation. Result: in Fiori the Approve button is greyed out unless the request is Submitted, and the Edit button disappears once it is Approved. If an API caller tries anyway, RAP rejects the call.
Static field control is declared directly in the BDEF: "field ( mandatory ) Description, CurrencyCode;" and "field ( readonly ) Status, TotalAmount;".
━━━━━━━━━━━━━━━━━━━━
▶ STEP BY STEP
- Change "update;" and "delete;" on the root to "update ( features : instance ); delete ( features : instance );".
- Add the determination, validation and action lines from the code example to the root and to the item.
- Place the cursor on each new name and use Ctrl+1 to generate the method stubs in ZBP_R_PURCHREQ.
- Implement the methods as shown. Always READ first, process the set, then one MODIFY.
- Test with EML: create a request without description and COMMIT ENTITIES — you get FAILED and a message. Create one with an item, call EXECUTE submit, then approve, and read the status.
━━━━━━━━━━━━━━━━━━━━
▶ WHAT YOU HAVE AFTER PART 3
A business object with a real workflow and rules the UI will respect automatically. Part 4 makes the header-item relationship first-class with create-by-association and adds draft, so users can edit safely over a longer session.
Code example
// ===== BDEF: root entity PurchaseRequest (changes) ===== create; update ( features : instance ); delete ( features : instance ); association _Item { create; } field ( mandatory ) Description, CurrencyCode; determination setInitialStatus on modify { create; } determination setRequestID on save { create; } validation validateDescription on save { create; field Description; } action ( features : instance ) submit result [1] $self; action ( features : instance ) approve result [1] $self; action ( features : instance ) reject result [1] $self; // ===== BDEF: child entity Item (additions) ===== field ( mandatory ) Quantity, Unit; determination calculateTotal on modify { create; field Quantity, Price; } validation validateQuantity on save { create; field Quantity; } " ===== Handler for PurchaseRequest (add to lhc_PurchaseRequest) ===== METHOD setInitialStatus. READ ENTITIES OF zr_purchreq IN LOCAL MODE ENTITY PurchaseRequest FIELDS ( Status ) WITH CORRESPONDING #( keys ) RESULT DATA(requests). DELETE requests WHERE Status IS NOT INITIAL. " idempotent CHECK requests IS NOT INITIAL. MODIFY ENTITIES OF zr_purchreq IN LOCAL MODE ENTITY PurchaseRequest UPDATE FIELDS ( Status Requester ) WITH VALUE #( FOR r IN requests ( %tky = r-%tky Status = 'N' Requester = cl_abap_context_info=>get_user_technical_name( ) ) ). ENDMETHOD. METHOD validateDescription. READ ENTITIES OF zr_purchreq IN LOCAL MODE ENTITY PurchaseRequest FIELDS ( Description ) WITH CORRESPONDING #( keys ) RESULT DATA(requests). LOOP AT requests INTO DATA(request). " clear old message for this check (important with draft) APPEND VALUE #( %tky = request-%tky %state_area = 'VALIDATE_DESCR' ) TO reported-purchaserequest. IF request-Description IS INITIAL. APPEND VALUE #( %tky = request-%tky ) TO failed-purchaserequest. APPEND VALUE #( %tky = request-%tky %state_area = 'VALIDATE_DESCR' %msg = new_message_with_text( severity = if_abap_behv_message=>severity-error text = 'Enter a description for the request' ) %element-Description = if_abap_behv=>mk-on ) TO reported-purchaserequest. ENDIF. ENDLOOP. ENDMETHOD. METHOD submit. " A request without items may not be submitted READ ENTITIES OF zr_purchreq IN LOCAL MODE ENTITY PurchaseRequest BY \_Item FIELDS ( RequestUUID ) WITH CORRESPONDING #( keys ) RESULT DATA(items). DATA ok_keys LIKE keys. LOOP AT keys INTO DATA(key). IF line_exists( items[ RequestUUID = key-RequestUUID ] ). APPEND key TO ok_keys. ELSE. APPEND VALUE #( %tky = key-%tky ) TO failed-purchaserequest. APPEND VALUE #( %tky = key-%tky %msg = new_message_with_text( severity = if_abap_behv_message=>severity-error text = 'Add at least one item before submitting' ) ) TO reported-purchaserequest. ENDIF. ENDLOOP. CHECK ok_keys IS NOT INITIAL. MODIFY ENTITIES OF zr_purchreq IN LOCAL MODE ENTITY PurchaseRequest UPDATE FIELDS ( Status ) WITH VALUE #( FOR k IN ok_keys ( %tky = k-%tky Status = 'S' ) ). READ ENTITIES OF zr_purchreq IN LOCAL MODE ENTITY PurchaseRequest ALL FIELDS WITH CORRESPONDING #( ok_keys ) RESULT DATA(result_requests). result = VALUE #( FOR r IN result_requests ( %tky = r-%tky %param = r ) ). ENDMETHOD. METHOD approve. " reject is identical with Status = 'R' MODIFY ENTITIES OF zr_purchreq IN LOCAL MODE ENTITY PurchaseRequest UPDATE FIELDS ( Status ) WITH VALUE #( FOR key IN keys ( %tky = key-%tky Status = 'A' ) ). READ ENTITIES OF zr_purchreq IN LOCAL MODE ENTITY PurchaseRequest ALL FIELDS WITH CORRESPONDING #( keys ) RESULT DATA(requests). result = VALUE #( FOR r IN requests ( %tky = r-%tky %param = r ) ). ENDMETHOD. METHOD get_instance_features. READ ENTITIES OF zr_purchreq IN LOCAL MODE ENTITY PurchaseRequest FIELDS ( Status ) WITH CORRESPONDING #( keys ) RESULT DATA(requests). result = VALUE #( FOR r IN requests ( %tky = r-%tky %update = COND #( WHEN r-Status = 'N' THEN if_abap_behv=>fc-o-enabled ELSE if_abap_behv=>fc-o-disabled ) %delete = COND #( WHEN r-Status = 'N' THEN if_abap_behv=>fc-o-enabled ELSE if_abap_behv=>fc-o-disabled ) %action-submit = COND #( WHEN r-Status = 'N' THEN if_abap_behv=>fc-o-enabled ELSE if_abap_behv=>fc-o-disabled ) %action-approve = COND #( WHEN r-Status = 'S' THEN if_abap_behv=>fc-o-enabled ELSE if_abap_behv=>fc-o-disabled ) %action-reject = COND #( WHEN r-Status = 'S' THEN if_abap_behv=>fc-o-enabled ELSE if_abap_behv=>fc-o-disabled ) ) ). ENDMETHOD. " ===== Handler for Item (local class lhc_Item) ===== METHOD calculateTotal. " 1. Which requests are affected? READ ENTITIES OF zr_purchreq IN LOCAL MODE ENTITY Item BY \_Request FIELDS ( RequestUUID ) WITH CORRESPONDING #( keys ) RESULT DATA(requests). " 2. Read ALL items of those requests, not only the changed ones READ ENTITIES OF zr_purchreq IN LOCAL MODE ENTITY PurchaseRequest BY \_Item FIELDS ( Quantity Price ) WITH CORRESPONDING #( requests ) RESULT DATA(items). " 3. Sum per request and write the total to the parent MODIFY ENTITIES OF zr_purchreq IN LOCAL MODE ENTITY PurchaseRequest UPDATE FIELDS ( TotalAmount ) WITH VALUE #( FOR r IN requests ( %tky = r-%tky TotalAmount = REDUCE #( INIT sum = CONV zpr_head-total_amount( 0 ) FOR i IN items WHERE ( RequestUUID = r-RequestUUID ) NEXT sum = sum + i-Quantity * i-Price ) ) ). ENDMETHOD. " Note: as declared, deleting an item does not re-run this determination. " Recalculate on the parent at save time or via an action if deletions matter. " ===== Test with EML =====MODIFY ENTITIES OF zr_purchreq ENTITY PurchaseRequest CREATE FIELDS ( Description CurrencyCode ) WITH VALUE #( ( %cid = 'R1' Description = 'Monitors' CurrencyCode = 'INR' ) ) CREATE BY \_Item FIELDS ( ItemNo ItemText Quantity Unit Price CurrencyCode ) WITH VALUE #( ( %cid_ref = 'R1' %target = VALUE #( ( %cid = 'I1' ItemNo = '0010' ItemText = '27 inch monitor' Quantity = 4 Unit = 'ST' Price = '18000.00' CurrencyCode = 'INR' ) ) ) ) ENTITY PurchaseRequest EXECUTE submit FROM VALUE #( ( %cid_ref = 'R1' ) ) MAPPED DATA(mapped) FAILED DATA(failed) REPORTED DATA(reported).COMMIT ENTITIES.Real project scenario
A procurement team first put every rule inside one action called "save_and_check". It set defaults, recalculated totals, checked mandatory fields and changed status. The action worked in the UI, but the same request created through the API skipped all of it, because nobody called the action.
The fix was to split the logic by purpose. Defaults and totals became determinations, so they run no matter which channel creates the data. Mandatory checks became validations, so no channel can save invalid data. Only the explicit business steps (Submit, Approve, Reject) remained actions. The "you cannot approve your own request" check went into get_instance_authorizations, and "approved requests are read-only" went into feature control.
The UI and API then behaved identically, and support tickets about "API-created requests without totals" stopped.
Common mistakes
- Putting derivations or checks into an action — anything not triggered explicitly will then skip them. Use determinations and validations.
- Writing non-idempotent determinations (e.g. adding 1 each time) — they can run more than once.
- Reading only the changed items when calculating a total; you must read all items of the parent.
- Appending to FAILED without a REPORTED message: the save fails and the user does not know why.
- Forgetting %element in messages, so Fiori cannot highlight the field.
- Looping over keys and calling MODIFY ENTITIES once per key instead of once for the set.
- Using feature control for "the user has no role" (that is authorization) or authorization for "the status is wrong" (that is feature control).
- Using COMMIT ENTITIES or messages with MESSAGE ... TYPE 'E' inside handler methods.
Best practices
- Choose the tool by trigger: button = action, automatic = determination, block save = validation, grey out = feature control.
- Keep determination triggers narrow (specific fields), so they do not run on every change.
- Pattern for every method: one READ ENTITIES for all keys, process the table, one MODIFY ENTITIES.
- Use a message class (new_message with id/number) in production instead of hard-coded text, so messages are translatable.
- Use %state_area in validations so draft users see messages disappear once fixed.
- Return the changed instance from actions (result [1] $self) so the UI refreshes without a reload.
Interview angle
Classic questions: "Determination vs validation vs action?", "When does a determination on modify run vs on save?", "What is feature control and how is it different from authorization?", "What are FAILED, REPORTED and MAPPED?"
Strong answer: determinations derive values automatically (on modify in the interaction phase, on save during the save sequence), validations run on save and block it via FAILED plus a REPORTED message, and actions are explicit business operations. Instance feature control enables or disables operations based on data state; authorization decides based on the user. FAILED lists instances that failed, REPORTED carries messages, MAPPED maps %cid to the final keys. Mention bulk processing and idempotency — interviewers probe whether you know handlers receive sets of keys and that determinations can run repeatedly.
Practice this lesson
Quick check
1. The Approve button must be enabled only when a request has status Submitted. Which RAP element implements this?
2. When do RAP validations run in a managed business object?
3. In managed RAP, where are create and update changes held during the interaction phase, before the user saves?
Interview questions
1. Classify each requirement as a determination, validation, action, feature control or authorization: (1) status is New when a request is created, (2) description is mandatory, (3) manager clicks Approve, (4) Approve is only possible when status is Submitted, (5) only buyers may create requests. Explain why.
Answer it in your own words first, then compare.
2. Implement a Submit/Approve/Reject workflow in a managed RAP object. What do you declare in the BDEF, and how does the approve action implementation look in principle?
Answer it in your own words first, then compare.
Reference-verified: the steps are checked against official SAP documentation and tutorials, and object and field names in the sample code are checked for consistency. The code has not been compiled or run on an SAP system by ERPClimb. Syntax marked release-dependent varies by SAP release, so check it in your own system.