From 71e9c1149db8a4e1c56fdfe62237a3538d73dffc Mon Sep 17 00:00:00 2001 From: I543501 Date: Mon, 13 Jul 2026 14:00:18 +0200 Subject: [PATCH 1/9] recap init --- app/_i18n/i18n.properties | 2 ++ app/_i18n/i18n_de.properties | 1 + app/_i18n/i18n_en.properties | 1 + app/_i18n/i18n_fr.properties | 1 + app/travels/layouts.cds | 14 ++++++++++++++ db/schema.cds | 16 ++++++++-------- package.json | 12 +++++++++++- srv/data-federation.js | 5 ++++- srv/travel-service.cds | 2 +- srv/travel-service.js | 11 ++++++++++- test/saga.http | 30 +++++++++++++++++++++++++++--- 11 files changed, 80 insertions(+), 15 deletions(-) diff --git a/app/_i18n/i18n.properties b/app/_i18n/i18n.properties index a2c38a4..51ad8d1 100644 --- a/app/_i18n/i18n.properties +++ b/app/_i18n/i18n.properties @@ -7,6 +7,7 @@ ArrivalTime=Arrival Time BeginDate=Starting Date Booking=Booking BookingDate=Booking Date +Status.code=Status BookingFee=Booking Fee BookingID=Booking ID BookingSupplement=Booking Supplement @@ -50,4 +51,5 @@ Travel=Travel TravelAgency=Travel Agency Travels=Travels TravelStatus=Travel Status +Status=Booking Status WebAddress=Web Address diff --git a/app/_i18n/i18n_de.properties b/app/_i18n/i18n_de.properties index 15448f4..94fb764 100644 --- a/app/_i18n/i18n_de.properties +++ b/app/_i18n/i18n_de.properties @@ -50,4 +50,5 @@ Travel=Reise TravelAgency=Veranstalter Travels=Reisen TravelStatus=Reisestatus +Status=Buchungsstatus WebAddress=Web-Adresse diff --git a/app/_i18n/i18n_en.properties b/app/_i18n/i18n_en.properties index a2c38a4..be7a0f4 100644 --- a/app/_i18n/i18n_en.properties +++ b/app/_i18n/i18n_en.properties @@ -50,4 +50,5 @@ Travel=Travel TravelAgency=Travel Agency Travels=Travels TravelStatus=Travel Status +Status=Booking Status WebAddress=Web Address diff --git a/app/_i18n/i18n_fr.properties b/app/_i18n/i18n_fr.properties index 35086d9..d036d60 100644 --- a/app/_i18n/i18n_fr.properties +++ b/app/_i18n/i18n_fr.properties @@ -50,4 +50,5 @@ Travel=Voyage TravelAgency=Agence de voyage Travels=Voyages TravelStatus=Statut du voyage +Status=Statut de la réservation WebAddress=Adresse e-mail diff --git a/app/travels/layouts.cds b/app/travels/layouts.cds index ea8c232..d2ceb66 100644 --- a/app/travels/layouts.cds +++ b/app/travels/layouts.cds @@ -112,6 +112,13 @@ annotate TravelService.Bookings with @UI : { { Value : Flight.destination, Label : '{i18n>Destination}', @HTML5.CssDefaults: {width:'19em'}, @Common.FieldControl: #ReadOnly }, { Value : Flight.airline, Label : '{i18n>Airline}', @HTML5.CssDefaults: {width:'11em'}, @Common.FieldControl: #ReadOnly }, { Value : BookingDate, Label : '{i18n>BookingDate}' }, + { Value : Status.code, Label : '{i18n>Status}', @UI.Importance : #High, @HTML5.CssDefaults: {width:'10em'}, + Criticality : ( + Status.code == #Confirmed ? 3 : + Status.code == #Failed ? 1 : + 0 + ), + }, ], Facets : [{ @@ -140,6 +147,13 @@ annotate TravelService.Bookings with @UI : { { Value : Pos }, { Value : BookingDate }, { Value : Travel.Customer.ID }, + { Value : Status.code, Label : '{i18n>Status}', + Criticality : ( + Status.code == #Confirmed ? 3 : + Status.code == #Failed ? 1 : + 0 + ), + }, ]}, FieldGroup #Flight: {Data: [ diff --git a/db/schema.cds b/db/schema.cds index d6ff804..e9dbf7d 100644 --- a/db/schema.cds +++ b/db/schema.cds @@ -36,6 +36,14 @@ entity Bookings { Status : Association to BookingStatus default 'O'; } +entity BookingStatus : sap.common.CodeList { + key code : String(1) enum { + Open = 'O'; + Confirmed = 'C'; + Failed = 'F'; + } +} + entity TravelAgencies { key ID : String(6); @@ -60,14 +68,6 @@ entity TravelStatus : sap.common.CodeList { } } -entity BookingStatus : sap.common.CodeList { - key code : String(1) enum { - Open = 'O'; - Confirmed = 'C'; - Failed = 'F'; - } -} - type Price : Decimal(9,4); diff --git a/package.json b/package.json index 84f9b82..387e221 100644 --- a/package.json +++ b/package.json @@ -3,8 +3,9 @@ "version": "1.0.1", "description": "CAP XTravels sample application", "scripts": { + "start": "cds watch --port 4004", + "reset": "cds deploy && cds watch --port 4004", "lint": "npx eslint .", - "start": "cds-serve", "test": "cds test" }, "dependencies": { @@ -21,7 +22,16 @@ "requires": { "messaging": { "kind": "file-based-messaging", + "file": "../msg-box", "inboxed": true + }, + "queue": { + "maxAttempts": 6 + } + }, + "log": { + "levels": { + "odata": "error" } } }, diff --git a/srv/data-federation.js b/srv/data-federation.js index 949a97f..8bf479a 100644 --- a/srv/data-federation.js +++ b/srv/data-federation.js @@ -18,7 +18,10 @@ cds.on ('loaded', csn => { cds.once ('served', () => Promise.all (feed.map (async each => { const srv = await cds.connect.to (each.remote) srv._once ??=!! srv.on ('replicate', replicate) - await srv.schedule ('replicate', each) .as (`replicate ${each.entity}`) .every ('10 minutes') + await srv + .schedule ('replicate', each) + .as (`replicate ${each.entity}`) + .every ('10 minutes') }))) // Event handler for replicating single entities diff --git a/srv/travel-service.cds b/srv/travel-service.cds index 01ec5e4..e2bc514 100644 --- a/srv/travel-service.cds +++ b/srv/travel-service.cds @@ -17,7 +17,7 @@ using { sap.capire.s4 } from '../apis/capire/s4'; @readonly entity Customers as projection on s4.Customers; @readonly entity Flights as projection on x.Flights; @readonly entity Supplements as projection on x.Supplements; - + entity Outbox as projection on cds.outbox.Messages; } // Custom type for percentage values diff --git a/srv/travel-service.js b/srv/travel-service.js index 4fbdeb1..b24b243 100644 --- a/srv/travel-service.js +++ b/srv/travel-service.js @@ -26,12 +26,17 @@ class TravelService extends cds.ApplicationService { const { Flights, Supplements, Travels, Customers } = this.entities const { Bookings } = cds.entities ('sap.capire.travels') + this.on('READ', 'Outbox', () => cds.db.run(SELECT.from('cds.outbox.Messages'))) + // Delegate value helpp requests on Customers to S4 Business Partner service this.on ('READ', Customers, req => s4.run (req.query)) // REVISIT: generic crud does not handle @cds.persistence.table and hence rejects due to @cds.persistence.skip this.on('READ', [Flights, Supplements], async function(req) { - if (req.target['@cds.persistence.table']) return cds.db.run(req.query) + if (req.target['@cds.persistence.table']) { + console.log (`Select from local replica for ${req.query._target.name}`) + return cds.db.run(req.query) + } return xflights.run(req.query) }) @@ -41,17 +46,20 @@ class TravelService extends cds.ApplicationService { return Promise.all (Bookings.map (booking => { let { Flight_ID: flight, Flight_date: date, Travel_ID, Pos } = booking // REVISIT: transport Travel_ID, Pos to callback via headers -> we need a backpack + console.log (`[${1}] Emit new Booking request for ${Travel_ID}`) return yfligths.emit ('BookingCreated', { flight, date }, { Travel_ID, Pos }) })) }) xflights.after('BookingCreated/#succeeded', async function(_, req) { const { Travel_ID, Pos } = req.headers + console.log (`[${3}] Booking Succeeded: Update Booking Status for ${Travel_ID} to 'C'`) await UPDATE(Bookings, { Travel_ID, Pos }).set({ Status_code: 'C' }) }) xflights.after('BookingCreated/#failed', async function(err, req) { const { Travel_ID, Pos } = req.headers + console.log (`[${3}] Booking Failed: Update Booking Status for ${Travel_ID} to 'F'`) await UPDATE(Bookings, { Travel_ID, Pos }).set({ Status_code: 'F' }) }) @@ -59,6 +67,7 @@ class TravelService extends cds.ApplicationService { const { flight_ID: ID, date } = event.data // read current! free seats (messages can overtake each other -> don't propagate free seats in payload) const free_seats = await xflights.read(Flights, { ID, date }).columns('free_seats') + console.log (`[${4}] Update available seats for ${ID} to ${free_seats}`) await UPDATE(Flights, { ID, date }).set(free_seats) }) diff --git a/test/saga.http b/test/saga.http index a7cd2c2..2476ca2 100644 --- a/test/saga.http +++ b/test/saga.http @@ -1,12 +1,10 @@ @service = http://localhost:4004/odata/v4/travel ### - -GET {{service}}/Flights(ID='GA0322',date='2023-08-05') +GET {{service}}/Flights(ID='GA0322',date='2023-08-05') Authorization: Basic alice: ### - POST {{service}}/Travels Authorization: Basic alice: Content-Type: application/json @@ -28,3 +26,29 @@ Content-Type: application/json } ] } + + +### Fully Booked + +POST {{service}}/Travels +Authorization: Basic alice: +Content-Type: application/json + +{ + "ID": 4190, + "Description": "Test Travel Fully Booked", + "BeginDate": "2020-01-01", + "EndDate": "2030-12-31", + "Agency_ID": "070007", + "Customer_ID": "000001", + "Currency_code": "EUR", + "Bookings": [ + { + "Pos": 1, + "Flight_ID": "GA0322", + "Flight_date": "2023-08-05", + "BookingDate": "2022-08-05", + "Currency_code": "EUR" + } + ] +} From 91e2f5e9add211e02960d872ba36b3c8a491de49 Mon Sep 17 00:00:00 2001 From: I543501 Date: Mon, 13 Jul 2026 14:39:54 +0200 Subject: [PATCH 2/9] use cds logging --- package.json | 3 ++- srv/data-federation.js | 3 ++- srv/travel-service.js | 14 ++++++++------ 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index 387e221..a990b60 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,8 @@ }, "log": { "levels": { - "odata": "error" + "odata": "error", + "cds": "debug" } } }, diff --git a/srv/data-federation.js b/srv/data-federation.js index 8bf479a..e05c696 100644 --- a/srv/data-federation.js +++ b/srv/data-federation.js @@ -1,5 +1,6 @@ const cds = require ('@sap/cds') const feed = [] +const LOG = cds.log() // Collect all entities to be federated, and prepare replica tables cds.on ('loaded', csn => { @@ -30,7 +31,7 @@ async function replicate (req) { let { latest } = await SELECT.one `max(modifiedAt) as latest` .from (entity) let rows = await remote.read (entity) .where `modifiedAt > ${latest||0}` if (rows.length) await UPSERT (rows) .into (entity); else return - console.log ('Replicated', rows.length, 'entries', { for: entity, via: this.kind }) // eslint-disable-line no-console + LOG.info('Replicated', rows.length, 'entries', { for: entity, via: this.kind }) // eslint-disable-line no-console } // Helpers to identify remote services, and check whether they are connected diff --git a/srv/travel-service.js b/srv/travel-service.js index b24b243..fbd7d11 100644 --- a/srv/travel-service.js +++ b/srv/travel-service.js @@ -1,6 +1,7 @@ const cds = require ('@sap/cds') class TravelService extends cds.ApplicationService { + LOG async init() { await this.service_integration() @@ -9,6 +10,7 @@ class TravelService extends cds.ApplicationService { this.update_totals() this.status_flows() this.data_export() + LOG = LOG.info() return super.init() } @@ -34,7 +36,7 @@ class TravelService extends cds.ApplicationService { // REVISIT: generic crud does not handle @cds.persistence.table and hence rejects due to @cds.persistence.skip this.on('READ', [Flights, Supplements], async function(req) { if (req.target['@cds.persistence.table']) { - console.log (`Select from local replica for ${req.query._target.name}`) + LOG.info(`Select from local replica for ${req.query._target.name}`) return cds.db.run(req.query) } return xflights.run(req.query) @@ -46,20 +48,20 @@ class TravelService extends cds.ApplicationService { return Promise.all (Bookings.map (booking => { let { Flight_ID: flight, Flight_date: date, Travel_ID, Pos } = booking // REVISIT: transport Travel_ID, Pos to callback via headers -> we need a backpack - console.log (`[${1}] Emit new Booking request for ${Travel_ID}`) + LOG.info(`[${1}] Emit new Booking request for ${Travel_ID}`) return yfligths.emit ('BookingCreated', { flight, date }, { Travel_ID, Pos }) })) }) xflights.after('BookingCreated/#succeeded', async function(_, req) { const { Travel_ID, Pos } = req.headers - console.log (`[${3}] Booking Succeeded: Update Booking Status for ${Travel_ID} to 'C'`) + LOG.info(`[${3}] Booking Succeeded: Update Booking Status for ${Travel_ID} to 'C'`) await UPDATE(Bookings, { Travel_ID, Pos }).set({ Status_code: 'C' }) }) xflights.after('BookingCreated/#failed', async function(err, req) { const { Travel_ID, Pos } = req.headers - console.log (`[${3}] Booking Failed: Update Booking Status for ${Travel_ID} to 'F'`) + LOG.info(`[${3}] Booking Failed: Update Booking Status for ${Travel_ID} to 'F'`) await UPDATE(Bookings, { Travel_ID, Pos }).set({ Status_code: 'F' }) }) @@ -67,8 +69,8 @@ class TravelService extends cds.ApplicationService { const { flight_ID: ID, date } = event.data // read current! free seats (messages can overtake each other -> don't propagate free seats in payload) const free_seats = await xflights.read(Flights, { ID, date }).columns('free_seats') - console.log (`[${4}] Update available seats for ${ID} to ${free_seats}`) - await UPDATE(Flights, { ID, date }).set(free_seats) + LOG.info(`[${4}] Update available seats for ${ID} to ${free_seats.free_seats}`) + await UPDATE(Flights, { ID, date }).set(free_seats.free_seats) }) // Update local Flights data whenever occupied seats change in XFlights From 78b69719ec0a18d655de59b64b1fe3e5977272e5 Mon Sep 17 00:00:00 2001 From: I543501 Date: Mon, 13 Jul 2026 14:53:59 +0200 Subject: [PATCH 3/9] use cds log --- srv/travel-service.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/srv/travel-service.js b/srv/travel-service.js index fbd7d11..02a30c7 100644 --- a/srv/travel-service.js +++ b/srv/travel-service.js @@ -1,7 +1,6 @@ const cds = require ('@sap/cds') class TravelService extends cds.ApplicationService { - LOG async init() { await this.service_integration() @@ -10,7 +9,6 @@ class TravelService extends cds.ApplicationService { this.update_totals() this.status_flows() this.data_export() - LOG = LOG.info() return super.init() } @@ -19,6 +17,7 @@ class TravelService extends cds.ApplicationService { * Integrates with the XFlights service to keep Flights data in sync on both sides. */ async service_integration() { + const LOG = cds.log() const s4 = await cds.connect.to ('sap.capire.s4.business-partner') const xflights = await cds.connect.to ('sap.capire.flights.data') @@ -70,7 +69,7 @@ class TravelService extends cds.ApplicationService { // read current! free seats (messages can overtake each other -> don't propagate free seats in payload) const free_seats = await xflights.read(Flights, { ID, date }).columns('free_seats') LOG.info(`[${4}] Update available seats for ${ID} to ${free_seats.free_seats}`) - await UPDATE(Flights, { ID, date }).set(free_seats.free_seats) + await UPDATE(Flights, { ID, date }).set(free_seats) }) // Update local Flights data whenever occupied seats change in XFlights From 99179c907a3fe490379f70a5af73438d5b6cf51f Mon Sep 17 00:00:00 2001 From: I543501 Date: Tue, 14 Jul 2026 09:24:14 +0200 Subject: [PATCH 4/9] logging again --- srv/travel-service.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/srv/travel-service.js b/srv/travel-service.js index b8337d9..7ed6592 100644 --- a/srv/travel-service.js +++ b/srv/travel-service.js @@ -36,6 +36,7 @@ class TravelService extends cds.ApplicationService { return Promise.all (Bookings.map (booking => { let { Flight_ID: flight, Flight_date: date, Travel_ID, Pos } = booking // Transport Travel_ID, Pos to callback via headers + LOG.info(`[${1}] Emit BookingCreated for Flight ${flight}`) return yfligths.emit ('BookingCreated', { flight, date }, { Travel_ID, Pos }) })) }) @@ -55,6 +56,7 @@ class TravelService extends cds.ApplicationService { // Update local Flights data whenever occupied seats change in XFlights if (Flights['@cds.persistence.table']) xflights.on ('FlightsUpdated', async msg => { const { flight:ID, date, free_seats } = msg.data + LOG.info(`[${4}] Update Flight ${ID} free seats to ${free_seats}`) await UPDATE (Flights, { ID, date }) .with ({ free_seats }) }) } From 1be09269c62252e5860c9d0e681ef84a5c84ac1c Mon Sep 17 00:00:00 2001 From: Sebastian Van Syckel Date: Tue, 14 Jul 2026 15:03:35 +0200 Subject: [PATCH 5/9] rm Outbox --- srv/travel-service/service.cds | 1 - 1 file changed, 1 deletion(-) diff --git a/srv/travel-service/service.cds b/srv/travel-service/service.cds index 128eb46..98bd968 100644 --- a/srv/travel-service/service.cds +++ b/srv/travel-service/service.cds @@ -18,7 +18,6 @@ using { sap.capire.s4 } from '../../apis/capire/s4'; @readonly entity Customers as projection on s4.Customers; @readonly entity Flights as projection on x.Flights; @readonly entity Supplements as projection on x.Supplements; - entity Outbox as projection on cds.outbox.Messages; } // Custom type for percentage values From 35c27c5ccefcc8ca8f0b914b9630476fcbdf056d Mon Sep 17 00:00:00 2001 From: Sebastian Van Syckel Date: Tue, 14 Jul 2026 15:04:02 +0200 Subject: [PATCH 6/9] status next to main entity --- db/schema.cds | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/db/schema.cds b/db/schema.cds index e9dbf7d..8de7b3c 100644 --- a/db/schema.cds +++ b/db/schema.cds @@ -19,6 +19,16 @@ entity Travels : managed { Bookings : Composition of many Bookings on Bookings.Travel = $self; } +entity TravelStatus : sap.common.CodeList { + key code : String(1) enum { + Open = 'O'; + InReview = 'P'; + Blocked = 'B'; + Accepted = 'A'; + Rejected = 'X'; + } +} + entity Bookings { key Travel : Association to Travels; @@ -58,16 +68,6 @@ entity TravelAgencies { }; -entity TravelStatus : sap.common.CodeList { - key code : String(1) enum { - Open = 'O'; - InReview = 'P'; - Blocked = 'B'; - Accepted = 'A'; - Rejected = 'X'; - } -} - type Price : Decimal(9,4); From 106dd0df86e4cf36d598b817158a80b5f01deb6a Mon Sep 17 00:00:00 2001 From: Sebastian Van Syckel Date: Tue, 14 Jul 2026 15:05:02 +0200 Subject: [PATCH 7/9] revert --- srv/travel-service/service.cds | 1 + 1 file changed, 1 insertion(+) diff --git a/srv/travel-service/service.cds b/srv/travel-service/service.cds index 98bd968..57c8293 100644 --- a/srv/travel-service/service.cds +++ b/srv/travel-service/service.cds @@ -18,6 +18,7 @@ using { sap.capire.s4 } from '../../apis/capire/s4'; @readonly entity Customers as projection on s4.Customers; @readonly entity Flights as projection on x.Flights; @readonly entity Supplements as projection on x.Supplements; + } // Custom type for percentage values From ef65b02f898a1d9cf5b050401cee084a78f550aa Mon Sep 17 00:00:00 2001 From: sjvans <30337871+sjvans@users.noreply.github.com> Date: Tue, 14 Jul 2026 15:06:01 +0200 Subject: [PATCH 8/9] Apply suggestion from @sjvans --- srv/travel-service/service.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/srv/travel-service/service.js b/srv/travel-service/service.js index 9b3002b..6464625 100644 --- a/srv/travel-service/service.js +++ b/srv/travel-service/service.js @@ -25,8 +25,6 @@ class TravelService extends cds.ApplicationService { const { Flights, Travels, Customers } = this.entities const { Bookings } = cds.entities ('sap.capire.travels') - this.on('READ', 'Outbox', () => cds.db.run(SELECT.from('cds.outbox.Messages'))) - // Delegate value helpp requests on Customers to S4 Business Partner service this.on ('READ', Customers, req => s4.run (req.query)) From 39abd6b557ebdafe962e1a7685d10e8107b663c8 Mon Sep 17 00:00:00 2001 From: I543501 Date: Tue, 14 Jul 2026 15:18:20 +0200 Subject: [PATCH 9/9] logging --- srv/travel-service/service.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/srv/travel-service/service.js b/srv/travel-service/service.js index 6464625..a8c4ca4 100644 --- a/srv/travel-service/service.js +++ b/srv/travel-service/service.js @@ -41,13 +41,13 @@ class TravelService extends cds.ApplicationService { xflights.after('BookingCreated/#succeeded', async function(_, req) { const { Travel_ID, Pos } = req.headers - LOG.info(`[${3}] Booking Succeeded: Update Booking Status for ${Travel_ID} to 'C'`) + LOG.info(`[${4}] Booking Succeeded: Update Booking Status for ${Travel_ID} to 'C'`) await UPDATE(Bookings, { Travel_ID, Pos }).set({ Status_code: 'C' }) }) xflights.after('BookingCreated/#failed', async function(err, req) { const { Travel_ID, Pos } = req.headers - LOG.info(`[${3}] Booking Failed: Update Booking Status for ${Travel_ID} to 'F'`) + LOG.info(`[${4}] Booking Failed: Update Booking Status for ${Travel_ID} to 'F'`) await UPDATE(Bookings, { Travel_ID, Pos }).set({ Status_code: 'F' }) }) @@ -55,7 +55,7 @@ class TravelService extends cds.ApplicationService { if (Flights['@cds.persistence.table']) xflights.on ('FlightsUpdated', async function(msg) { const { flight:ID, date } = msg.data const { free_seats } = await this.read(Flights, { ID, date }).columns('free_seats') - LOG.info(`[${4}] Update Flight ${ID} free seats to ${free_seats}`) + LOG.info(`[${5}] Update Flight ${ID} free seats to ${free_seats}`) await UPDATE (Flights, { ID, date }) .with ({ free_seats }) }) }