Skip to content

Commit 3bf94e3

Browse files
Merge branch 'cap.cloud.sap' into main
2 parents c19b5a9 + 47f0e94 commit 3bf94e3

File tree

2 files changed

+133
-0
lines changed

2 files changed

+133
-0
lines changed

guides/flows.md

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
---
2+
synopsis: >
3+
Learn how to define and manage status-transition flows in your CDS models using annotations, without writing custom handlers.
4+
#status: released
5+
---
6+
7+
# Status-Transition Flows <Beta />
8+
9+
[...]
10+
11+
## Extending Flows
12+
13+
[...]
14+
15+
### Example Use Case
16+
17+
Consider a requirement where customers can withdraw from travel — for example, due to sickness — but only up to 24 hours before travel begins. This requires custom validation logic.
18+
19+
The status transition diagram below shows the new state and transitions:
20+
![](./assets/flows/xtravels-flow-extend.svg)
21+
22+
First, add the `Withdrawn` status and the `withdrawTravel` action to the model:
23+
24+
```cds
25+
// db/schema.cds
26+
entity TravelStatus : sap.common.CodeList {
27+
key code : String(1) enum {
28+
Open = 'O';
29+
Accepted = 'A';
30+
Canceled = 'X';
31+
Withdrawn = 'W'; // [!code highlight]
32+
}
33+
}
34+
35+
// srv/travel-service.cds
36+
service TravelService {
37+
38+
// Define entity and actions
39+
entity Travels as projection on db.Travels
40+
actions {
41+
action rejectTravel();
42+
action acceptTravel();
43+
action withdrawTravel(); // [!code highlight]
44+
action deductDiscount( percent: Percentage not null ) returns Travels;
45+
};
46+
47+
// Define flow through actions
48+
annotate Travels with @flow.status: Status actions {
49+
rejectTravel @from: #Open @to: #Canceled;
50+
acceptTravel @from: #Open @to: #Accepted;
51+
withdrawTravel @from: [#Open, #Accepted]; // [!code highlight]
52+
deductDiscount @from: #Open;
53+
};
54+
55+
}
56+
```
57+
58+
Note that `withdrawTravel` has no `@to` annotation; you implement the transition in a custom handler.
59+
60+
61+
### In Java
62+
63+
Here is a custom Java implementation that enforces the 24-hour rule:
64+
65+
```java
66+
@Component
67+
@ServiceName(TravelService_.CDS_NAME)
68+
public class WithdrawTravelHandler implements EventHandler {
69+
70+
private final PersistenceService persistenceService;
71+
72+
public WithdrawTravelHandler(PersistenceService persistenceService) {
73+
this.persistenceService = persistenceService;
74+
}
75+
76+
@Before(entity = Travel_.CDS_NAME)
77+
public void check24HoursBeforeTravel(final TravelWithdrawTravelContext context, CqnStructuredTypeRef travelRef) {
78+
Travel travel = ((ApplicationService) context.getService()).run(
79+
Select.from(travelRef).columns(Travel_.BEGIN_DATE)).first(Travel.class)
80+
.orElseThrow(() -> new ServiceException(ErrorStatuses.BAD_REQUEST, "TRAVEL_NOT_FOUND"));
81+
82+
if (travel.beginDate().isBefore(LocalDate.now().minusDays(1))) {
83+
context.getMessages().error("Travel can only be withdrawn up to 24 hours before travel begins.");
84+
}
85+
}
86+
87+
@On(entity = Travel_.CDS_NAME)
88+
public void onWithdrawTravel(final TravelWithdrawTravelContext context, CqnStructuredTypeRef travelRef) {
89+
boolean isDraftTarget =DraftUtils.isDraftTarget(
90+
travelRef,
91+
context.getModel().findEntity(travelRef.targetSegment().id()).get(),
92+
context.getModel());
93+
boolean isDraftEnabled = DraftUtils.isDraftEnabled(context.getTarget());
94+
var travel = Travel.create();
95+
travel.travelStatusCode(TravelStatusCode.WITHDRAWN);
96+
if (isDraftTarget) {
97+
((DraftService) context.getService()).patchDraft(Update.entity(travelRef).data(travel));
98+
} else {
99+
AnalysisResult analysis = CqnAnalyzer.create(context.getModel()).analyze(travelRef);
100+
Map<String, Object> keys = analysis.targetKeyValues();
101+
if (isDraftEnabled) {
102+
keys.remove(Drafts.IS_ACTIVE_ENTITY);
103+
}
104+
persistenceService.run(Update.entity(context.getTarget()).matching(keys).data(travel));
105+
}
106+
context.setCompleted();
107+
}
108+
109+
}
110+
```
111+
112+
The custom `before` handler reads the travel's `BeginDate` and validates that withdrawal occurs within the allowed timeframe. The custom `on` handler updates the travel status to `Withdrawn` and marks the action as completed.
113+
114+
<!--
115+
The custom `On` handler implements the transition by updating the travel status to `Withdrawn`.
116+
It checks whether the entity is a draft or a non-draft entity and applies the appropriate update logic.
117+
For draft entities, it uses the `patchDraft` method to update the draft data, while for non-draft entities, it uses the `PersistenceService` to persist the changes.
118+
Finally, it marks the action as completed.
119+
-->
120+
121+
The custom `on` handler updates the travel status to `Withdrawn` and marks the action as completed.
122+
123+
::: warning TODO: we should actually do the following!
124+
-> `withdrawTravel` should only have an additional before check.
125+
:::
126+
127+
While you could use the `@to` annotation with the default handler, omitting it signals that you implemented custom transition logic.
128+
129+
130+
### In Node.js
131+
132+
TODO

menu.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
### [Status Flows](guides/services/status-flows)
3030
### [Custom Logic](guides/providing-services#custom-logic)
3131
### [Actions & Functions](guides/providing-services#actions-functions)
32+
### [Status-Transition Flows](guides/providing-services#status-transition-flows)
3233
### [Serving Media Data](guides/providing-services#serving-media-data)
3334
### [Best Practices](guides/providing-services#best-practices)
3435

0 commit comments

Comments
 (0)