Last active
June 7, 2020 15:18
-
-
Save WoH/9e8778bbaefa3c4e60cbc0a5ecd8aff2 to your computer and use it in GitHub Desktop.
Order Ships
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { | |
Controller, | |
Route, | |
Security, | |
Post, | |
Request, | |
Body, | |
Res, | |
TsoaResponse, | |
Tags, | |
Response, | |
} from "tsoa"; | |
import { provideSingleton } from "../util/provideSingleton"; | |
import { InjectUser } from "../util/userDecorator"; | |
import { inject } from "inversify"; | |
abstract class Bookable { | |
abstract isAvailable(id: UUID, start: Date, end: Date): Promise<boolean>; | |
abstract reserve( | |
id: UUID, | |
start: Date, | |
end: Date | |
): Promise<Reservation | null>; | |
abstract book(id: UUID, start: Date, end: Date): Promise<boolean>; | |
abstract exists(id: UUID): Promise<boolean>; | |
abstract calculatePrice(id: UUID, start: Date, end: Date): number; | |
} | |
abstract class CaptainService extends Bookable { | |
abstract reserve( | |
id: UUID, | |
start: Date, | |
end: Date | |
): Promise<CaptainReservation | null>; | |
} | |
abstract class BoatService extends Bookable { | |
abstract reserve( | |
id: UUID, | |
start: Date, | |
end: Date | |
): Promise<BoatReservation | null>; | |
abstract getAvailableBoatId(start: Date, end: Date): Promise<UUID | null>; | |
abstract reserveVests( | |
amount: number, | |
start: Date, | |
end: Date | |
): Promise<true | null>; | |
} | |
abstract class ShipService extends Bookable { | |
abstract reserve( | |
id: UUID, | |
start: Date, | |
end: Date | |
): Promise<ShipReservation | null>; | |
} | |
abstract class ChargeAccountService { | |
abstract get(chargeAccountId: UUID, userId: UUID): Promise<ChargeAccount>; | |
} | |
abstract class OrderService { | |
abstract book( | |
chargeAccountId: ChargeAccount, | |
reservations: Reservation[] | |
): Promise<Order | null>; | |
} | |
@Route("/orders") | |
@provideSingleton(OrdersController) | |
@Security("jwt") | |
@Tags("order") | |
@Response<UnauthorizedErrorMessage>(401, "Unauthorized") | |
@Response<ValidateErrorMessage>(422, "Validation failed") | |
export class OrdersController extends Controller { | |
constructor( | |
@inject(OrderService) private orderService: OrderService, | |
@inject(ShipService) private shipService: ShipService, | |
@inject(CaptainService) private captainService: CaptainService, | |
@inject(BoatService) private boatService: BoatService, | |
@inject(ChargeAccountService) | |
private chargeAccountService: ChargeAccountService | |
) { | |
super(); | |
} | |
/** | |
* This endpoint is used to rent a boat or a ship. | |
* @summary Add a new rental order. | |
* @param badRequest Bad Request | |
* @param paymentRequired Insufficient funds available | |
* @param notFound Not Found | |
* @param requestBody The Create Order payload | |
*/ | |
@Post() | |
public async createOrder( | |
@Request() @InjectUser() requestor: User, | |
@Res() badRequest: TsoaResponse<400, ErrorMessage>, | |
@Res() paymentRequired: TsoaResponse<402, ErrorMessage>, | |
@Res() notFound: TsoaResponse<404, ErrorMessage>, | |
@Body() | |
requestBody: CreateOrderBody | |
): Promise<Order> { | |
const isShipBooking = ( | |
config: BoatConfiguration | ShipConfiguration | |
): config is ShipConfiguration => config.hasOwnProperty("shipId"); | |
const { configuration, startTime, endTime, chargeAccountId } = requestBody; | |
let reservations: Reservation[] = []; | |
if (isShipBooking(configuration)) { | |
if (!(await this.shipService.exists(configuration.shipId))) { | |
return notFound(404, { message: "Ship with this id not exist" }); | |
} | |
const shipReservation = await this.captainService.reserve( | |
configuration.shipId, | |
startTime, | |
endTime | |
); | |
if (!shipReservation) { | |
return badRequest(400, { | |
message: "The requested ship is not available at this time", | |
}); | |
} | |
reservations.push(shipReservation); | |
if (configuration.captainId) { | |
if (!this.captainService.exists(configuration.captainId)) { | |
return notFound(404, { | |
message: "Captain with this id does not exist", | |
}); | |
} | |
const captainReservation = await this.captainService.reserve( | |
configuration.captainId, | |
startTime, | |
endTime | |
); | |
if (!captainReservation) { | |
return badRequest(400, { | |
message: "The requested captain is not available at this time", | |
}); | |
} | |
reservations.push(captainReservation); | |
} | |
} else { | |
const boatId = await this.boatService.getAvailableBoatId( | |
startTime, | |
endTime | |
); | |
if (!boatId) { | |
return badRequest(400, { message: "No boat available at that time" }); | |
} | |
const boatReservation = await this.boatService.reserve( | |
boatId, | |
startTime, | |
endTime | |
); | |
configuration.lifevests && | |
(await this.boatService.reserveVests( | |
configuration.lifevests, | |
startTime, | |
endTime | |
)); | |
reservations.push(boatReservation); | |
} | |
const chargeAccount = await this.chargeAccountService.get( | |
requestor.id, | |
chargeAccountId | |
); | |
if (!chargeAccount) { | |
return notFound(404, { message: "ChargeAccount not found" }); | |
} | |
const order = await this.orderService.book(chargeAccount, reservations); | |
if (!order) { | |
return paymentRequired(402, { message: "Missing funds" }); | |
} | |
return order; | |
} | |
} | |
interface CreateOrderBody { | |
/** | |
* Time when the rental period begins | |
*/ | |
startTime: Date; | |
/** | |
* Time at which the rentals are returned | |
*/ | |
endTime: Date; | |
configuration: ShipConfiguration | BoatConfiguration; | |
/** | |
* uuid of the charge account used to pay for the rental | |
*/ | |
chargeAccountId: UUID; | |
} | |
interface ShipConfiguration { | |
/** | |
* uuid of the ship to rent | |
* @isInt | |
*/ | |
shipId: UUID; | |
/** | |
* uuid of the captain used to navigate the ship. | |
* In case the renter is allowed to navigate the ship, the capitainId should be explicitly set to null. | |
* @isInt | |
*/ | |
captainId: UUID | null; | |
} | |
interface BoatConfiguration { | |
/** | |
* @isInt | |
* @maximum 8 | |
* @minimum 0 | |
*/ | |
lifevests?: number; | |
} | |
interface Order {} | |
interface ChargeAccount {} | |
interface Ship {} | |
interface Captain {} | |
interface Boat { | |
lifevests: number; | |
} | |
interface Reservation { | |
type: string; | |
} | |
interface ShipReservation extends Reservation { | |
type: "ship"; | |
ship: Ship; | |
} | |
interface CaptainReservation extends Reservation { | |
type: "captain"; | |
captain: Captain; | |
} | |
interface BoatReservation extends Reservation { | |
type: "boat"; | |
boat: Boat; | |
} | |
interface ErrorMessage { | |
message: string; | |
details?: string; | |
} | |
interface ValidateErrorMessage { | |
message: "Validation failed"; | |
details: { [name: string]: unknown }; | |
} | |
interface UnauthorizedErrorMessage extends ErrorMessage { | |
message: "Unauthorized"; | |
} | |
/** | |
* Stringified UUIDv4. | |
* See [RFC 4112](https://tools.ietf.org/html/rfc4122) | |
* @pattern [0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-4[0-9A-Fa-f]{3}-[89ABab][0-9A-Fa-f]{3}-[0-9A-Fa-f]{12} | |
* @example "52907745-7672-470e-a803-a2f8feb52944" | |
*/ | |
export type UUID = string; | |
interface User { | |
id: UUID; | |
name: string; | |
email: string; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment