Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 0 additions & 89 deletions srv/travel-service.js

This file was deleted.

245 changes: 67 additions & 178 deletions srv/travel-service.ts
Original file line number Diff line number Diff line change
@@ -1,179 +1,99 @@
import { ApplicationService } from '@sap/cds'
import * as cds from '@sap/cds'
import { Booking, BookingSupplement, Travel } from '#cds-models/TravelService'
import { BookingStatusCode, TravelStatusCode } from '#cds-models/sap/fe/cap/travel'
import { Booking, BookingSupplement as Supplements, Travel } from '#cds-models/TravelService'
import { TravelStatusCode } from '#cds-models/sap/fe/cap/travel'
import { CdsDate } from '#cds-models/_'

export class TravelService extends cds.ApplicationService { init() {

export class TravelService extends ApplicationService {
init() {
// Reflected definitions from the service's CDS model
const { today } = cds.builtin.types.Date as unknown as { today(): CdsDate };

/**
* Fill in primary keys for new Travels.
* Note: In contrast to Bookings and BookingSupplements that has to happen
* upon SAVE, as multiple users could create new Travels concurrently.
*/
this.before ('CREATE', Travel, async req => {
// FIXME: TS v
const { maxID } = await SELECT.one `max(TravelID) as maxID` .from(Travel) as unknown as { maxID: number }
req.data.TravelID = maxID + 1
})

// Fill in alternative keys as consecutive numbers for new Travels, Bookings, and Supplements.
// Note: For Travels that can't be done at NEW events, that is when drafts are created,
// but on CREATE only, as multiple users could create new Travels concurrently.

/**
* Fill in defaults for new Bookings when editing Travels.
*/
this.before ('NEW', Booking.drafts, async (req) => {
const { to_Travel_TravelUUID } = req.data
const { status } = await SELECT.one .from (Travel.drafts, to_Travel_TravelUUID, t => t.TravelStatus_code.as('status')) as { status: string }
if (status === TravelStatusCode.Canceled) throw req.reject (400, 'Cannot add new bookings to rejected travels.')
// FIXME: TS v
const { maxID } = await SELECT.one `max(BookingID) as maxID` .from(Booking.drafts) .where({ to_Travel_TravelUUID }) as unknown as { maxID: number }
req.data.BookingID = maxID + 1
req.data.BookingStatus_code = BookingStatusCode.New
req.data.BookingDate = (new Date).toISOString().slice(0, 10) as CdsDate // today
this.before ('CREATE', Travel, async req => {
let { maxID } = await SELECT.one (`max(TravelID) as maxID`) .from (Travel) as { maxID: number }
req.data.TravelID = ++maxID
})


/**
* Fill in defaults for new BookingSupplements when editing Travels.
*/
this.before ('NEW', BookingSupplement.drafts, async (req) => {
const { to_Booking_BookingUUID } = req.data
// FIXME: TS v
const { maxID } = await SELECT.one `max(BookingSupplementID) as maxID` .from(BookingSupplement.drafts) .where({ to_Booking_BookingUUID }) as unknown as { maxID: number }
req.data.BookingSupplementID = maxID + 1
this.before ('NEW', Booking.drafts, async req => {
let { maxID } = await SELECT.one (`max(BookingID) as maxID`) .from (Booking.drafts) .where (req.data) as { maxID: number }
req.data.BookingID = ++maxID
req.data.BookingDate = today() // REVISIT: could that be filled in by CAP automatically?
})


/**
* Changing Booking Fees is only allowed for not yet accapted Travels.
*/
this.before ('UPDATE', Travel.drafts, async (req) => { if ('BookingFee' in req.data) {
const { TravelStatus_code: status } = await SELECT.one .from(req.subject) as Travel
if (status === TravelStatusCode.Accepted) req.reject(400, 'Booking fee can not be updated for accepted travels.', 'BookingFee')
}})


/**
* Update the Travel's TotalPrice when its BookingFee is modified.
*/
this.after('UPDATE', Travel.drafts, (_, req) => {
const { TravelUUID } = req.data
if ('BookingFee' in req.data || 'GoGreen' in req.data) {
return this._update_totals4(TravelUUID)
}
this.before ('NEW', Supplements.drafts, async req => {
let { maxID } = await SELECT.one (`max(BookingSupplementID) as maxID`) .from (Supplements.drafts) .where (req.data) as { maxID: number }
req.data.BookingSupplementID = ++maxID
})


/**
* Update the Travel's TotalPrice when a Booking's FlightPrice is modified.
*/
this.after ('UPDATE', Booking.drafts, async (_, req) => { if ('FlightPrice' in req.data) {
// We need to fetch the Travel's UUID for the given Booking target
const { travel } = await SELECT.one `to_Travel_TravelUUID as travel` .from(req.subject)
return this._update_totals4 (travel)
}})


/**
* Update the Travel's TotalPrice when a Supplement's Price is modified.
*/
this.after ('UPDATE', BookingSupplement.drafts, async (_, req) => { if ('Price' in req.data) {
const { BookSupplUUID } = req.data
// We need to fetch the Travel's UUID for the given Supplement target
const BookingUUID = SELECT.one(BookingSupplement.drafts, b => b.to_Booking_BookingUUID) .where({ BookSupplUUID })
const { to_Travel_TravelUUID: travel } = await SELECT.one (Booking.drafts, b => b.to_Travel_TravelUUID) .where({ BookingUUID })
return this._update_totals4(travel)
}})

/**
* Update the Travel's TotalPrice when a Booking Supplement is deleted.
*/
this.on('CANCEL', BookingSupplement.drafts, async (req, next) => {
// Find out which travel is affected before the delete
const { BookSupplUUID } = req.data
const { to_Travel_TravelUUID } = await SELECT.one
.from(BookingSupplement.drafts, b => b.to_Travel_TravelUUID)
.where({ BookSupplUUID })
// Delete handled by generic handlers
const res = await next()
// After the delete, update the totals
await this._update_totals4(to_Travel_TravelUUID)
return res
// Ensure BeginDate is not before today and not after EndDate.
this.before ('SAVE', Travel, req => {
const { BeginDate, EndDate } = req.data
if (BeginDate < today()) req.error (400, `Begin Date must not be before today.`, 'in/BeginDate')
if (BeginDate > EndDate) req.error (400, `End Date must be after Begin Date.`, 'in/EndDate')
})

/**
* Update the Travel's TotalPrice when a Booking is deleted.
*/
this.on('CANCEL', Booking.drafts, async (req, next) => {
// Find out which travel is affected before the delete
const { BookingUUID } = req.data
const { to_Travel_TravelUUID } = await SELECT.one
.from(Booking.drafts, b => b.to_Travel_TravelUUID)
.where({ BookingUUID })
// Delete handled by generic handlers
const res = await next()
// After the delete, update the totals
await this._update_totals4(to_Travel_TravelUUID)
return res
})

// Update a Travel's TotalPrice whenever its BookingFee is modified,
// or when a nested Booking is deleted or its FlightPrice is modified,
// or when a nested Supplement is deleted or its Price is modified.

this.on ('UPDATE', Travel.drafts, (req, next) => update_totals (req, next, ['BookingFee', 'GoGreen']))
this.on ('UPDATE', Booking.drafts, (req, next) => update_totals (req, next, ['FlightPrice']))
this.on ('UPDATE', Supplements.drafts, (req, next) => update_totals (req, next, ['Price']))
this.on ('DELETE', Booking.drafts, (req, next) => update_totals (req, next))
this.on ('DELETE', Supplements.drafts, (req, next) => update_totals (req, next))

// Note: using .on handlers as we need to read a Booking's or Supplement's TravelUUID before they are deleted.
async function update_totals (req: cds.Request, next: Function, fields?: string[]) {
if (fields && !fields.some(f => f in req.data)) return next() //> skip if no relevant data changed
const travel = (req.data as Travel).TravelUUID || ( await SELECT.one `to_Travel.TravelUUID as id` .from (req.subject) ).id
await next() // actually UPDATE or DELETE the subject entity
await update_totalsGreen(travel);
await cds.run(`UPDATE ${Travel.drafts} SET TotalPrice = coalesce (BookingFee,0)
+ ( SELECT coalesce (sum(FlightPrice),0) from ${Booking.drafts} where to_Travel_TravelUUID = TravelUUID )
+ ( SELECT coalesce (sum(Price),0) from ${Supplements.drafts} where to_Travel_TravelUUID = TravelUUID )
WHERE TravelUUID = ?`, [travel])
}

/**
* Validate a Travel's edited data before save.
* Trees-for-Tickets: helper to update totals including green flight fee
*/
this.before ('SAVE', Travel, req => {
const { BeginDate, EndDate, BookingFee, to_Agency_AgencyID, to_Customer_CustomerID, to_Booking, TravelStatus_code } = req.data, today = (new Date).toISOString().slice(0,10)

// validate only not rejected travels
if (TravelStatus_code !== TravelStatusCode.Canceled) {
if (BookingFee == null) req.error(400, "Enter a booking fee", "in/BookingFee") // 0 is a valid BookingFee
if (!BeginDate) req.error(400, "Enter a begin date", "in/BeginDate")
if (!EndDate) req.error(400, "Enter an end date", "in/EndDate")
if (!to_Agency_AgencyID) req.error(400, "Enter a travel agency", "in/to_Agency_AgencyID")
if (!to_Customer_CustomerID) req.error(400, "Enter a customer", "in/to_Customer_CustomerID")

for (const booking of to_Booking) {
const { BookingUUID, ConnectionID, FlightDate, FlightPrice, BookingStatus_code, to_Carrier_AirlineID, to_Customer_CustomerID } = booking
if (!ConnectionID) req.error(400, "Enter a flight", `in/to_Booking(BookingUUID='${BookingUUID}',IsActiveEntity=false)/ConnectionID`)
if (!FlightDate) req.error(400, "Enter a flight date", `in/to_Booking(BookingUUID='${BookingUUID}',IsActiveEntity=false)/FlightDate`)
if (!FlightPrice) req.error(400, "Enter a flight price", `in/to_Booking(BookingUUID='${BookingUUID}',IsActiveEntity=false)/FlightPrice`)
if (!BookingStatus_code) req.error(400, "Enter a booking status", `in/to_Booking(BookingUUID='${BookingUUID}',IsActiveEntity=false)/BookingStatus_code`)
if (!to_Carrier_AirlineID) req.error(400, "Enter an airline", `in/to_Booking(BookingUUID='${BookingUUID}',IsActiveEntity=false)/to_Carrier_AirlineID`)
if (!to_Customer_CustomerID) req.error(400, "Enter a customer", `in/to_Booking(BookingUUID='${BookingUUID}',IsActiveEntity=false)/to_Customer_CustomerID`)

for (const suppl of booking.to_BookSupplement) {
const { BookSupplUUID, Price, to_Supplement_SupplementID } = suppl
if (!Price) req.error(400, "Enter a price", `in/to_Booking(BookingUUID='${BookingUUID}',IsActiveEntity=false)/to_BookSupplement(BookSupplUUID='${BookSupplUUID}',IsActiveEntity=false)/Price`)
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`)
}
}
async function update_totalsGreen(TravelUUID: string) {
const { GoGreen } = await SELECT.one .from(Travel.drafts) .columns('GoGreen') .where({ TravelUUID })
if (GoGreen) {
await UPDATE(Travel.drafts, TravelUUID)
.set `GreenFee = round(BookingFee * 0.1, 0)`
.set `TreesPlanted = round(BookingFee * 0.1, 0)`
} else {
await UPDATE(Travel.drafts, TravelUUID)
.set `GreenFee = 0`
.set `TreesPlanted = 0`
}

if (BeginDate < today) req.error (400, `Begin Date ${BeginDate} must not be before today ${today}.`, 'in/BeginDate')
if (BeginDate > EndDate) req.error (400, `Begin Date ${BeginDate} must be before End Date ${EndDate}.`, 'in/BeginDate')
})
}


//
// Action Implementations...
//
const { acceptTravel, rejectTravel, deductDiscount } = Travel.actions

this.on(acceptTravel, req => UPDATE(req.subject).with({ TravelStatus_code: TravelStatusCode.Accepted }))
this.on(rejectTravel, req => UPDATE(req.subject).with({ TravelStatus_code: TravelStatusCode.Canceled }))
this.on(deductDiscount, async req => {
const { acceptTravel, rejectTravel, deductDiscount } = Travel.actions;
this.on (acceptTravel, req => UPDATE (req.subject) .with ({ TravelStatus_code: TravelStatusCode.Accepted }))
this.on (rejectTravel, req => UPDATE (req.subject) .with ({ TravelStatus_code: TravelStatusCode.Canceled }))
this.on (deductDiscount, async req => {
let discount = req.data.percent / 100
let succeeded = await UPDATE(req.subject)
.where `TravelStatus_code != 'A'`
.and `BookingFee is not null`
.set `TotalPrice = round (TotalPrice - BookingFee * ${discount}, 3)`
.set `BookingFee = round (BookingFee - BookingFee * ${discount}, 3)`
let succeeded = await UPDATE (req.subject) .where `TravelStatus.code != 'A'` .and `BookingFee != null`
.with `TotalPrice = round (TotalPrice - BookingFee * ${discount}, 3)`
.with `BookingFee = round (BookingFee - BookingFee * ${discount}, 3)`

if (!succeeded) { //> let's find out why...
let travel = await SELECT.one `TravelID as ID, TravelStatus_code as status, BookingFee` .from(req.subject)
let travel = await SELECT.one `TravelID as ID, TravelStatus.code as status, BookingFee` .from (req.subject)
if (!travel) throw req.reject (404, `Travel "${travel.ID}" does not exist; may have been deleted meanwhile.`)
if (travel.status === TravelStatusCode.Accepted) req.reject (400, `Travel "${travel.ID}" has been approved already.`)
if (travel.status === TravelStatusCode.Accepted) throw req.reject (400, `Travel "${travel.ID}" has been approved already.`)
if (travel.BookingFee == null) throw req.reject (404, `No discount possible, as travel "${travel.ID}" does not yet have a booking fee added.`)
} else {
return SELECT(req.subject)
Expand All @@ -183,35 +103,4 @@ init() {
// Add base class's handlers. Handlers registered above go first.
return super.init()

}

/**
* Helper to re-calculate a Travel's TotalPrice from BookingFees, FlightPrices and Supplement Prices.
*/
async _update_totals4(travel: string) {
await this._update_totalsGreen(travel)
// Using plain native SQL for such complex queries
await cds.run(`UPDATE ${Travel.drafts} SET
TotalPrice = coalesce(BookingFee,0)
+ coalesce(GreenFee,0)
+ ( SELECT coalesce (sum(FlightPrice),0) from ${Booking.drafts} where to_Travel_TravelUUID = TravelUUID )
+ ( SELECT coalesce (sum(Price),0) from ${BookingSupplement.drafts} where to_Travel_TravelUUID = TravelUUID )
WHERE TravelUUID = ?`, [travel])
}

/**
* Trees-for-Tickets: helper to update totals including green flight fee
*/
async _update_totalsGreen(TravelUUID) {
const { GoGreen } = await SELECT.one`GoGreen`.from(Travel.drafts).where({ TravelUUID })
if (GoGreen) {
await UPDATE(Travel.drafts, TravelUUID)
.set`GreenFee = round(BookingFee * 0.1, 0)`
.set`TreesPlanted = round(BookingFee * 0.1, 0)`
} else {
await UPDATE(Travel.drafts, TravelUUID)
.set`GreenFee = 0`
.set`TreesPlanted = 0`
}
}
}
}}