1- import { ApplicationService } from '@sap/cds'
21import * as cds from '@sap/cds'
3- import { Booking , BookingSupplement , Travel } from '#cds-models/TravelService'
4- import { BookingStatusCode , TravelStatusCode } from '#cds-models/sap/fe/cap/travel'
2+ import { Booking , BookingSupplement as Supplements , Travel } from '#cds-models/TravelService'
3+ import { TravelStatusCode } from '#cds-models/sap/fe/cap/travel'
54import { CdsDate } from '#cds-models/_'
65
6+ export class TravelService extends cds . ApplicationService { init ( ) {
77
8- export class TravelService extends ApplicationService {
9- init ( ) {
8+ // Reflected definitions from the service's CDS model
9+ const { today } = cds . builtin . types . Date as unknown as { today ( ) : CdsDate } ;
1010
11- /**
12- * Fill in primary keys for new Travels.
13- * Note: In contrast to Bookings and BookingSupplements that has to happen
14- * upon SAVE, as multiple users could create new Travels concurrently.
15- */
16- this . before ( 'CREATE' , Travel , async req => {
17- // FIXME: TS v
18- const { maxID } = await SELECT . one `max(TravelID) as maxID` . from ( Travel ) as unknown as { maxID : number }
19- req . data . TravelID = maxID + 1
20- } )
2111
12+ // Fill in alternative keys as consecutive numbers for new Travels, Bookings, and Supplements.
13+ // Note: For Travels that can't be done at NEW events, that is when drafts are created,
14+ // but on CREATE only, as multiple users could create new Travels concurrently.
2215
23- /**
24- * Fill in defaults for new Bookings when editing Travels.
25- */
26- this . before ( 'NEW' , Booking . drafts , async ( req ) => {
27- const { to_Travel_TravelUUID } = req . data
28- const { status } = await SELECT . one . from ( Travel . drafts , to_Travel_TravelUUID , t => t . TravelStatus_code . as ( 'status' ) ) as { status : string }
29- if ( status === TravelStatusCode . Canceled ) throw req . reject ( 400 , 'Cannot add new bookings to rejected travels.' )
30- // FIXME: TS v
31- const { maxID } = await SELECT . one `max(BookingID) as maxID` . from ( Booking . drafts ) . where ( { to_Travel_TravelUUID } ) as unknown as { maxID : number }
32- req . data . BookingID = maxID + 1
33- req . data . BookingStatus_code = BookingStatusCode . New
34- req . data . BookingDate = ( new Date ) . toISOString ( ) . slice ( 0 , 10 ) as CdsDate // today
16+ this . before ( 'CREATE' , Travel , async req => {
17+ let { maxID } = await SELECT . one ( `max(TravelID) as maxID` ) . from ( Travel ) as { maxID : number }
18+ req . data . TravelID = ++ maxID
3519 } )
3620
37-
38- /**
39- * Fill in defaults for new BookingSupplements when editing Travels.
40- */
41- this . before ( 'NEW' , BookingSupplement . drafts , async ( req ) => {
42- const { to_Booking_BookingUUID } = req . data
43- // FIXME: TS v
44- const { maxID } = await SELECT . one `max(BookingSupplementID) as maxID` . from ( BookingSupplement . drafts ) . where ( { to_Booking_BookingUUID } ) as unknown as { maxID : number }
45- req . data . BookingSupplementID = maxID + 1
21+ this . before ( 'NEW' , Booking . drafts , async req => {
22+ let { maxID } = await SELECT . one ( `max(BookingID) as maxID` ) . from ( Booking . drafts ) . where ( req . data ) as { maxID : number }
23+ req . data . BookingID = ++ maxID
24+ req . data . BookingDate = today ( ) // REVISIT: could that be filled in by CAP automatically?
4625 } )
4726
48-
49- /**
50- * Changing Booking Fees is only allowed for not yet accapted Travels.
51- */
52- this . before ( 'UPDATE' , Travel . drafts , async ( req ) => { if ( 'BookingFee' in req . data ) {
53- const { TravelStatus_code : status } = await SELECT . one . from ( req . subject ) as Travel
54- if ( status === TravelStatusCode . Accepted ) req . reject ( 400 , 'Booking fee can not be updated for accepted travels.' , 'BookingFee' )
55- } } )
56-
57-
58- /**
59- * Update the Travel's TotalPrice when its BookingFee is modified.
60- */
61- this . after ( 'UPDATE' , Travel . drafts , ( _ , req ) => {
62- const { TravelUUID } = req . data
63- if ( 'BookingFee' in req . data || 'GoGreen' in req . data ) {
64- return this . _update_totals4 ( TravelUUID )
65- }
27+ this . before ( 'NEW' , Supplements . drafts , async req => {
28+ let { maxID } = await SELECT . one ( `max(BookingSupplementID) as maxID` ) . from ( Supplements . drafts ) . where ( req . data ) as { maxID : number }
29+ req . data . BookingSupplementID = ++ maxID
6630 } )
6731
6832
69- /**
70- * Update the Travel's TotalPrice when a Booking's FlightPrice is modified.
71- */
72- this . after ( 'UPDATE' , Booking . drafts , async ( _ , req ) => { if ( 'FlightPrice' in req . data ) {
73- // We need to fetch the Travel's UUID for the given Booking target
74- const { travel } = await SELECT . one `to_Travel_TravelUUID as travel` . from ( req . subject )
75- return this . _update_totals4 ( travel )
76- } } )
77-
78-
79- /**
80- * Update the Travel's TotalPrice when a Supplement's Price is modified.
81- */
82- this . after ( 'UPDATE' , BookingSupplement . drafts , async ( _ , req ) => { if ( 'Price' in req . data ) {
83- const { BookSupplUUID } = req . data
84- // We need to fetch the Travel's UUID for the given Supplement target
85- const BookingUUID = SELECT . one ( BookingSupplement . drafts , b => b . to_Booking_BookingUUID ) . where ( { BookSupplUUID } )
86- const { to_Travel_TravelUUID : travel } = await SELECT . one ( Booking . drafts , b => b . to_Travel_TravelUUID ) . where ( { BookingUUID } )
87- return this . _update_totals4 ( travel )
88- } } )
89-
90- /**
91- * Update the Travel's TotalPrice when a Booking Supplement is deleted.
92- */
93- this . on ( 'CANCEL' , BookingSupplement . drafts , async ( req , next ) => {
94- // Find out which travel is affected before the delete
95- const { BookSupplUUID } = req . data
96- const { to_Travel_TravelUUID } = await SELECT . one
97- . from ( BookingSupplement . drafts , b => b . to_Travel_TravelUUID )
98- . where ( { BookSupplUUID } )
99- // Delete handled by generic handlers
100- const res = await next ( )
101- // After the delete, update the totals
102- await this . _update_totals4 ( to_Travel_TravelUUID )
103- return res
33+ // Ensure BeginDate is not before today and not after EndDate.
34+ this . before ( 'SAVE' , Travel , req => {
35+ const { BeginDate, EndDate } = req . data
36+ if ( BeginDate < today ( ) ) req . error ( 400 , `Begin Date must not be before today.` , 'in/BeginDate' )
37+ if ( BeginDate > EndDate ) req . error ( 400 , `End Date must be after Begin Date.` , 'in/EndDate' )
10438 } )
10539
106- /**
107- * Update the Travel's TotalPrice when a Booking is deleted.
108- */
109- this . on ( 'CANCEL' , Booking . drafts , async ( req , next ) => {
110- // Find out which travel is affected before the delete
111- const { BookingUUID } = req . data
112- const { to_Travel_TravelUUID } = await SELECT . one
113- . from ( Booking . drafts , b => b . to_Travel_TravelUUID )
114- . where ( { BookingUUID } )
115- // Delete handled by generic handlers
116- const res = await next ( )
117- // After the delete, update the totals
118- await this . _update_totals4 ( to_Travel_TravelUUID )
119- return res
120- } )
40+
41+ // Update a Travel's TotalPrice whenever its BookingFee is modified,
42+ // or when a nested Booking is deleted or its FlightPrice is modified,
43+ // or when a nested Supplement is deleted or its Price is modified.
44+
45+ this . on ( 'UPDATE' , Travel . drafts , ( req , next ) => update_totals ( req , next , [ 'BookingFee' , 'GoGreen' ] ) )
46+ this . on ( 'UPDATE' , Booking . drafts , ( req , next ) => update_totals ( req , next , [ 'FlightPrice' ] ) )
47+ this . on ( 'UPDATE' , Supplements . drafts , ( req , next ) => update_totals ( req , next , [ 'Price' ] ) )
48+ this . on ( 'DELETE' , Booking . drafts , ( req , next ) => update_totals ( req , next ) )
49+ this . on ( 'DELETE' , Supplements . drafts , ( req , next ) => update_totals ( req , next ) )
50+
51+ // Note: using .on handlers as we need to read a Booking's or Supplement's TravelUUID before they are deleted.
52+ async function update_totals ( req : cds . Request , next : Function , fields ?: string [ ] ) {
53+ if ( fields && ! fields . some ( f => f in req . data ) ) return next ( ) //> skip if no relevant data changed
54+ const travel = ( req . data as Travel ) . TravelUUID || ( await SELECT . one `to_Travel.TravelUUID as id` . from ( req . subject ) ) . id
55+ await next ( ) // actually UPDATE or DELETE the subject entity
56+ await update_totalsGreen ( travel ) ;
57+ await cds . run ( `UPDATE ${ Travel . drafts } SET TotalPrice = coalesce (BookingFee,0)
58+ + ( SELECT coalesce (sum(FlightPrice),0) from ${ Booking . drafts } where to_Travel_TravelUUID = TravelUUID )
59+ + ( SELECT coalesce (sum(Price),0) from ${ Supplements . drafts } where to_Travel_TravelUUID = TravelUUID )
60+ WHERE TravelUUID = ?` , [ travel ] )
61+ }
12162
12263 /**
123- * Validate a Travel's edited data before save.
64+ * Trees-for-Tickets: helper to update totals including green flight fee
12465 */
125- this . before ( 'SAVE' , Travel , req => {
126- const { BeginDate, EndDate, BookingFee, to_Agency_AgencyID, to_Customer_CustomerID, to_Booking, TravelStatus_code } = req . data , today = ( new Date ) . toISOString ( ) . slice ( 0 , 10 )
127-
128- // validate only not rejected travels
129- if ( TravelStatus_code !== TravelStatusCode . Canceled ) {
130- if ( BookingFee == null ) req . error ( 400 , "Enter a booking fee" , "in/BookingFee" ) // 0 is a valid BookingFee
131- if ( ! BeginDate ) req . error ( 400 , "Enter a begin date" , "in/BeginDate" )
132- if ( ! EndDate ) req . error ( 400 , "Enter an end date" , "in/EndDate" )
133- if ( ! to_Agency_AgencyID ) req . error ( 400 , "Enter a travel agency" , "in/to_Agency_AgencyID" )
134- if ( ! to_Customer_CustomerID ) req . error ( 400 , "Enter a customer" , "in/to_Customer_CustomerID" )
135-
136- for ( const booking of to_Booking ) {
137- const { BookingUUID, ConnectionID, FlightDate, FlightPrice, BookingStatus_code, to_Carrier_AirlineID, to_Customer_CustomerID } = booking
138- if ( ! ConnectionID ) req . error ( 400 , "Enter a flight" , `in/to_Booking(BookingUUID='${ BookingUUID } ',IsActiveEntity=false)/ConnectionID` )
139- if ( ! FlightDate ) req . error ( 400 , "Enter a flight date" , `in/to_Booking(BookingUUID='${ BookingUUID } ',IsActiveEntity=false)/FlightDate` )
140- if ( ! FlightPrice ) req . error ( 400 , "Enter a flight price" , `in/to_Booking(BookingUUID='${ BookingUUID } ',IsActiveEntity=false)/FlightPrice` )
141- if ( ! BookingStatus_code ) req . error ( 400 , "Enter a booking status" , `in/to_Booking(BookingUUID='${ BookingUUID } ',IsActiveEntity=false)/BookingStatus_code` )
142- if ( ! to_Carrier_AirlineID ) req . error ( 400 , "Enter an airline" , `in/to_Booking(BookingUUID='${ BookingUUID } ',IsActiveEntity=false)/to_Carrier_AirlineID` )
143- if ( ! to_Customer_CustomerID ) req . error ( 400 , "Enter a customer" , `in/to_Booking(BookingUUID='${ BookingUUID } ',IsActiveEntity=false)/to_Customer_CustomerID` )
144-
145- for ( const suppl of booking . to_BookSupplement ) {
146- const { BookSupplUUID, Price, to_Supplement_SupplementID } = suppl
147- if ( ! Price ) req . error ( 400 , "Enter a price" , `in/to_Booking(BookingUUID='${ BookingUUID } ',IsActiveEntity=false)/to_BookSupplement(BookSupplUUID='${ BookSupplUUID } ',IsActiveEntity=false)/Price` )
148- if ( ! to_Supplement_SupplementID ) req . error ( 400 , "Enter a supplement" , `in/to_Booking(BookingUUID='${ BookingUUID } ',IsActiveEntity=false)/to_BookSupplement(BookSupplUUID='${ BookSupplUUID } ',IsActiveEntity=false)/to_Supplement_SupplementID` )
149- }
150- }
66+ async function update_totalsGreen ( TravelUUID : string ) {
67+ const { GoGreen } = await SELECT . one . from ( Travel . drafts ) . columns ( 'GoGreen' ) . where ( { TravelUUID } )
68+ if ( GoGreen ) {
69+ await UPDATE ( Travel . drafts , TravelUUID )
70+ . set `GreenFee = round(BookingFee * 0.1, 0)`
71+ . set `TreesPlanted = round(BookingFee * 0.1, 0)`
72+ } else {
73+ await UPDATE ( Travel . drafts , TravelUUID )
74+ . set `GreenFee = 0`
75+ . set `TreesPlanted = 0`
15176 }
152-
153- if ( BeginDate < today ) req . error ( 400 , `Begin Date ${ BeginDate } must not be before today ${ today } .` , 'in/BeginDate' )
154- if ( BeginDate > EndDate ) req . error ( 400 , `Begin Date ${ BeginDate } must be before End Date ${ EndDate } .` , 'in/BeginDate' )
155- } )
77+ }
15678
15779
15880 //
15981 // Action Implementations...
16082 //
161- const { acceptTravel, rejectTravel, deductDiscount } = Travel . actions
16283
163- this . on ( acceptTravel , req => UPDATE ( req . subject ) . with ( { TravelStatus_code : TravelStatusCode . Accepted } ) )
164- this . on ( rejectTravel , req => UPDATE ( req . subject ) . with ( { TravelStatus_code : TravelStatusCode . Canceled } ) )
165- this . on ( deductDiscount , async req => {
84+ const { acceptTravel, rejectTravel, deductDiscount } = Travel . actions ;
85+ this . on ( acceptTravel , req => UPDATE ( req . subject ) . with ( { TravelStatus_code : TravelStatusCode . Accepted } ) )
86+ this . on ( rejectTravel , req => UPDATE ( req . subject ) . with ( { TravelStatus_code : TravelStatusCode . Canceled } ) )
87+ this . on ( deductDiscount , async req => {
16688 let discount = req . data . percent / 100
167- let succeeded = await UPDATE ( req . subject )
168- . where `TravelStatus_code != 'A'`
169- . and `BookingFee is not null`
170- . set `TotalPrice = round (TotalPrice - BookingFee * ${ discount } , 3)`
171- . set `BookingFee = round (BookingFee - BookingFee * ${ discount } , 3)`
89+ let succeeded = await UPDATE ( req . subject ) . where `TravelStatus.code != 'A'` . and `BookingFee != null`
90+ . with `TotalPrice = round (TotalPrice - BookingFee * ${ discount } , 3)`
91+ . with `BookingFee = round (BookingFee - BookingFee * ${ discount } , 3)`
17292
17393 if ( ! succeeded ) { //> let's find out why...
174- let travel = await SELECT . one `TravelID as ID, TravelStatus_code as status, BookingFee` . from ( req . subject )
94+ let travel = await SELECT . one `TravelID as ID, TravelStatus.code as status, BookingFee` . from ( req . subject )
17595 if ( ! travel ) throw req . reject ( 404 , `Travel "${ travel . ID } " does not exist; may have been deleted meanwhile.` )
176- if ( travel . status === TravelStatusCode . Accepted ) req . reject ( 400 , `Travel "${ travel . ID } " has been approved already.` )
96+ if ( travel . status === TravelStatusCode . Accepted ) throw req . reject ( 400 , `Travel "${ travel . ID } " has been approved already.` )
17797 if ( travel . BookingFee == null ) throw req . reject ( 404 , `No discount possible, as travel "${ travel . ID } " does not yet have a booking fee added.` )
17898 } else {
17999 return SELECT ( req . subject )
@@ -183,35 +103,4 @@ init() {
183103 // Add base class's handlers. Handlers registered above go first.
184104 return super . init ( )
185105
186- }
187-
188- /**
189- * Helper to re-calculate a Travel's TotalPrice from BookingFees, FlightPrices and Supplement Prices.
190- */
191- async _update_totals4 ( travel : string ) {
192- await this . _update_totalsGreen ( travel )
193- // Using plain native SQL for such complex queries
194- await cds . run ( `UPDATE ${ Travel . drafts } SET
195- TotalPrice = coalesce(BookingFee,0)
196- + coalesce(GreenFee,0)
197- + ( SELECT coalesce (sum(FlightPrice),0) from ${ Booking . drafts } where to_Travel_TravelUUID = TravelUUID )
198- + ( SELECT coalesce (sum(Price),0) from ${ BookingSupplement . drafts } where to_Travel_TravelUUID = TravelUUID )
199- WHERE TravelUUID = ?` , [ travel ] )
200- }
201-
202- /**
203- * Trees-for-Tickets: helper to update totals including green flight fee
204- */
205- async _update_totalsGreen ( TravelUUID ) {
206- const { GoGreen } = await SELECT . one `GoGreen` . from ( Travel . drafts ) . where ( { TravelUUID } )
207- if ( GoGreen ) {
208- await UPDATE ( Travel . drafts , TravelUUID )
209- . set `GreenFee = round(BookingFee * 0.1, 0)`
210- . set `TreesPlanted = round(BookingFee * 0.1, 0)`
211- } else {
212- await UPDATE ( Travel . drafts , TravelUUID )
213- . set `GreenFee = 0`
214- . set `TreesPlanted = 0`
215- }
216- }
217- }
106+ } }
0 commit comments