Skip to content

feat: booking#38

Merged
nnh53 merged 1 commit into
mainfrom
booking
Nov 11, 2025
Merged

feat: booking#38
nnh53 merged 1 commit into
mainfrom
booking

Conversation

@nnh53

@nnh53 nnh53 commented Nov 11, 2025

Copy link
Copy Markdown
Member

No description provided.

Copilot AI review requested due to automatic review settings November 11, 2025 21:00
@naming-conventions-bot

Copy link
Copy Markdown

Thank you for following naming conventions! 😻

@nnh53 nnh53 enabled auto-merge (rebase) November 11, 2025 21:00
@nnh53 nnh53 merged commit 3f934d3 into main Nov 11, 2025
9 checks passed
@nnh53 nnh53 deleted the booking branch November 11, 2025 21:01

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR implements a comprehensive booking feature for the car rental application, including vehicle selection, booking creation, payment integration with VnPay, and booking management. The implementation follows Angular v20 patterns with signals, standalone components, and modern control flow syntax.

  • Adds complete booking flow with stepper UI (vehicle selection → checkout → payment → confirmation)
  • Integrates VnPay payment gateway for processing rental payments
  • Implements booking draft persistence using session storage
  • Adds booking list and detail views for renters to track their bookings

Reviewed Changes

Copilot reviewed 32 out of 34 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
booking.routes.ts Reorganizes routes to separate cars and bookings paths with detail views
layout.ts Updates navigation links to point to new /cars and /bookings routes
booking-storage.service.ts New service for persisting booking drafts in session storage
payment.ts New component handling VnPay payment return and success/failure display
checkout.ts Major rewrite implementing booking confirmation and payment initiation
cars-page.ts New stepper container orchestrating the booking flow across multiple steps
car-detail.ts Enhanced with date/time validation, past date checking, and booking data emission
booking-detail.ts New component displaying booking details with QR code for verification
booking.ts (BookingsPage) New component listing renter's bookings grouped by status
car-list.ts Adds filtering for available vehicles only
session.service.ts New service wrapping sessionStorage with JSON serialization support
payments.service.ts New service for VnPay payment creation and return handling
bookings.service.ts Adds methods for creating bookings and fetching renter bookings
user.service.ts Adds renterId signal for tracking current renter
auth.service.ts Extracts and stores renterId from JWT token
package.json Adds angularx-qrcode dependency for QR code generation
Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported
Comments suppressed due to low confidence (1)

src/app/features/customer/booking/pages/cars-page/cars-page.ts:1

  • [nitpick] Chaining multiple method calls in template event binding (nextStep)=\"markPaymentStarted(); goToNextStep()\" reduces readability and testability. Consider creating a single method like onCheckoutComplete() that calls both methods in sequence.
