-
Notifications
You must be signed in to change notification settings - Fork 2
Add staff booking workflow detail page #36
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,245 @@ | ||||||||
| import { inject, Injectable, signal } from '@angular/core'; | ||||||||
| import { Observable, catchError, finalize, map, switchMap, tap, throwError } from 'rxjs'; | ||||||||
| import { | ||||||||
| BookingService, | ||||||||
| BookingVerificationStatus, | ||||||||
| CheckinBookingRequest, | ||||||||
| CreateContractRequest, | ||||||||
| CreateRentalRequest, | ||||||||
| EsignProvider, | ||||||||
| GuidApiResponse, | ||||||||
| PartyRole, | ||||||||
| ReceiveInspectionRequest, | ||||||||
| ReceiveVehicleRequest, | ||||||||
| RentalService, | ||||||||
| SignContractRequest, | ||||||||
| SignatureEvent, | ||||||||
| SignatureType, | ||||||||
| } from '../../../contract'; | ||||||||
|
|
||||||||
| export type BookingWorkflowStep = | ||||||||
| | 'idle' | ||||||||
| | 'checkingIn' | ||||||||
| | 'creatingRental' | ||||||||
| | 'creatingContract' | ||||||||
| | 'recordingInspection' | ||||||||
| | 'signingContractRenter' | ||||||||
| | 'signingContractStaff' | ||||||||
| | 'receivingVehicle' | ||||||||
| | 'completed'; | ||||||||
|
|
||||||||
| export interface SignatureESignPayload { | ||||||||
| readonly signerIp?: string; | ||||||||
| readonly userAgent?: string; | ||||||||
| readonly providerSignatureId?: string; | ||||||||
| readonly signatureImageUrl?: string; | ||||||||
| readonly certSubject?: string; | ||||||||
| readonly certIssuer?: string; | ||||||||
| readonly certSerial?: string; | ||||||||
| readonly certFingerprintSha256?: string; | ||||||||
| readonly signatureHash?: string; | ||||||||
| readonly evidenceUrl?: string; | ||||||||
| } | ||||||||
|
|
||||||||
| export interface ContractSignaturePayload { | ||||||||
| readonly role: PartyRole; | ||||||||
| readonly signatureEvent: SignatureEvent; | ||||||||
| readonly type: SignatureType; | ||||||||
| readonly signedAt: string; | ||||||||
| readonly documentUrl?: string; | ||||||||
| readonly documentHash?: string; | ||||||||
| readonly eSignPayload?: SignatureESignPayload; | ||||||||
| } | ||||||||
|
|
||||||||
| export interface BookingWorkflowPayload { | ||||||||
| readonly bookingId: string; | ||||||||
| readonly verificationStatus: BookingVerificationStatus; | ||||||||
| readonly verifiedByStaffId: string; | ||||||||
| readonly rental: { | ||||||||
| readonly startTime?: string; | ||||||||
| readonly endTime?: string; | ||||||||
| }; | ||||||||
| readonly contract: { | ||||||||
| readonly provider: EsignProvider; | ||||||||
| }; | ||||||||
| readonly inspection: { | ||||||||
| readonly currentBatteryCapacityKwh: number; | ||||||||
| readonly inspectedAt: string; | ||||||||
| readonly inspectorStaffId: string; | ||||||||
| readonly url?: string; | ||||||||
| }; | ||||||||
| readonly signatures: readonly [ContractSignaturePayload, ContractSignaturePayload]; | ||||||||
| readonly receive: { | ||||||||
| readonly receivedAt: string; | ||||||||
| readonly receivedByStaffId: string; | ||||||||
| }; | ||||||||
| } | ||||||||
|
|
||||||||
| export interface BookingWorkflowResult { | ||||||||
| readonly bookingId: string; | ||||||||
| readonly rentalId: string; | ||||||||
| readonly contractId: string; | ||||||||
| readonly inspectionId: string; | ||||||||
| readonly signatureIds: readonly [string, string]; | ||||||||
| } | ||||||||
|
|
||||||||
| @Injectable({ providedIn: 'root' }) | ||||||||
| export class BookingWorkflowService { | ||||||||
| private readonly _bookingService = inject(BookingService); | ||||||||
| private readonly _rentalService = inject(RentalService); | ||||||||
|
|
||||||||
| private readonly _loading = signal(false); | ||||||||
| private readonly _error = signal<string | null>(null); | ||||||||
| private readonly _step = signal<BookingWorkflowStep>('idle'); | ||||||||
| private readonly _result = signal<BookingWorkflowResult | null>(null); | ||||||||
|
|
||||||||
| readonly loading = this._loading.asReadonly(); | ||||||||
| readonly error = this._error.asReadonly(); | ||||||||
| readonly step = this._step.asReadonly(); | ||||||||
| readonly result = this._result.asReadonly(); | ||||||||
|
|
||||||||
| process(payload: BookingWorkflowPayload): Observable<BookingWorkflowResult> { | ||||||||
| this._loading.set(true); | ||||||||
| this._error.set(null); | ||||||||
| this._result.set(null); | ||||||||
| this._step.set('checkingIn'); | ||||||||
|
|
||||||||
| const checkinRequest: CheckinBookingRequest = { | ||||||||
| bookingId: payload.bookingId, | ||||||||
| verifiedByStaffId: payload.verifiedByStaffId, | ||||||||
| bookingVerificationStatus: payload.verificationStatus, | ||||||||
| }; | ||||||||
|
|
||||||||
| return this._bookingService.apiBookingCheckinPost(checkinRequest).pipe( | ||||||||
| switchMap(() => { | ||||||||
| this._step.set('creatingRental'); | ||||||||
| const rentalRequest: CreateRentalRequest = { | ||||||||
| bookingId: payload.bookingId, | ||||||||
| startTime: payload.rental.startTime, | ||||||||
| endTime: payload.rental.endTime, | ||||||||
| }; | ||||||||
| return this._rentalService.apiRentalPost(rentalRequest); | ||||||||
| }), | ||||||||
| map((rentalResponse: GuidApiResponse) => this._extractGuid(rentalResponse, 'rental')), | ||||||||
| switchMap((rentalId) => { | ||||||||
| this._step.set('creatingContract'); | ||||||||
| const contractRequest: CreateContractRequest = { | ||||||||
| rentalId, | ||||||||
| provider: payload.contract.provider, | ||||||||
| }; | ||||||||
| return this._rentalService.apiRentalContractPost(contractRequest).pipe( | ||||||||
| map((contractResponse: GuidApiResponse) => ({ | ||||||||
| rentalId, | ||||||||
| contractId: this._extractGuid(contractResponse, 'contract'), | ||||||||
| })), | ||||||||
| ); | ||||||||
| }), | ||||||||
| switchMap(({ rentalId, contractId }) => { | ||||||||
| this._step.set('recordingInspection'); | ||||||||
| const inspectionRequest: ReceiveInspectionRequest = { | ||||||||
| rentalId, | ||||||||
| currentBatteryCapacityKwh: payload.inspection.currentBatteryCapacityKwh, | ||||||||
| inspectedAt: payload.inspection.inspectedAt, | ||||||||
| inspectorStaffId: payload.inspection.inspectorStaffId, | ||||||||
| url: payload.inspection.url ?? null, | ||||||||
| }; | ||||||||
| return this._rentalService.apiRentalInspectionPost(inspectionRequest).pipe( | ||||||||
| map((inspectionResponse: GuidApiResponse) => ({ | ||||||||
| rentalId, | ||||||||
| contractId, | ||||||||
| inspectionId: this._extractGuid(inspectionResponse, 'inspection'), | ||||||||
| })), | ||||||||
| ); | ||||||||
| }), | ||||||||
| switchMap(({ rentalId, contractId, inspectionId }) => { | ||||||||
| this._step.set('signingContractRenter'); | ||||||||
| const [renterSignature, staffSignature] = payload.signatures; | ||||||||
| return this._signContract(contractId, renterSignature).pipe( | ||||||||
| switchMap((firstSignatureId) => { | ||||||||
| this._step.set('signingContractStaff'); | ||||||||
| return this._signContract(contractId, staffSignature).pipe( | ||||||||
| map((secondSignatureId) => ({ | ||||||||
| rentalId, | ||||||||
| contractId, | ||||||||
| inspectionId, | ||||||||
| signatureIds: [firstSignatureId, secondSignatureId] as const, | ||||||||
| })), | ||||||||
| ); | ||||||||
| }), | ||||||||
| ); | ||||||||
| }), | ||||||||
| switchMap(({ rentalId, contractId, inspectionId, signatureIds }) => { | ||||||||
| this._step.set('receivingVehicle'); | ||||||||
| const receiveRequest: ReceiveVehicleRequest = { | ||||||||
| rentalId, | ||||||||
| receivedAt: payload.receive.receivedAt, | ||||||||
| receivedByStaffId: payload.receive.receivedByStaffId, | ||||||||
| }; | ||||||||
| return ( | ||||||||
| this._rentalService.apiRentalVehicleReceivePost(receiveRequest) as Observable<unknown> | ||||||||
| ).pipe( | ||||||||
| map(() => ({ | ||||||||
| bookingId: payload.bookingId, | ||||||||
| rentalId, | ||||||||
| contractId, | ||||||||
| inspectionId, | ||||||||
| signatureIds, | ||||||||
| })), | ||||||||
| ); | ||||||||
| }), | ||||||||
| tap((result) => { | ||||||||
| this._result.set(result); | ||||||||
| this._step.set('completed'); | ||||||||
| }), | ||||||||
| catchError((error: unknown) => { | ||||||||
| this._error.set(this._resolveErrorMessage(error)); | ||||||||
| return throwError(() => error); | ||||||||
| }), | ||||||||
| finalize(() => { | ||||||||
| this._loading.set(false); | ||||||||
| if (this._step() !== 'completed') { | ||||||||
| this._step.set('idle'); | ||||||||
|
||||||||
| this._step.set('idle'); | |
| this._step.set('idle'); | |
| this._error.set(null); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The inspection URL is converted from
string | undefinedtostring | null. This inconsistency could cause issues since the OpenAPI contract might expect one format over the other. Verify that the API acceptsnullfor optional string fields, or keep it asundefined.