import {

@Component({
selector: 'app-checkout',
imports: [],
standalone: true,

Copilot AI Nov 11, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove standalone: true from component metadata. According to the coding guidelines, Angular v20 defaults to standalone components, so this property should never be explicitly added.

Suggested change
standalone: true,

Copilot uses AI. Check for mistakes.
const id = this.bookingId();
if (id) {
try {
sessionStorage.setItem('lastBookingId', id);

Copilot AI Nov 11, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Direct sessionStorage access bypasses the SessionService abstraction. Use inject(SessionService).setItem('lastBookingId', id) instead to maintain consistent error handling and SSR compatibility.

Copilot uses AI. Check for mistakes.
Comment on lines +70 to +78
readonly vehicleName = computed(() => {
const vehicle = this.bookingData()?.vehicle;
if (!vehicle) return '';

const parts: string[] = [];
if (vehicle.make) parts.push(vehicle.make);
if (vehicle.model) parts.push(vehicle.model);
return parts.length ? parts.join(' ') : 'Xe chưa xác định';
});

Copilot AI Nov 11, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The vehicleName computed logic is duplicated in both Checkout (lines 70-78) and BookingDetail (lines 70-78). Extract this into a shared utility function or method to reduce duplication and ensure consistent vehicle name formatting across components.

Copilot uses AI. Check for mistakes.
}
},
});
this.destroyRef.onDestroy(() => renderRef.destroy());

Copilot AI Nov 11, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Registering a destroy callback inside setActiveStep (which can be called multiple times) will leak memory as each call creates a new destroy listener that never gets cleaned up until component destruction. The afterNextRender callback is automatically cleaned up, so the manual destroyRef.onDestroy registration on line 229 should be removed.

Suggested change
this.destroyRef.onDestroy(() => renderRef.destroy());

Copilot uses AI. Check for mistakes.
@for (metric of vehicleMetrics(); track metric.label) {
<div
class="car-detail__metric flex items-center gap-4 px-4 py-4 first:pt-0 last:pb-0 hover:bg-surface-container-high transition-colors"
class="car-detail__metric flex items-center gap-4 px-4 py-4 hover:bg-surface-container-high transition-colors"

Copilot AI Nov 11, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] Removed first:pt-0 last:pb-0 utility classes. If the first/last item padding adjustment was intentional for visual alignment, this change may affect the layout. Consider whether the padding should be restored or if the parent container's padding should be adjusted instead.

Suggested change
class="car-detail__metric flex items-center gap-4 px-4 py-4 hover:bg-surface-container-high transition-colors"
class="car-detail__metric flex items-center gap-4 px-4 py-4 first:pt-0 last:pb-0 hover:bg-surface-container-high transition-colors"

Copilot uses AI. Check for mistakes.
Comment on lines +64 to +68
getRental(rentalId: string): Observable<RentalDetailsDto> {
return this._rentalService
.apiRentalRentalIdGet(rentalId)
.pipe(
map((response: RentalDetailsDtoApiResponse) => response.data ?? ({} as RentalDetailsDto)),

Copilot AI Nov 11, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Returning an empty object {} as RentalDetailsDto when response.data is null/undefined can lead to unexpected behavior in consuming code. Consider throwing an error or returning null to make the absence of data explicit, forcing callers to handle the error case properly.

Suggested change
getRental(rentalId: string): Observable<RentalDetailsDto> {
return this._rentalService
.apiRentalRentalIdGet(rentalId)
.pipe(
map((response: RentalDetailsDtoApiResponse) => response.data ?? ({} as RentalDetailsDto)),
getRental(rentalId: string): Observable<RentalDetailsDto | null> {
return this._rentalService
.apiRentalRentalIdGet(rentalId)
.pipe(
map((response: RentalDetailsDtoApiResponse) => response.data ?? null),

Copilot uses AI. Check for mistakes.
Comment on lines +69 to +73
getRenterProfile(renterId: string): Observable<RenterProfileDto> {
return this._bookingService
.apiBookingRenterProfileGet(renterId)
.pipe(
map((response: RenterProfileDtoApiResponse) => response.data ?? ({} as RenterProfileDto)),

Copilot AI Nov 11, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Returning an empty object {} as RenterProfileDto when response.data is null/undefined can lead to unexpected behavior. Consider throwing an error or returning null to make the absence of data explicit, forcing callers to handle the error case.

Suggested change
getRenterProfile(renterId: string): Observable<RenterProfileDto> {
return this._bookingService
.apiBookingRenterProfileGet(renterId)
.pipe(
map((response: RenterProfileDtoApiResponse) => response.data ?? ({} as RenterProfileDto)),
getRenterProfile(renterId: string): Observable<RenterProfileDto | null> {
return this._bookingService
.apiBookingRenterProfileGet(renterId)
.pipe(
map((response: RenterProfileDtoApiResponse) => response.data ?? null),

Copilot uses AI. Check for mistakes.
Comment on lines +210 to +212
setTimeout(() => {
window.location.href = paymentUrl;
}, 100);

Copilot AI Nov 11, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using a magic number timeout of 100ms is fragile and may not be sufficient on slower devices. The comment on line 209 suggests this is to allow parent state updates, but a better approach would be to use afterNextRender to ensure the UI has updated before navigation, or emit the event and let the parent handle the redirect.

Copilot uses AI. Check for mistakes.
private readonly destroyRef = inject(DestroyRef);

readonly hourOptions = HOUR_OPTIONS;
readonly today = this.stripTime(new Date());

Copilot AI Nov 11, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The today signal is initialized with this.stripTime(new Date()) which will be stale if the component lives past midnight. While unlikely to cause issues in a booking flow, consider using a computed signal that regenerates the date value, or document this limitation if the current behavior is acceptable.

Suggested change
readonly today = this.stripTime(new Date());
// Signal for the current time, updated at midnight to ensure 'today' is always correct
private readonly now = signal(new Date());
// Effect to update 'now' at the next midnight (and every midnight thereafter)
private readonly _midnightUpdater = effect(() => {
const now = new Date();
const nextMidnight = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1, 0, 0, 0, 0);
const msUntilMidnight = nextMidnight.getTime() - now.getTime();
const timeout = setTimeout(() => {
this.now.set(new Date());
}, msUntilMidnight);
return () => clearTimeout(timeout);
});
readonly today = computed(() => this.stripTime(this.now()));

Copilot uses AI. Check for mistakes.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +79 to +106
// Handle payment return only; ignore external step changes via URL
this.route.queryParams.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((params) => {
// Check if this is a payment return (VnPay usually returns with vnp_* params)
const hasPaymentParams = Object.keys(params).some((key) => key.startsWith('vnp_'));
if (hasPaymentParams) {
this.handlePaymentReturn();
}
});

this.route.fragment.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((fragment) => {
const normalized = fragment?.trim().toLowerCase();
if (!normalized) {
return;
}

const targetIndex = this.steps.findIndex((step) => step.key === normalized);
if (targetIndex < 0 || targetIndex === this.activeStep()) {
return;
}

this.setActiveStep(targetIndex, { updateFragment: false });
});
}

private handlePaymentReturn(): void {
this.paymentsService
.getPaymentReturn()
.pipe(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Skip forwarding VnPay return params to backend

When the user is redirected back from VnPay, you detect the presence of vnp_* query parameters but then call paymentsService.getPaymentReturn() without forwarding any of those values. The generated apiPaymentReturnGet() endpoint issues a plain GET to /api/Payment/return; without the signature, amount, order id, etc. from the query string the server cannot validate or even identify the transaction, so the verification call will consistently fail or return empty data even though the client has the required parameters. Pass the current query parameters through to the service so that the backend can verify the payment result.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants